2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
10 local jid_bare
= require
"util.jid".bare
;
11 local jid_split
= require
"util.jid".split
;
12 local st
= require
"util.stanza";
13 local is_contact_subscribed
= require
"core.rostermanager".is_contact_subscribed
;
17 local calculate_hash
= require
"util.caps".calculate_hash
;
18 local core_post_stanza
= prosody
.core_post_stanza
;
19 local bare_sessions
= prosody
.bare_sessions
;
21 local xmlns_pubsub
= "http://jabber.org/protocol/pubsub";
23 -- Used as canonical 'empty table'
25 -- data[user_bare_jid][node] = item_stanza
27 --- recipients[user_bare_jid][contact_full_jid][subscribed_node] = true
28 local recipients
= {};
29 -- hash_map[hash][subscribed_nodes] = true
32 module
.save
= function()
33 return { data
= data
, recipients
= recipients
, hash_map
= hash_map
};
35 module
.restore
= function(state
)
36 data
= state
.data
or {};
37 recipients
= state
.recipients
or {};
38 hash_map
= state
.hash_map
or {};
41 local function subscription_presence(user_bare
, recipient
)
42 local recipient_bare
= jid_bare(recipient
);
43 if (recipient_bare
== user_bare
) then return true end
44 local username
, host
= jid_split(user_bare
);
45 return is_contact_subscribed(username
, host
, recipient_bare
);
48 module
:hook("pep-publish-item", function (event
)
49 local session
, bare
, node
, id
, item
= event
.session
, event
.user
, event
.node
, event
.id
, event
.item
;
50 item
.attr
.xmlns
= nil;
51 local disable
= #item
.tags
~= 1 or #item
.tags
[1] == 0;
52 if #item
.tags
== 0 then item
.name
= "retract"; end
53 local stanza
= st
.message({from
=bare
, type='headline'})
54 :tag('event', {xmlns
='http://jabber.org/protocol/pubsub#event'})
55 :tag('items', {node
=node
})
60 -- store for the future
61 local user_data
= data
[bare
];
64 user_data
[node
] = nil;
65 if not next(user_data
) then data
[bare
] = nil; end
68 if not user_data
then user_data
= {}; data
[bare
] = user_data
; end
69 user_data
[node
] = {id
, item
};
73 for recipient
, notify
in pairs(recipients
[bare
] or NULL
) do
75 stanza
.attr
.to
= recipient
;
76 core_post_stanza(session
, stanza
);
81 local function publish_all(user
, recipient
, session
)
83 local notify
= recipients
[user
] and recipients
[user
][recipient
];
85 for node
in pairs(notify
) do
87 local id
, item
= unpack(d
[node
]);
88 session
.send(st
.message({from
=user
, to
=recipient
, type='headline'})
89 :tag('event', {xmlns
='http://jabber.org/protocol/pubsub#event'})
90 :tag('items', {node
=node
})
99 local function get_caps_hash_from_presence(stanza
, current
)
100 local t
= stanza
.attr
.type;
102 for _
, child
in pairs(stanza
.tags
) do
103 if child
.name
== "c" and child
.attr
.xmlns
== "http://jabber.org/protocol/caps" then
104 local attr
= child
.attr
;
105 if attr
.hash
then -- new caps
106 if attr
.hash
== 'sha-1' and attr
.node
and attr
.ver
then return attr
.ver
, attr
.node
.."#"..attr
.ver
; end
108 if attr
.node
and attr
.ver
then return attr
.node
.."#"..attr
.ver
.."#"..(attr
.ext
or ""), attr
.node
.."#"..attr
.ver
; end
110 return; -- bad caps format
113 elseif t
== "unavailable" or t
== "error" then
116 return current
; -- no caps, could mean caps optimization, so return current
119 module
:hook("presence/bare", function(event
)
120 -- inbound presence to bare JID received
121 local origin
, stanza
= event
.origin
, event
.stanza
;
122 local user
= stanza
.attr
.to
or (origin
.username
..'@'..origin
.host
);
123 local t
= stanza
.attr
.type;
124 local self
= not stanza
.attr
.to
;
126 -- Only cache subscriptions if user is online
127 if not bare_sessions
[user
] then return; end
129 if not t
then -- available presence
130 if self
or subscription_presence(user
, stanza
.attr
.from
) then
131 local recipient
= stanza
.attr
.from
;
132 local current
= recipients
[user
] and recipients
[user
][recipient
];
133 local hash
= get_caps_hash_from_presence(stanza
, current
);
134 if current
== hash
or (current
and current
== hash_map
[hash
]) then return; end
136 if recipients
[user
] then recipients
[user
][recipient
] = nil; end
138 recipients
[user
] = recipients
[user
] or {};
139 if hash_map
[hash
] then
140 recipients
[user
][recipient
] = hash_map
[hash
];
141 publish_all(user
, recipient
, origin
);
143 recipients
[user
][recipient
] = hash
;
144 local from_bare
= origin
.type == "c2s" and origin
.username
.."@"..origin
.host
;
145 if self
or origin
.type ~= "c2s" or (recipients
[from_bare
] and recipients
[from_bare
][origin
.full_jid
]) ~= hash
then
146 -- COMPAT from ~= stanza.attr.to because OneTeam and Asterisk 1.8 can't deal with missing from attribute
148 st
.stanza("iq", {from
=user
, to
=stanza
.attr
.from
, id
="disco", type="get"})
149 :query("http://jabber.org/protocol/disco#info")
155 elseif t
== "unavailable" then
156 if recipients
[user
] then recipients
[user
][stanza
.attr
.from
] = nil; end
157 elseif not self
and t
== "unsubscribe" then
158 local from
= jid_bare(stanza
.attr
.from
);
159 local subscriptions
= recipients
[user
];
160 if subscriptions
then
161 for subscriber
in pairs(subscriptions
) do
162 if jid_bare(subscriber
) == from
then
163 recipients
[user
][subscriber
] = nil;
170 module
:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event
)
171 local session
, stanza
= event
.origin
, event
.stanza
;
172 local payload
= stanza
.tags
[1];
174 if stanza
.attr
.type == 'set' and (not stanza
.attr
.to
or jid_bare(stanza
.attr
.from
) == stanza
.attr
.to
) then
175 payload
= payload
.tags
[1]; -- <publish node='http://jabber.org/protocol/tune'>
176 if payload
and (payload
.name
== 'publish' or payload
.name
== 'retract') and payload
.attr
.node
then
177 local node
= payload
.attr
.node
;
178 payload
= payload
.tags
[1];
179 if payload
and payload
.name
== "item" then -- <item>
180 local id
= payload
.attr
.id
or "1";
181 payload
.attr
.id
= id
;
182 session
.send(st
.reply(stanza
));
183 module
:fire_event("pep-publish-item", {
184 node
= node
, user
= jid_bare(session
.full_jid
), actor
= session
.jid
,
185 id
= id
, session
= session
, item
= st
.clone(payload
);
189 module
:log("debug", "Payload is missing the <item>", node
);
192 module
:log("debug", "Unhandled payload: %s", payload
and payload
:top_tag() or "(no payload)");
194 elseif stanza
.attr
.type == 'get' then
195 local user
= stanza
.attr
.to
and jid_bare(stanza
.attr
.to
) or session
.username
..'@'..session
.host
;
196 if subscription_presence(user
, stanza
.attr
.from
) then
197 local user_data
= data
[user
];
198 local node
, requested_id
;
199 payload
= payload
.tags
[1];
200 if payload
and payload
.name
== 'items' then
201 node
= payload
.attr
.node
;
202 local item
= payload
.tags
[1];
203 if item
and item
.name
== "item" then
204 requested_id
= item
.attr
.id
;
207 if node
and user_data
and user_data
[node
] then -- Send the last item
208 local id
, item
= unpack(user_data
[node
]);
209 if not requested_id
or id
== requested_id
then
210 local reply_stanza
= st
.reply(stanza
)
211 :tag('pubsub', {xmlns
='http://jabber.org/protocol/pubsub'})
212 :tag('items', {node
=node
})
216 session
.send(reply_stanza
);
218 else -- requested item doesn't exist
219 local reply_stanza
= st
.reply(stanza
)
220 :tag('pubsub', {xmlns
='http://jabber.org/protocol/pubsub'})
221 :tag('items', {node
=node
})
223 session
.send(reply_stanza
);
226 elseif node
then -- node doesn't exist
227 session
.send(st
.error_reply(stanza
, 'cancel', 'item-not-found'));
228 module
:log("debug", "Item '%s' not found", node
)
230 else --invalid request
231 session
.send(st
.error_reply(stanza
, 'modify', 'bad-request'));
232 module
:log("debug", "Invalid request: %s", tostring(payload
));
235 else --no presence subscription
236 session
.send(st
.error_reply(stanza
, 'auth', 'not-authorized')
237 :tag('presence-subscription-required', {xmlns
='http://jabber.org/protocol/pubsub#errors'}));
238 module
:log("debug", "Unauthorized request: %s", tostring(payload
));
244 module
:hook("iq-result/bare/disco", function(event
)
245 local session
, stanza
= event
.origin
, event
.stanza
;
246 if stanza
.attr
.type == "result" then
247 local disco
= stanza
.tags
[1];
248 if disco
and disco
.name
== "query" and disco
.attr
.xmlns
== "http://jabber.org/protocol/disco#info" then
249 -- Process disco response
250 local self
= not stanza
.attr
.to
;
251 local user
= stanza
.attr
.to
or (session
.username
..'@'..session
.host
);
252 local contact
= stanza
.attr
.from
;
253 local current
= recipients
[user
] and recipients
[user
][contact
];
254 if type(current
) ~= "string" then return; end -- check if waiting for recipient's response
256 if not string.find(current
, "#") then
257 ver
= calculate_hash(disco
.tags
); -- calculate hash
260 for _
, feature
in pairs(disco
.tags
) do
261 if feature
.name
== "feature" and feature
.attr
.var
then
262 local nfeature
= feature
.attr
.var
:match("^(.*)%+notify$");
263 if nfeature
then notify
[nfeature
] = true; end
266 hash_map
[ver
] = notify
; -- update hash map
268 for jid
, item
in pairs(session
.roster
) do -- for all interested contacts
269 if item
.subscription
== "both" or item
.subscription
== "from" then
270 if not recipients
[jid
] then recipients
[jid
] = {}; end
271 recipients
[jid
][contact
] = notify
;
272 publish_all(jid
, contact
, session
);
276 recipients
[user
][contact
] = notify
; -- set recipient's data to calculated data
277 -- send messages to recipient
278 publish_all(user
, contact
, session
);
283 module
:hook("account-disco-info", function(event
)
284 local reply
= event
.reply
;
285 reply
:tag('identity', {category
='pubsub', type='pep'}):up();
286 reply
:tag('feature', {var
=xmlns_pubsub
}):up();
291 "filtered-notifications",
294 "presence-notifications",
295 "presence-subscribe",
300 for _
, feature
in ipairs(features
) do
301 reply
:tag('feature', {var
=xmlns_pubsub
.."#"..feature
}):up();
305 module
:hook("account-disco-items", function(event
)
306 local reply
= event
.reply
;
307 local bare
= reply
.attr
.to
;
308 local user_data
= data
[bare
];
311 for node
, _
in pairs(user_data
) do
312 reply
:tag('item', {jid
=bare
, node
=node
}):up();
317 module
:hook("account-disco-info-node", function (event
)
318 local stanza
, node
= event
.stanza
, event
.node
;
319 local user
= stanza
.attr
.to
;
320 local user_data
= data
[user
];
321 if user_data
and user_data
[node
] then
323 event
.reply
:tag('identity', {category
='pubsub', type='leaf'}):up();
327 module
:hook("resource-unbind", function (event
)
328 local user_bare_jid
= event
.session
.username
.."@"..event
.session
.host
;
329 if not bare_sessions
[user_bare_jid
] then -- User went offline
330 -- We don't need this info cached anymore, clear it.
331 recipients
[user_bare_jid
] = nil;