| 1 | -- Prosody IM |
| 2 | -- Copyright (C) 2008-2010 Matthew Wild |
| 3 | -- Copyright (C) 2008-2010 Waqas Hussain |
| 4 | -- |
| 5 | -- This project is MIT/X11 licensed. Please see the |
| 6 | -- COPYING file in the source package for more information. |
| 7 | -- |
| 8 | |
| 9 | |
| 10 | local jid_bare = require "util.jid".bare; |
| 11 | local jid_split = require "util.jid".split; |
| 12 | local st = require "util.stanza"; |
| 13 | local hosts = hosts; |
| 14 | local user_exists = require "core.usermanager".user_exists; |
| 15 | local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; |
| 16 | local pairs, ipairs = pairs, ipairs; |
| 17 | local next = next; |
| 18 | local type = type; |
| 19 | local calculate_hash = require "util.caps".calculate_hash; |
| 20 | |
| 21 | local NULL = {}; |
| 22 | local data = {}; |
| 23 | local recipients = {}; |
| 24 | local hash_map = {}; |
| 25 | |
| 26 | module.save = function() |
| 27 | return { data = data, recipients = recipients, hash_map = hash_map }; |
| 28 | end |
| 29 | module.restore = function(state) |
| 30 | data = state.data or {}; |
| 31 | recipients = state.recipients or {}; |
| 32 | hash_map = state.hash_map or {}; |
| 33 | end |
| 34 | |
| 35 | module:add_identity("pubsub", "pep", "Prosody"); |
| 36 | module:add_feature("http://jabber.org/protocol/pubsub#publish"); |
| 37 | |
| 38 | local function subscription_presence(user_bare, recipient) |
| 39 | local recipient_bare = jid_bare(recipient); |
| 40 | if (recipient_bare == user_bare) then return true end |
| 41 | local username, host = jid_split(user_bare); |
| 42 | return is_contact_subscribed(username, host, recipient_bare); |
| 43 | end |
| 44 | |
| 45 | local function publish(session, node, id, item) |
| 46 | item.attr.xmlns = nil; |
| 47 | local disable = #item.tags ~= 1 or #item.tags[1] == 0; |
| 48 | if #item.tags == 0 then item.name = "retract"; end |
| 49 | local bare = session.username..'@'..session.host; |
| 50 | local stanza = st.message({from=bare, type='headline'}) |
| 51 | :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) |
| 52 | :tag('items', {node=node}) |
| 53 | :add_child(item) |
| 54 | :up() |
| 55 | :up(); |
| 56 | |
| 57 | -- store for the future |
| 58 | local user_data = data[bare]; |
| 59 | if disable then |
| 60 | if user_data then |
| 61 | user_data[node] = nil; |
| 62 | if not next(user_data) then data[bare] = nil; end |
| 63 | end |
| 64 | else |
| 65 | if not user_data then user_data = {}; data[bare] = user_data; end |
| 66 | user_data[node] = {id or "1", item}; |
| 67 | end |
| 68 | |
| 69 | -- broadcast |
| 70 | for recipient, notify in pairs(recipients[bare] or NULL) do |
| 71 | if notify[node] then |
| 72 | stanza.attr.to = recipient; |
| 73 | core_post_stanza(session, stanza); |
| 74 | end |
| 75 | end |
| 76 | end |
| 77 | local function publish_all(user, recipient, session) |
| 78 | local d = data[user]; |
| 79 | local notify = recipients[user] and recipients[user][recipient]; |
| 80 | if d and notify then |
| 81 | for node in pairs(notify) do |
| 82 | if d[node] then |
| 83 | local id, item = unpack(d[node]); |
| 84 | session.send(st.message({from=user, to=recipient, type='headline'}) |
| 85 | :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) |
| 86 | :tag('items', {node=node}) |
| 87 | :add_child(item) |
| 88 | :up() |
| 89 | :up()); |
| 90 | end |
| 91 | end |
| 92 | end |
| 93 | end |
| 94 | |
| 95 | local function get_caps_hash_from_presence(stanza, current) |
| 96 | local t = stanza.attr.type; |
| 97 | if not t then |
| 98 | for _, child in pairs(stanza.tags) do |
| 99 | if child.name == "c" and child.attr.xmlns == "http://jabber.org/protocol/caps" then |
| 100 | local attr = child.attr; |
| 101 | if attr.hash then -- new caps |
| 102 | if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end |
| 103 | else -- legacy caps |
| 104 | if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end |
| 105 | end |
| 106 | return; -- bad caps format |
| 107 | end |
| 108 | end |
| 109 | elseif t == "unavailable" or t == "error" then |
| 110 | return; |
| 111 | end |
| 112 | return current; -- no caps, could mean caps optimization, so return current |
| 113 | end |
| 114 | |
| 115 | module:hook("presence/bare", function(event) |
| 116 | -- inbound presence to bare JID recieved |
| 117 | local origin, stanza = event.origin, event.stanza; |
| 118 | local user = stanza.attr.to or (origin.username..'@'..origin.host); |
| 119 | local t = stanza.attr.type; |
| 120 | local self = not stanza.attr.to; |
| 121 | |
| 122 | if not t then -- available presence |
| 123 | if self or subscription_presence(user, stanza.attr.from) then |
| 124 | local recipient = stanza.attr.from; |
| 125 | local current = recipients[user] and recipients[user][recipient]; |
| 126 | local hash = get_caps_hash_from_presence(stanza, current); |
| 127 | if current == hash then return; end |
| 128 | if not hash then |
| 129 | if recipients[user] then recipients[user][recipient] = nil; end |
| 130 | else |
| 131 | recipients[user] = recipients[user] or {}; |
| 132 | if hash_map[hash] then |
| 133 | recipients[user][recipient] = hash_map[hash]; |
| 134 | publish_all(user, recipient, origin); |
| 135 | else |
| 136 | recipients[user][recipient] = hash; |
| 137 | local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; |
| 138 | if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then |
| 139 | origin.send( |
| 140 | st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) |
| 141 | :query("http://jabber.org/protocol/disco#info") |
| 142 | ); |
| 143 | end |
| 144 | end |
| 145 | end |
| 146 | end |
| 147 | elseif t == "unavailable" then |
| 148 | if recipients[user] then recipients[user][stanza.attr.from] = nil; end |
| 149 | elseif not self and t == "unsubscribe" then |
| 150 | local from = jid_bare(stanza.attr.from); |
| 151 | local subscriptions = recipients[user]; |
| 152 | if subscriptions then |
| 153 | for subscriber in pairs(subscriptions) do |
| 154 | if jid_bare(subscriber) == from then |
| 155 | recipients[user][subscriber] = nil; |
| 156 | end |
| 157 | end |
| 158 | end |
| 159 | end |
| 160 | end, 10); |
| 161 | |
| 162 | module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) |
| 163 | local session, stanza = event.origin, event.stanza; |
| 164 | local payload = stanza.tags[1]; |
| 165 | |
| 166 | if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then |
| 167 | payload = payload.tags[1]; |
| 168 | if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'> |
| 169 | local node = payload.attr.node; |
| 170 | payload = payload.tags[1]; |
| 171 | if payload and payload.name == "item" then -- <item> |
| 172 | local id = payload.attr.id; |
| 173 | session.send(st.reply(stanza)); |
| 174 | publish(session, node, id, st.clone(payload)); |
| 175 | return true; |
| 176 | end |
| 177 | end |
| 178 | elseif stanza.attr.type == 'get' then |
| 179 | local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host; |
| 180 | if subscription_presence(user, stanza.attr.from) then |
| 181 | local user_data = data[user]; |
| 182 | local node, requested_id; |
| 183 | payload = payload.tags[1]; |
| 184 | if payload and payload.name == 'items' then |
| 185 | node = payload.attr.node; |
| 186 | local item = payload.tags[1]; |
| 187 | if item and item.name == "item" then |
| 188 | requested_id = item.attr.id; |
| 189 | end |
| 190 | end |
| 191 | if node and user_data and user_data[node] then -- Send the last item |
| 192 | local id, item = unpack(user_data[node]); |
| 193 | if not requested_id or id == requested_id then |
| 194 | local stanza = st.reply(stanza) |
| 195 | :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) |
| 196 | :tag('items', {node=node}) |
| 197 | :add_child(item) |
| 198 | :up() |
| 199 | :up(); |
| 200 | session.send(stanza); |
| 201 | return true; |
| 202 | else -- requested item doesn't exist |
| 203 | local stanza = st.reply(stanza) |
| 204 | :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) |
| 205 | :tag('items', {node=node}) |
| 206 | :up(); |
| 207 | session.send(stanza); |
| 208 | return true; |
| 209 | end |
| 210 | elseif node then -- node doesn't exist |
| 211 | session.send(st.error_reply(stanza, 'cancel', 'item-not-found')); |
| 212 | return true; |
| 213 | else --invalid request |
| 214 | session.send(st.error_reply(stanza, 'modify', 'bad-request')); |
| 215 | return true; |
| 216 | end |
| 217 | else --no presence subscription |
| 218 | session.send(st.error_reply(stanza, 'auth', 'not-authorized') |
| 219 | :tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'})); |
| 220 | return true; |
| 221 | end |
| 222 | end |
| 223 | end); |
| 224 | |
| 225 | module:hook("iq-result/bare/disco", function(event) |
| 226 | local session, stanza = event.origin, event.stanza; |
| 227 | if stanza.attr.type == "result" then |
| 228 | local disco = stanza.tags[1]; |
| 229 | if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then |
| 230 | -- Process disco response |
| 231 | local self = not stanza.attr.to; |
| 232 | local user = stanza.attr.to or (session.username..'@'..session.host); |
| 233 | local contact = stanza.attr.from; |
| 234 | local current = recipients[user] and recipients[user][contact]; |
| 235 | if type(current) ~= "string" then return; end -- check if waiting for recipient's response |
| 236 | local ver = current; |
| 237 | if not string.find(current, "#") then |
| 238 | ver = calculate_hash(disco.tags); -- calculate hash |
| 239 | end |
| 240 | local notify = {}; |
| 241 | for _, feature in pairs(disco.tags) do |
| 242 | if feature.name == "feature" and feature.attr.var then |
| 243 | local nfeature = feature.attr.var:match("^(.*)%+notify$"); |
| 244 | if nfeature then notify[nfeature] = true; end |
| 245 | end |
| 246 | end |
| 247 | hash_map[ver] = notify; -- update hash map |
| 248 | if self then |
| 249 | for jid, item in pairs(session.roster) do -- for all interested contacts |
| 250 | if item.subscription == "both" or item.subscription == "from" then |
| 251 | if not recipients[jid] then recipients[jid] = {}; end |
| 252 | recipients[jid][contact] = notify; |
| 253 | publish_all(jid, contact, session); |
| 254 | end |
| 255 | end |
| 256 | end |
| 257 | recipients[user][contact] = notify; -- set recipient's data to calculated data |
| 258 | -- send messages to recipient |
| 259 | publish_all(user, contact, session); |
| 260 | end |
| 261 | end |
| 262 | end); |
| 263 | |
| 264 | module:hook("account-disco-info", function(event) |
| 265 | local stanza = event.stanza; |
| 266 | stanza:tag('identity', {category='pubsub', type='pep'}):up(); |
| 267 | stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); |
| 268 | end); |
| 269 | |
| 270 | module:hook("account-disco-items", function(event) |
| 271 | local stanza = event.stanza; |
| 272 | local bare = stanza.attr.to; |
| 273 | local user_data = data[bare]; |
| 274 | |
| 275 | if user_data then |
| 276 | for node, _ in pairs(user_data) do |
| 277 | stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes |
| 278 | end |
| 279 | end |
| 280 | end); |