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 -- Used as canonical 'empty table'
23 -- data[user_bare_jid][node] = item_stanza
25 --- recipients[user_bare_jid][contact_full_jid][subscribed_node] = true
26 local recipients
= {};
27 -- hash_map[hash][subscribed_nodes] = true
30 module
.save
= function()
31 return { data
= data
, recipients
= recipients
, hash_map
= hash_map
};
33 module
.restore
= function(state
)
34 data
= state
.data
or {};
35 recipients
= state
.recipients
or {};
36 hash_map
= state
.hash_map
or {};
39 module
:add_identity("pubsub", "pep", module
:get_option_string("name", "Prosody"));
40 module
:add_feature("http://jabber.org/protocol/pubsub#publish");
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 local function publish(session
, node
, id
, 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 bare
= session
.username
..'@'..session
.host
;
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
);
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 recieved
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];
176 if payload
and (payload
.name
== 'publish' or payload
.name
== 'retract') and payload
.attr
.node
then -- <publish node='http://jabber.org/protocol/tune'>
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 publish(session
, node
, id
, st
.clone(payload
));
187 elseif stanza
.attr
.type == 'get' then
188 local user
= stanza
.attr
.to
and jid_bare(stanza
.attr
.to
) or session
.username
..'@'..session
.host
;
189 if subscription_presence(user
, stanza
.attr
.from
) then
190 local user_data
= data
[user
];
191 local node
, requested_id
;
192 payload
= payload
.tags
[1];
193 if payload
and payload
.name
== 'items' then
194 node
= payload
.attr
.node
;
195 local item
= payload
.tags
[1];
196 if item
and item
.name
== "item" then
197 requested_id
= item
.attr
.id
;
200 if node
and user_data
and user_data
[node
] then -- Send the last item
201 local id
, item
= unpack(user_data
[node
]);
202 if not requested_id
or id
== requested_id
then
203 local stanza
= st
.reply(stanza
)
204 :tag('pubsub', {xmlns
='http://jabber.org/protocol/pubsub'})
205 :tag('items', {node
=node
})
209 session
.send(stanza
);
211 else -- requested item doesn't exist
212 local stanza
= st
.reply(stanza
)
213 :tag('pubsub', {xmlns
='http://jabber.org/protocol/pubsub'})
214 :tag('items', {node
=node
})
216 session
.send(stanza
);
219 elseif node
then -- node doesn't exist
220 session
.send(st
.error_reply(stanza
, 'cancel', 'item-not-found'));
222 else --invalid request
223 session
.send(st
.error_reply(stanza
, 'modify', 'bad-request'));
226 else --no presence subscription
227 session
.send(st
.error_reply(stanza
, 'auth', 'not-authorized')
228 :tag('presence-subscription-required', {xmlns
='http://jabber.org/protocol/pubsub#errors'}));
234 module
:hook("iq-result/bare/disco", function(event
)
235 local session
, stanza
= event
.origin
, event
.stanza
;
236 if stanza
.attr
.type == "result" then
237 local disco
= stanza
.tags
[1];
238 if disco
and disco
.name
== "query" and disco
.attr
.xmlns
== "http://jabber.org/protocol/disco#info" then
239 -- Process disco response
240 local self
= not stanza
.attr
.to
;
241 local user
= stanza
.attr
.to
or (session
.username
..'@'..session
.host
);
242 local contact
= stanza
.attr
.from
;
243 local current
= recipients
[user
] and recipients
[user
][contact
];
244 if type(current
) ~= "string" then return; end -- check if waiting for recipient's response
246 if not string.find(current
, "#") then
247 ver
= calculate_hash(disco
.tags
); -- calculate hash
250 for _
, feature
in pairs(disco
.tags
) do
251 if feature
.name
== "feature" and feature
.attr
.var
then
252 local nfeature
= feature
.attr
.var
:match("^(.*)%+notify$");
253 if nfeature
then notify
[nfeature
] = true; end
256 hash_map
[ver
] = notify
; -- update hash map
258 for jid
, item
in pairs(session
.roster
) do -- for all interested contacts
259 if item
.subscription
== "both" or item
.subscription
== "from" then
260 if not recipients
[jid
] then recipients
[jid
] = {}; end
261 recipients
[jid
][contact
] = notify
;
262 publish_all(jid
, contact
, session
);
266 recipients
[user
][contact
] = notify
; -- set recipient's data to calculated data
267 -- send messages to recipient
268 publish_all(user
, contact
, session
);
273 module
:hook("account-disco-info", function(event
)
274 local stanza
= event
.stanza
;
275 stanza
:tag('identity', {category
='pubsub', type='pep'}):up();
276 stanza
:tag('feature', {var
='http://jabber.org/protocol/pubsub#publish'}):up();
279 module
:hook("account-disco-items", function(event
)
280 local stanza
= event
.stanza
;
281 local bare
= stanza
.attr
.to
;
282 local user_data
= data
[bare
];
285 for node
, _
in pairs(user_data
) do
286 stanza
:tag('item', {jid
=bare
, node
=node
}):up(); -- TODO we need to handle queries to these nodes
291 module
:hook("resource-unbind", function (event
)
292 local user_bare_jid
= event
.session
.username
.."@"..event
.session
.host
;
293 if not bare_sessions
[user_bare_jid
] then -- User went offline
294 -- We don't need this info cached anymore, clear it.
295 recipients
[user_bare_jid
] = nil;