Mercurial > prosody-modules
annotate mod_http_roster_admin/mod_http_roster_admin.lua @ 2670:6e01878103c0
mod_smacks: Ignore user when writing or reading session_cache on prosody 0.9
At least under some circumstances it seems that session.username is nil when
a user tries to resume his session in prosody 0.9.
The username is not relevant when no limiting is done (limiting the number of
entries in the session cache is only possible in prosody 0.10), so this
commit removes the usage of the username when accessing the prosody 0.9 session
cache.
author | tmolitor <thilo@eightysoft.de> |
---|---|
date | Thu, 06 Apr 2017 02:12:14 +0200 |
parents | 2bfa7d476092 |
children | 9d8098f4b652 |
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); | |
2631
2bfa7d476092
mod_http_roster_admin: Don't call callback if it's nil
JC Brand <jc@opkode.com>
parents:
2622
diff
changeset
|
121 if cb ~= nil then |
2bfa7d476092
mod_http_roster_admin: Don't call callback if it's nil
JC Brand <jc@opkode.com>
parents:
2622
diff
changeset
|
122 cb(true); |
2bfa7d476092
mod_http_roster_admin: Don't call callback if it's nil
JC Brand <jc@opkode.com>
parents:
2622
diff
changeset
|
123 end |
2161 | 124 end); |
125 end | |
126 | |
127 -- Fetch the named user's roster from the API, call callback (cb) | |
128 -- with status and result (friends list) when received. | |
129 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
|
130 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
|
131 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
|
132 module:log("debug", "Fetching roster at URL: %s", roster_url:format(username)); |
2161 | 133 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
|
134 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
|
135 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
|
136 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
|
137 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
|
138 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
|
139 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
|
140 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
|
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 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
|
143 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
|
144 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
|
145 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
|
146 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
|
147 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
|
148 ); |
2161 | 149 if not ok then |
150 module:log("error", "Failed to connect to roster API at %s: %s", roster_url:format(username), err); | |
151 cb(false, 0, err); | |
152 end | |
153 end | |
154 | |
155 -- Fetch the named user's roster from the API, synchronize it with | |
156 -- the user's current roster. Notify callback (cb) with true/false | |
157 -- depending on success or failure. | |
158 function refresh_roster(username, cb) | |
159 local user = sessions[username]; | |
160 if not (user and user.roster) then | |
161 module:log("debug", "User's (%q) roster updated, but they are not online - ignoring", username); | |
162 cb(true); | |
163 return; | |
164 end | |
165 fetch_roster(username, updated_friends_handler(username, cb)); | |
166 end | |
167 | |
168 --- Roster protocol handling --- | |
169 | |
170 -- Build a reply to a "roster get" request | |
171 local function build_roster_reply(stanza, roster_data) | |
172 local roster = st.reply(stanza) | |
173 :tag("query", { xmlns = "jabber:iq:roster" }); | |
174 | |
175 for jid, item in pairs(roster_data) do | |
176 if jid and jid ~= "pending" then | |
177 roster:tag("item", { | |
178 jid = jid, | |
179 subscription = item.subscription, | |
180 ask = item.ask, | |
181 name = item.name, | |
182 }); | |
183 for group in pairs(item.groups) do | |
184 roster:tag("group"):text(group):up(); | |
185 end | |
186 roster:up(); -- move out from item | |
187 end | |
188 end | |
189 return roster; | |
190 end | |
191 | |
192 -- Handle clients requesting their roster (generally at login) | |
193 -- This will not work if mod_roster is loaded (in 0.9). | |
194 module:hook("iq-get/self/jabber:iq:roster:query", function(event) | |
195 local session, stanza = event.origin, event.stanza; | |
196 | |
197 session.interested = true; -- resource is interested in roster updates | |
198 | |
199 local roster = session.roster; | |
200 if roster[false].downloaded then | |
201 return session.send(build_roster_reply(stanza, roster)); | |
202 end | |
203 | |
204 -- It's possible that we can call this more than once for a new roster | |
205 -- Should happen rarely (multiple clients of the same user request the | |
206 -- roster in the time it takes the API to respond). Currently we just | |
207 -- issue multiple requests, as it's harmless apart from the wasted | |
208 -- requests. | |
209 fetch_roster(session.username, function (ok, code, friends) | |
210 if not ok then | |
211 session.send(st.error_reply(stanza, "cancel", "internal-server-error")); | |
212 session:close("internal-server-error"); | |
213 return; | |
214 end | |
215 | |
216 -- Are we the first callback to handle the downloaded roster? | |
217 local first = roster[false].downloaded == nil; | |
218 if first then | |
219 -- Fill out new roster | |
220 for jid, friend in pairs(friends) do | |
221 roster[jid] = friend_to_roster_item(friend); | |
222 end | |
223 end | |
2617
7c3a1688e385
Purge the roster from RAM when the user logs off.
JC Brand <jc@opkode.com>
parents:
2441
diff
changeset
|
224 |
7c3a1688e385
Purge the roster from RAM when the user logs off.
JC Brand <jc@opkode.com>
parents:
2441
diff
changeset
|
225 roster[false].downloaded = true; |
2161 | 226 |
227 -- Send full roster to client | |
228 session.send(build_roster_reply(stanza, roster)); | |
229 | |
230 if not first then | |
231 -- We already had a roster, make sure to handle any changes... | |
232 updated_friends_handler(session.username, nil)(ok, code, friends); | |
233 end | |
234 end); | |
235 | |
236 return true; | |
237 end); | |
238 | |
239 -- Prevent client from making changes to the roster. This will not | |
240 -- work if mod_roster is loaded (in 0.9). | |
241 module:hook("iq-set/self/jabber:iq:roster:query", function(event) | |
242 local session, stanza = event.origin, event.stanza; | |
243 return session.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
244 end); | |
245 | |
246 --- HTTP endpoint to trigger roster refresh --- | |
247 | |
248 -- Handles updating for a single user: GET /roster_admin/refresh/USERNAME | |
249 function handle_refresh_single(event, username) | |
250 refresh_roster(username, function (ok, code, err) | |
251 event.response.headers["Content-Type"] = "application/json"; | |
252 event.response:send(json.encode({ | |
253 status = ok and "ok" or "error"; | |
254 message = err or "roster update complete"; | |
255 })); | |
256 end); | |
257 return true; | |
258 end | |
259 | |
260 -- Handles updating for multiple users: POST /roster_admin/refresh | |
261 -- Payload should be a JSON array of usernames, e.g. ["user1", "user2", "user3"] | |
262 function handle_refresh_multi(event) | |
263 local users = json.decode(event.request.body); | |
264 if not users then | |
265 module:log("warn", "Multi-user refresh attempted with missing/invalid payload"); | |
266 event.response:send(400); | |
267 return true; | |
268 end | |
269 | |
270 local count, count_err = 0, 0; | |
271 | |
272 local function cb(ok) | |
273 count = count + 1; | |
274 if not ok then | |
275 count_err = count_err + 1; | |
276 end | |
277 | |
278 if count == #users then | |
279 event.response.headers["Content-Type"] = "application/json"; | |
280 event.response:send(json.encode({ | |
281 status = "ok"; | |
282 message = "roster update complete"; | |
283 updated = count - count_err; | |
284 errors = count_err; | |
285 })); | |
286 end | |
287 end | |
288 | |
289 for _, username in ipairs(users) do | |
290 refresh_roster(username, cb); | |
291 end | |
292 | |
293 return true; | |
294 end | |
295 | |
296 module:provides("http", { | |
297 route = { | |
298 ["POST /refresh"] = handle_refresh_multi; | |
299 ["GET /refresh/*"] = handle_refresh_single; | |
300 }; | |
301 }); |