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 unpack
= table.unpack
or unpack
; -- luacheck: ignore 113
18 local calculate_hash
= require
"util.caps".calculate_hash
;
19 local core_post_stanza
= prosody
.core_post_stanza
;
20 local bare_sessions
= prosody
.bare_sessions
;
22 local xmlns_pubsub
= "http://jabber.org/protocol/pubsub";
24 -- Used as canonical 'empty table'
26 -- data[user_bare_jid][node] = item_stanza
28 --- recipients[user_bare_jid][contact_full_jid][subscribed_node] = true
29 local recipients
= {};
30 -- hash_map[hash][subscribed_nodes] = true
33 module
.save
= function()
34 return { data
= data
, recipients
= recipients
, hash_map
= hash_map
};
36 module
.restore
= function(state
)
37 data
= state
.data
or {};
38 recipients
= state
.recipients
or {};
39 hash_map
= state
.hash_map
or {};
42 local function subscription_presence(user_bare
, recipient
)
43 local recipient_bare
= jid_bare(recipient
);
44 if (recipient_bare
== user_bare
) then return true end
45 local username
, host
= jid_split(user_bare
);
46 return is_contact_subscribed(username
, host
, recipient_bare
);
49 module
:hook("pep-publish-item", function (event
)
50 local session
, bare
, node
, id
, item
= event
.session
, event
.user
, event
.node
, event
.id
, event
.item
;
51 item
.attr
.xmlns
= nil;
52 local disable
= #item
.tags
~= 1 or #item
.tags
[1] == 0;
53 if #item
.tags
== 0 then item
.name
= "retract"; end
54 local stanza
= st
.message({from
=bare
, type='headline'})
55 :tag('event', {xmlns
='http://jabber.org/protocol/pubsub#event'})
56 :tag('items', {node
=node
})
61 -- store for the future
62 local user_data
= data
[bare
];
65 user_data
[node
] = nil;
66 if not next(user_data
) then data
[bare
] = nil; end
69 if not user_data
then user_data
= {}; data
[bare
] = user_data
; end
70 user_data
[node
] = {id
, item
};
74 for recipient
, notify
in pairs(recipients
[bare
] or NULL
) do
76 stanza
.attr
.to
= recipient
;
77 core_post_stanza(session
, stanza
);
82 local function publish_all(user
, recipient
, session
)
84 local notify
= recipients
[user
] and recipients
[user
][recipient
];
86 for node
in pairs(notify
) do
88 local id
, item
= unpack(d
[node
]);
89 session
.send(st
.message({from
=user
, to
=recipient
, type='headline'})
90 :tag('event', {xmlns
='http://jabber.org/protocol/pubsub#event'})
91 :tag('items', {node
=node
})
100 local function get_caps_hash_from_presence(stanza
, current
)
101 local t
= stanza
.attr
.type;
103 for _
, child
in pairs(stanza
.tags
) do
104 if child
.name
== "c" and child
.attr
.xmlns
== "http://jabber.org/protocol/caps" then
105 local attr
= child
.attr
;
106 if attr
.hash
then -- new caps
107 if attr
.hash
== 'sha-1' and attr
.node
and attr
.ver
then return attr
.ver
, attr
.node
.."#"..attr
.ver
; end
109 if attr
.node
and attr
.ver
then return attr
.node
.."#"..attr
.ver
.."#"..(attr
.ext
or ""), attr
.node
.."#"..attr
.ver
; end
111 return; -- bad caps format
114 elseif t
== "unavailable" or t
== "error" then
117 return current
; -- no caps, could mean caps optimization, so return current
120 module
:hook("presence/bare", function(event
)
121 -- inbound presence to bare JID received
122 local origin
, stanza
= event
.origin
, event
.stanza
;
123 local user
= stanza
.attr
.to
or (origin
.username
..'@'..origin
.host
);
124 local t
= stanza
.attr
.type;
125 local self
= not stanza
.attr
.to
;
127 -- Only cache subscriptions if user is online
128 if not bare_sessions
[user
] then return; end
130 if not t
then -- available presence
131 if self
or subscription_presence(user
, stanza
.attr
.from
) then
132 local recipient
= stanza
.attr
.from
;
133 local current
= recipients
[user
] and recipients
[user
][recipient
];
134 local hash
= get_caps_hash_from_presence(stanza
, current
);
135 if current
== hash
or (current
and current
== hash_map
[hash
]) then return; end
137 if recipients
[user
] then recipients
[user
][recipient
] = nil; end
139 recipients
[user
] = recipients
[user
] or {};
140 if hash_map
[hash
] then
141 recipients
[user
][recipient
] = hash_map
[hash
];
142 publish_all(user
, recipient
, origin
);
144 recipients
[user
][recipient
] = hash
;
145 local from_bare
= origin
.type == "c2s" and origin
.username
.."@"..origin
.host
;
146 if self
or origin
.type ~= "c2s" or (recipients
[from_bare
] and recipients
[from_bare
][origin
.full_jid
]) ~= hash
then
147 -- COMPAT from ~= stanza.attr.to because OneTeam and Asterisk 1.8 can't deal with missing from attribute
149 st
.stanza("iq", {from
=user
, to
=stanza
.attr
.from
, id
="disco", type="get"})
150 :query("http://jabber.org/protocol/disco#info")
156 elseif t
== "unavailable" then
157 if recipients
[user
] then recipients
[user
][stanza
.attr
.from
] = nil; end
158 elseif not self
and t
== "unsubscribe" then
159 local from
= jid_bare(stanza
.attr
.from
);
160 local subscriptions
= recipients
[user
];
161 if subscriptions
then
162 for subscriber
in pairs(subscriptions
) do
163 if jid_bare(subscriber
) == from
then
164 recipients
[user
][subscriber
] = nil;
171 module
:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event
)
172 local session
, stanza
= event
.origin
, event
.stanza
;
173 local payload
= stanza
.tags
[1];
175 if stanza
.attr
.type == 'set' and (not stanza
.attr
.to
or jid_bare(stanza
.attr
.from
) == stanza
.attr
.to
) then
176 payload
= payload
.tags
[1]; -- <publish node='http://jabber.org/protocol/tune'>
177 if payload
and (payload
.name
== 'publish' or payload
.name
== 'retract') and payload
.attr
.node
then
178 local node
= payload
.attr
.node
;
179 payload
= payload
.tags
[1];
180 if payload
and payload
.name
== "item" then -- <item>
181 local id
= payload
.attr
.id
or "1";
182 payload
.attr
.id
= id
;
183 session
.send(st
.reply(stanza
));
184 module
:fire_event("pep-publish-item", {
185 node
= node
, user
= jid_bare(session
.full_jid
), actor
= session
.jid
,
186 id
= id
, session
= session
, item
= st
.clone(payload
);
190 module
:log("debug", "Payload is missing the <item>", node
);
193 module
:log("debug", "Unhandled payload: %s", payload
and payload
:top_tag() or "(no payload)");
195 elseif stanza
.attr
.type == 'get' then
196 local user
= stanza
.attr
.to
and jid_bare(stanza
.attr
.to
) or session
.username
..'@'..session
.host
;
197 if subscription_presence(user
, stanza
.attr
.from
) then
198 local user_data
= data
[user
];
199 local node
, requested_id
;
200 payload
= payload
.tags
[1];
201 if payload
and payload
.name
== 'items' then
202 node
= payload
.attr
.node
;
203 local item
= payload
.tags
[1];
204 if item
and item
.name
== "item" then
205 requested_id
= item
.attr
.id
;
208 if node
and user_data
and user_data
[node
] then -- Send the last item
209 local id
, item
= unpack(user_data
[node
]);
210 if not requested_id
or id
== requested_id
then
211 local reply_stanza
= st
.reply(stanza
)
212 :tag('pubsub', {xmlns
='http://jabber.org/protocol/pubsub'})
213 :tag('items', {node
=node
})
217 session
.send(reply_stanza
);
219 else -- requested item doesn't exist
220 local reply_stanza
= st
.reply(stanza
)
221 :tag('pubsub', {xmlns
='http://jabber.org/protocol/pubsub'})
222 :tag('items', {node
=node
})
224 session
.send(reply_stanza
);
227 elseif node
then -- node doesn't exist
228 session
.send(st
.error_reply(stanza
, 'cancel', 'item-not-found'));
229 module
:log("debug", "Item '%s' not found", node
)
231 else --invalid request
232 session
.send(st
.error_reply(stanza
, 'modify', 'bad-request'));
233 module
:log("debug", "Invalid request: %s", payload
);
236 else --no presence subscription
237 session
.send(st
.error_reply(stanza
, 'auth', 'not-authorized')
238 :tag('presence-subscription-required', {xmlns
='http://jabber.org/protocol/pubsub#errors'}));
239 module
:log("debug", "Unauthorized request: %s", payload
);
245 module
:hook("iq-result/bare/disco", function(event
)
246 local session
, stanza
= event
.origin
, event
.stanza
;
247 if stanza
.attr
.type == "result" then
248 local disco
= stanza
.tags
[1];
249 if disco
and disco
.name
== "query" and disco
.attr
.xmlns
== "http://jabber.org/protocol/disco#info" then
250 -- Process disco response
251 local self
= not stanza
.attr
.to
;
252 local user
= stanza
.attr
.to
or (session
.username
..'@'..session
.host
);
253 local contact
= stanza
.attr
.from
;
254 local current
= recipients
[user
] and recipients
[user
][contact
];
255 if type(current
) ~= "string" then return; end -- check if waiting for recipient's response
257 if not string.find(current
, "#") then
258 ver
= calculate_hash(disco
.tags
); -- calculate hash
261 for _
, feature
in pairs(disco
.tags
) do
262 if feature
.name
== "feature" and feature
.attr
.var
then
263 local nfeature
= feature
.attr
.var
:match("^(.*)%+notify$");
264 if nfeature
then notify
[nfeature
] = true; end
267 hash_map
[ver
] = notify
; -- update hash map
269 for jid
, item
in pairs(session
.roster
) do -- for all interested contacts
270 if item
.subscription
== "both" or item
.subscription
== "from" then
271 if not recipients
[jid
] then recipients
[jid
] = {}; end
272 recipients
[jid
][contact
] = notify
;
273 publish_all(jid
, contact
, session
);
277 recipients
[user
][contact
] = notify
; -- set recipient's data to calculated data
278 -- send messages to recipient
279 publish_all(user
, contact
, session
);
284 module
:hook("account-disco-info", function(event
)
285 local reply
= event
.reply
;
286 reply
:tag('identity', {category
='pubsub', type='pep'}):up();
287 reply
:tag('feature', {var
=xmlns_pubsub
}):up();
292 "filtered-notifications",
295 "presence-notifications",
296 "presence-subscribe",
301 for _
, feature
in ipairs(features
) do
302 reply
:tag('feature', {var
=xmlns_pubsub
.."#"..feature
}):up();
306 module
:hook("account-disco-items", function(event
)
307 local reply
= event
.reply
;
308 local bare
= reply
.attr
.to
;
309 local user_data
= data
[bare
];
312 for node
, _
in pairs(user_data
) do
313 reply
:tag('item', {jid
=bare
, node
=node
}):up();
318 module
:hook("account-disco-info-node", function (event
)
319 local stanza
, node
= event
.stanza
, event
.node
;
320 local user
= stanza
.attr
.to
;
321 local user_data
= data
[user
];
322 if user_data
and user_data
[node
] then
324 event
.reply
:tag('identity', {category
='pubsub', type='leaf'}):up();
328 module
:hook("resource-unbind", function (event
)
329 local user_bare_jid
= event
.session
.username
.."@"..event
.session
.host
;
330 if not bare_sessions
[user_bare_jid
] then -- User went offline
331 -- We don't need this info cached anymore, clear it.
332 recipients
[user_bare_jid
] = nil;