Mercurial > prosody-modules
annotate mod_http_roster_admin/mod_http_roster_admin.lua @ 2494:d300ae5dba87
mod_smacks: Fix some bugs with smacks-ack-delayed event triggering.
The old code had several flaws which are addressed here.
First of all this fixes the if statement guarding the event generation
There where some timing glitches addressed by this commit as well.
author | tmolitor <thilo@eightysoft.de> |
---|---|
date | Sun, 12 Feb 2017 21:23:22 +0100 |
parents | 68ebc52222dc |
children | 7c3a1688e385 |
rev | line source |
---|---|
2161 | 1 -- mod_http_roster_admin |
2 -- Description: Allow user rosters to be sourced from a remote HTTP API | |
3 -- | |
4 -- Version: 1.0 | |
5 -- Date: 2015-03-06 | |
6 -- Author: Matthew Wild <matthew@prosody.im> | |
7 -- License: MPLv2 | |
8 -- | |
9 -- Requirements: | |
10 -- Prosody config: | |
11 -- storage = { roster = "memory" } | |
12 -- modules_disabled = { "roster" } | |
13 -- Dependencies: | |
14 -- Prosody 0.9 | |
15 -- lua-cjson (Debian/Ubuntu/LuaRocks: lua-cjson) | |
16 | |
17 local http = require "net.http"; | |
18 local json = require "cjson"; | |
19 local it = require "util.iterators"; | |
20 local set = require "util.set"; | |
21 local rm = require "core.rostermanager"; | |
22 local st = require "util.stanza"; | |
23 local array = require "util.array"; | |
24 | |
25 local host = module.host; | |
26 local sessions = hosts[host].sessions; | |
27 | |
28 local roster_url = module:get_option_string("http_roster_url", "http://localhost/%s"); | |
29 | |
30 -- Send a roster push to the named user, with the given roster, for the specified | |
31 -- contact's roster entry. Used to notify clients of changes/removals. | |
32 local function roster_push(username, roster, contact_jid) | |
33 local stanza = st.iq({type="set"}) | |
34 :tag("query", {xmlns = "jabber:iq:roster" }); | |
35 local item = roster[contact_jid]; | |
36 if item then | |
37 stanza:tag("item", {jid = contact_jid, subscription = item.subscription, name = item.name, ask = item.ask}); | |
38 for group in pairs(item.groups) do | |
39 stanza:tag("group"):text(group):up(); | |
40 end | |
41 else | |
42 stanza:tag("item", {jid = contact_jid, subscription = "remove"}); | |
43 end | |
44 stanza:up():up(); -- move out from item | |
45 for _, session in pairs(hosts[host].sessions[username].sessions) do | |
46 if session.interested then | |
47 session.send(stanza); | |
48 end | |
49 end | |
50 end | |
51 | |
52 -- Send latest presence from the named local user to a contact. | |
53 local function send_presence(username, contact_jid, available) | |
54 module:log("debug", "Sending %savailable presence from %s to contact %s", (available and "" or "un"), username, contact_jid); | |
55 for resource, session in pairs(sessions[username].sessions) do | |
56 local pres; | |
57 if available then | |
58 pres = st.clone(session.presence); | |
59 pres.attr.to = contact_jid; | |
60 else | |
61 pres = st.presence({ to = contact_jid, from = session.full_jid, type = "unavailable" }); | |
62 end | |
63 module:send(pres); | |
64 end | |
65 end | |
66 | |
67 -- Converts a 'friend' object from the API to a Prosody roster item object | |
68 local function friend_to_roster_item(friend) | |
69 return { | |
70 name = friend.name; | |
71 subscription = "both"; | |
72 groups = friend.groups or {}; | |
73 }; | |
74 end | |
75 | |
76 -- Returns a handler function to consume the data returned from | |
77 -- the API, compare it to the user's current roster, and perform | |
78 -- any actions necessary (roster pushes, presence probes) to | |
79 -- synchronize them. | |
80 local function updated_friends_handler(username, cb) | |
81 return (function (ok, code, friends) | |
82 if not ok then | |
83 cb(false, code); | |
84 end | |
85 local user = sessions[username]; | |
86 local roster = user.roster; | |
87 local old_contacts = set.new(array.collect(it.keys(roster))); | |
88 local new_contacts = set.new(array.collect(it.keys(friends))); | |
89 | |
90 -- These two entries are not real contacts, ignore them | |
91 old_contacts:remove(false); | |
92 old_contacts:remove("pending"); | |
93 | |
94 module:log("debug", "New friends list of %s: %s", username, json.encode(friends)); | |
95 | |
96 -- Calculate which contacts have been added/removed since | |
97 -- the last time we fetched the roster | |
98 local added_contacts = new_contacts - old_contacts; | |
99 local removed_contacts = old_contacts - new_contacts; | |
100 | |
101 local added, removed = 0, 0; | |
102 | |
103 -- Add new contacts and notify connected clients | |
104 for contact_jid in added_contacts do | |
105 module:log("debug", "Processing new friend of %s: %s", username, contact_jid); | |
106 roster[contact_jid] = friend_to_roster_item(friends[contact_jid]); | |
107 roster_push(username, roster, contact_jid); | |
108 send_presence(username, contact_jid, true); | |
109 added = added + 1; | |
110 end | |
111 | |
112 -- Remove contacts and notify connected clients | |
113 for contact_jid in removed_contacts do | |
114 module:log("debug", "Processing removed friend of %s: %s", username, contact_jid); | |
115 roster[contact_jid] = nil; | |
116 roster_push(username, roster, contact_jid); | |
117 send_presence(username, contact_jid, false); | |
118 removed = removed + 1; | |
119 end | |
120 module:log("debug", "User %s: added %d new contacts, removed %d contacts", username, added, removed); | |
121 cb(true); | |
122 end); | |
123 end | |
124 | |
125 -- Fetch the named user's roster from the API, call callback (cb) | |
126 -- with status and result (friends list) when received. | |
127 function fetch_roster(username, cb) | |
2210
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
128 local x = {headers = {}}; |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
129 x["headers"]["ACCEPT"] = "application/json, text/plain, */*"; |
2441
68ebc52222dc
Log URL called by http_roster_admin
JC Brand <jc@opkode.com>
parents:
2210
diff
changeset
|
130 module:log("debug", "Fetching roster at URL: %s", roster_url:format(username)); |
2161 | 131 local ok, err = http.request( |
2210
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
132 roster_url:format(username), |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
133 x, |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
134 function (roster_data, code) |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
135 if code ~= 200 then |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
136 module:log("error", "Error fetching roster from %s (code %d): %s", roster_url:format(username), code, tostring(roster_data):sub(1, 40):match("^[^\r\n]+")); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
137 if code ~= 0 then |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
138 cb(nil, code, roster_data); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
139 end |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
140 return; |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
141 end |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
142 module:log("debug", "Successfully fetched roster for %s", username); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
143 module:log("debug", "The roster data is %s", roster_data); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
144 cb(true, code, json.decode(roster_data)); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
145 end |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
146 ); |
2161 | 147 if not ok then |
148 module:log("error", "Failed to connect to roster API at %s: %s", roster_url:format(username), err); | |
149 cb(false, 0, err); | |
150 end | |
151 end | |
152 | |
153 -- Fetch the named user's roster from the API, synchronize it with | |
154 -- the user's current roster. Notify callback (cb) with true/false | |
155 -- depending on success or failure. | |
156 function refresh_roster(username, cb) | |
157 local user = sessions[username]; | |
158 if not (user and user.roster) then | |
159 module:log("debug", "User's (%q) roster updated, but they are not online - ignoring", username); | |
160 cb(true); | |
161 return; | |
162 end | |
163 fetch_roster(username, updated_friends_handler(username, cb)); | |
164 end | |
165 | |
166 --- Roster protocol handling --- | |
167 | |
168 -- Build a reply to a "roster get" request | |
169 local function build_roster_reply(stanza, roster_data) | |
170 local roster = st.reply(stanza) | |
171 :tag("query", { xmlns = "jabber:iq:roster" }); | |
172 | |
173 for jid, item in pairs(roster_data) do | |
174 if jid and jid ~= "pending" then | |
175 roster:tag("item", { | |
176 jid = jid, | |
177 subscription = item.subscription, | |
178 ask = item.ask, | |
179 name = item.name, | |
180 }); | |
181 for group in pairs(item.groups) do | |
182 roster:tag("group"):text(group):up(); | |
183 end | |
184 roster:up(); -- move out from item | |
185 end | |
186 end | |
187 return roster; | |
188 end | |
189 | |
190 -- Handle clients requesting their roster (generally at login) | |
191 -- This will not work if mod_roster is loaded (in 0.9). | |
192 module:hook("iq-get/self/jabber:iq:roster:query", function(event) | |
193 local session, stanza = event.origin, event.stanza; | |
194 | |
195 session.interested = true; -- resource is interested in roster updates | |
196 | |
197 local roster = session.roster; | |
198 if roster[false].downloaded then | |
199 return session.send(build_roster_reply(stanza, roster)); | |
200 end | |
201 | |
202 -- It's possible that we can call this more than once for a new roster | |
203 -- Should happen rarely (multiple clients of the same user request the | |
204 -- roster in the time it takes the API to respond). Currently we just | |
205 -- issue multiple requests, as it's harmless apart from the wasted | |
206 -- requests. | |
207 fetch_roster(session.username, function (ok, code, friends) | |
208 if not ok then | |
209 session.send(st.error_reply(stanza, "cancel", "internal-server-error")); | |
210 session:close("internal-server-error"); | |
211 return; | |
212 end | |
213 | |
214 -- Are we the first callback to handle the downloaded roster? | |
215 local first = roster[false].downloaded == nil; | |
216 | |
217 if first then | |
218 -- Fill out new roster | |
219 for jid, friend in pairs(friends) do | |
220 roster[jid] = friend_to_roster_item(friend); | |
221 end | |
222 end | |
223 | |
224 -- Send full roster to client | |
225 session.send(build_roster_reply(stanza, roster)); | |
226 | |
227 if not first then | |
228 -- We already had a roster, make sure to handle any changes... | |
229 updated_friends_handler(session.username, nil)(ok, code, friends); | |
230 end | |
231 end); | |
232 | |
233 return true; | |
234 end); | |
235 | |
236 -- Prevent client from making changes to the roster. This will not | |
237 -- work if mod_roster is loaded (in 0.9). | |
238 module:hook("iq-set/self/jabber:iq:roster:query", function(event) | |
239 local session, stanza = event.origin, event.stanza; | |
240 return session.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
241 end); | |
242 | |
243 --- HTTP endpoint to trigger roster refresh --- | |
244 | |
245 -- Handles updating for a single user: GET /roster_admin/refresh/USERNAME | |
246 function handle_refresh_single(event, username) | |
247 refresh_roster(username, function (ok, code, err) | |
248 event.response.headers["Content-Type"] = "application/json"; | |
249 event.response:send(json.encode({ | |
250 status = ok and "ok" or "error"; | |
251 message = err or "roster update complete"; | |
252 })); | |
253 end); | |
254 return true; | |
255 end | |
256 | |
257 -- Handles updating for multiple users: POST /roster_admin/refresh | |
258 -- Payload should be a JSON array of usernames, e.g. ["user1", "user2", "user3"] | |
259 function handle_refresh_multi(event) | |
260 local users = json.decode(event.request.body); | |
261 if not users then | |
262 module:log("warn", "Multi-user refresh attempted with missing/invalid payload"); | |
263 event.response:send(400); | |
264 return true; | |
265 end | |
266 | |
267 local count, count_err = 0, 0; | |
268 | |
269 local function cb(ok) | |
270 count = count + 1; | |
271 if not ok then | |
272 count_err = count_err + 1; | |
273 end | |
274 | |
275 if count == #users then | |
276 event.response.headers["Content-Type"] = "application/json"; | |
277 event.response:send(json.encode({ | |
278 status = "ok"; | |
279 message = "roster update complete"; | |
280 updated = count - count_err; | |
281 errors = count_err; | |
282 })); | |
283 end | |
284 end | |
285 | |
286 for _, username in ipairs(users) do | |
287 refresh_roster(username, cb); | |
288 end | |
289 | |
290 return true; | |
291 end | |
292 | |
293 | |
294 module:provides("http", { | |
295 route = { | |
296 ["POST /refresh"] = handle_refresh_multi; | |
297 ["GET /refresh/*"] = handle_refresh_single; | |
298 }; | |
299 }); |