Mercurial > prosody-modules
annotate mod_twitter/mod_twitter.lua @ 5549:01a0b67a9afd
mod_http_oauth2: Add TODO about disabling password grant
Per recommendation in draft-ietf-oauth-security-topics-23 it should at
the very least be disabled by default.
However since this is used by the Snikket web portal some care needs to
be taken not to break this, unless it's already broken by other changes
to this module.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 16 Jun 2023 00:06:53 +0200 |
parents | 65082d91950e |
children |
rev | line source |
---|---|
249 | 1 -- for Prosody |
2 -- via dersd | |
3 | |
4 if module:get_host_type() ~= "component" then | |
5 error(module.name.." should be loaded as a component, check out http://prosody.im/doc/components", 0); | |
6 end | |
7 | |
8 local jid_split = require "util.jid".split; | |
9 local st = require "util.stanza"; | |
10 local componentmanager = require "core.componentmanager"; | |
11 local datamanager = require "util.datamanager"; | |
12 local timer = require "util.timer"; | |
13 local http = require "net.http"; | |
790
4f9cd19c4658
mod_twitter: fixed to depend on Prosody's internal util.json. TODO: Discuss (MattJ, Zash, Waqas, Maranda) about migration all json content to use cjson ( http://www.kyne.com.au/~mark/software/lua-cjson-manual.html ) library instead
Vadim Misbakh-Soloviov <mva@mva.name>
parents:
249
diff
changeset
|
14 local json = require "util.json"; |
249 | 15 local base64 = require "util.encodings".base64; |
16 | |
17 local component_host = module:get_host(); | |
18 local component_name = module.name; | |
19 local data_cache = {}; | |
20 | |
21 function print_r(obj) | |
22 return require("util.serialization").serialize(obj); | |
23 end | |
24 | |
25 function dmsg(jid, msg) | |
26 module:log("debug", msg or "nil"); | |
27 if jid ~= nil then | |
2887
65082d91950e
Many modules: Simplify st.message(…):tag("body"):text(…):up() into st.message(…, …)
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
983
diff
changeset
|
28 module:send(st.message({to=jid, from=component_host, type='chat'}, msg or "nil")); |
249 | 29 end |
30 end | |
31 | |
32 function substring(string, start_string, ending_string) | |
33 local s_value_start, s_value_finish = nil, nil; | |
34 if start_string ~= nil then | |
35 _, s_value_start = string:find(start_string); | |
36 if s_value_start == nil then | |
37 -- error | |
38 return nil; | |
39 end | |
40 else | |
41 return nil; | |
42 end | |
43 if ending_string ~= nil then | |
44 _, s_value_finish = string:find(ending_string, s_value_start+1); | |
45 if s_value_finish == nil then | |
46 -- error | |
47 return nil; | |
48 end | |
49 else | |
50 s_value_finish = string:len()+1; | |
51 end | |
52 return string:sub(s_value_start+1, s_value_finish-1); | |
53 end | |
54 | |
55 local http_timeout = 30; | |
56 local http_queue = setmetatable({}, { __mode = "k" }); -- auto-cleaning nil elements | |
57 data_cache['prosody_os'] = prosody.platform; | |
58 data_cache['prosody_version'] = prosody.version; | |
59 local http_headers = { | |
60 ["User-Agent"] = "Prosody ("..data_cache['prosody_version'].."; "..data_cache['prosody_os']..")" --"ELinks (0.4pre5; Linux 2.4.27 i686; 80x25)", | |
61 }; | |
62 | |
63 function http_action_callback(response, code, request, xcallback) | |
64 if http_queue == nil or http_queue[request] == nil then return; end | |
65 local id = http_queue[request]; | |
66 http_queue[request] = nil; | |
67 if xcallback == nil then | |
68 dmsg(nil, "http_action_callback reports that xcallback is nil"); | |
69 else | |
70 xcallback(id, response, request); | |
71 end | |
72 return true; | |
73 end | |
74 | |
75 function http_add_action(tid, url, method, post, fcallback) | |
983
ac9bf3fcbcfe
mod_pubsub_feeds, mod_sms_clickatell, mod_twitter: Update for net.http API change in prosody:e3b9dc9dd940
Matthew Wild <mwild1@gmail.com>
parents:
803
diff
changeset
|
76 local request = http.request(url, { headers = http_headers or {}, body = http.formencode(post or {}), method = method or "GET" }, function(response_body, code, response, request) http_action_callback(response_body, code, request, fcallback) end); |
249 | 77 http_queue[request] = tid; |
78 timer.add_task(http_timeout, function() http.destroy_request(request); end); | |
79 return true; | |
80 end | |
81 | |
82 local users = setmetatable({}, {__mode="k"}); | |
83 local user = {}; | |
84 user.__index = user; | |
85 user.dosync = false; | |
86 user.valid = false; | |
87 user.data = {}; | |
88 | |
89 function user:login() | |
90 userdata = datamanager.load(self.jid, component_host, "data"); | |
91 if userdata ~= nil then | |
92 self.data = userdata; | |
93 if self.data['_twitter_sess'] ~= nil then | |
94 http_headers['Cookie'] = "_twitter_sess="..self.data['_twitter_sess']..";"; | |
95 end | |
803 | 96 module:send(st.presence({to=self.jid, from=component_host})); |
249 | 97 self:twitterAction("VerifyCredentials"); |
98 if self.data.dosync == 1 then | |
99 self.dosync = true; | |
100 timer.add_task(self.data.refreshrate, function() return users[self.jid]:sync(); end) | |
101 end | |
102 else | |
2887
65082d91950e
Many modules: Simplify st.message(…):tag("body"):text(…):up() into st.message(…, …)
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
983
diff
changeset
|
103 module:send(st.message({to=self.jid, from=component_host, type='chat'}, "You are not signed in.")); |
249 | 104 end |
105 end | |
106 | |
107 function user:logout() | |
108 datamanager.store(self.jid, component_host, "data", self.data); | |
109 self.dosync = false; | |
803 | 110 module:send(st.presence({to=self.jid, from=component_host, type='unavailable'})); |
249 | 111 end |
112 | |
113 function user:sync() | |
114 if self.dosync then | |
115 table.foreach(self.data.synclines, function(ind, line) self:twitterAction(line.name, {sinceid=line.sinceid}) end); | |
116 return self.data.refreshrate; | |
117 end | |
118 end | |
119 | |
120 function user:signin() | |
121 if datamanager.load(self.jid, component_host, "data") == nil then | |
122 datamanager.store(self.jid, component_host, "data", {login=self.data.login, password=self.data.password, refreshrate=60, dosync=1, synclines={{name='HomeTimeline', sinceid=0}}, syncstatus=0}) | |
803 | 123 module:send(st.presence{to=self.jid, from=component_host, type='subscribe'}); |
124 module:send(st.presence{to=self.jid, from=component_host, type='subscribed'}); | |
249 | 125 end |
126 end | |
127 | |
128 function user:signout() | |
129 if datamanager.load(self.jid, component_host, "data") ~= nil then | |
130 datamanager.store(self.jid, component_host, "data", nil); | |
803 | 131 module:send(st.presence({to=self.jid, from=component_host, type='unavailable'})); |
132 module:send(st.presence({to=self.jid, from=component_host, type='unsubscribe'})); | |
133 module:send(st.presence({to=self.jid, from=component_host, type='unsubscribed'})); | |
249 | 134 end |
135 end | |
136 | |
137 local twitterApiUrl = "http://api.twitter.com"; | |
138 local twitterApiVersion = "1"; | |
139 local twitterApiDataType = "json"; | |
140 local twitterActionUrl = function(action) return twitterApiUrl.."/"..twitterApiVersion.."/"..action.."."..twitterApiDataType end; | |
141 local twitterActionMap = { | |
142 PublicTimeline = { | |
143 url = twitterActionUrl("statuses/public_timeline"), | |
144 method = "GET", | |
145 needauth = false, | |
146 }, | |
147 HomeTimeline = { | |
148 url = twitterActionUrl("statuses/home_timeline"), | |
149 method = "GET", | |
150 needauth = true, | |
151 }, | |
152 FriendsTimeline = { | |
153 url = twitterActionUrl("statuses/friends_timeline"), | |
154 method = "GET", | |
155 needauth = true, | |
156 }, | |
157 UserTimeline = { | |
158 url = twitterActionUrl("statuses/friends_timeline"), | |
159 method = "GET", | |
160 needauth = true, | |
161 }, | |
162 VerifyCredentials = { | |
163 url = twitterActionUrl("account/verify_credentials"), | |
164 method = "GET", | |
165 needauth = true, | |
166 }, | |
167 UpdateStatus = { | |
168 url = twitterActionUrl("statuses/update"), | |
169 method = "POST", | |
170 needauth = true, | |
171 }, | |
172 Retweet = { | |
173 url = twitterActionUrl("statuses/retweet/%tweetid"), | |
174 method = "POST", | |
175 needauth = true, | |
176 } | |
177 } | |
178 | |
179 function user:twitterAction(line, params) | |
180 local action = twitterActionMap[line]; | |
181 if action then | |
182 local url = action.url; | |
183 local post = {}; | |
184 --if action.needauth and not self.valid and line ~= "VerifyCredentials" then | |
185 -- return | |
186 --end | |
187 if action.needauth then | |
188 http_headers['Authorization'] = "Basic "..base64.encode(self.data.login..":"..self.data.password); | |
189 --url = string.gsub(url, "http\:\/\/", string.format("http://%s:%s@", self.data.login, self.data.password)); | |
190 end | |
191 if params and type(params) == "table" then | |
192 post = params; | |
193 end | |
194 if action.method == "GET" and post ~= {} then | |
195 url = url.."?"..http.formencode(post); | |
196 end | |
197 http_add_action(line, url, action.method, post, function(...) self:twitterActionResult(...) end); | |
198 else | |
2887
65082d91950e
Many modules: Simplify st.message(…):tag("body"):text(…):up() into st.message(…, …)
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
983
diff
changeset
|
199 module:send(st.message({to=self.jid, from=component_host, type='chat'}, "Wrong twitter action!")); |
249 | 200 end |
201 end | |
202 | |
203 local twitterActionResultMap = { | |
204 PublicTimeline = {exec=function(jid, response) | |
2887
65082d91950e
Many modules: Simplify st.message(…):tag("body"):text(…):up() into st.message(…, …)
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
983
diff
changeset
|
205 --module:send(st.message({to=jid, from=component_host, type='chat'}, print_r(response))); |
249 | 206 return |
207 end}, | |
208 HomeTimeline = {exec=function(jid, response) | |
2887
65082d91950e
Many modules: Simplify st.message(…):tag("body"):text(…):up() into st.message(…, …)
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
983
diff
changeset
|
209 --module:send(st.message({to=jid, from=component_host, type='chat'}, print_r(response))); |
249 | 210 return |
211 end}, | |
212 FriendsTimeline = {function(jid, response) | |
213 return | |
214 end}, | |
215 UserTimeline = {exec=function(jid, response) | |
216 return | |
217 end}, | |
218 VerifyCredentials = {exec=function(jid, response) | |
219 if response ~= nil and response.id ~= nil then | |
220 users[jid].valid = true; | |
221 users[jid].id = response.id; | |
222 end | |
223 return | |
224 end}, | |
225 UpdateStatus = {exec=function(jid, response) | |
226 return | |
227 end}, | |
228 Retweet = {exec=function(jid, response) | |
229 return | |
230 end} | |
231 } | |
232 | |
233 function user:twitterActionResult(id, response, request) | |
234 if request ~= nil and request.responseheaders['set-cookie'] ~= nil and request.responseheaders['location'] ~= nil then | |
235 --self.data['_twitter_sess'] = substring(request.responseheaders['set-cookie'], "_twitter_sess=", ";"); | |
236 --http_add_action(id, request.responseheaders['location'], "GET", {}, function(...) self:twitterActionResult(...) end); | |
237 return true; | |
238 end | |
239 local result, tmp_json = pcall(function() json.decode(response or "{}") end); | |
240 if result and id ~= nil then | |
241 twitterActionResultMap[id]:exec(self.jid, tmp_json); | |
242 end | |
243 return true; | |
244 end | |
245 | |
246 function iq_success(event) | |
247 local origin, stanza = event.origin, event.stanza; | |
248 local reply = data_cache.success; | |
249 if reply == nil then | |
250 reply = st.iq({type='result', from=stanza.attr.to or component_host}); | |
251 data_cache.success = reply; | |
252 end | |
253 reply.attr.id = stanza.attr.id; | |
254 reply.attr.to = stanza.attr.from; | |
255 origin.send(reply); | |
256 return true; | |
257 end | |
258 | |
259 function iq_disco_info(event) | |
260 local origin, stanza = event.origin, event.stanza; | |
261 local from = {}; | |
262 from.node, from.host, from.resource = jid_split(stanza.attr.from); | |
263 local bjid = from.node.."@"..from.host; | |
264 local reply = data_cache.disco_info; | |
265 if reply == nil then | |
266 reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#info") | |
267 :tag("identity", {category='gateway', type='chat', name=component_name}):up(); | |
268 reply = reply:tag("feature", {var="urn:xmpp:receipts"}):up(); | |
269 reply = reply:tag("feature", {var="http://jabber.org/protocol/commands"}):up(); | |
270 reply = reply:tag("feature", {var="jabber:iq:register"}):up(); | |
271 --reply = reply:tag("feature", {var="jabber:iq:time"}):up(); | |
272 --reply = reply:tag("feature", {var="jabber:iq:version"}):up(); | |
273 --reply = reply:tag("feature", {var="http://jabber.org/protocol/stats"}):up(); | |
274 data_cache.disco_info = reply; | |
275 end | |
276 reply.attr.id = stanza.attr.id; | |
277 reply.attr.to = stanza.attr.from; | |
278 origin.send(reply); | |
279 return true; | |
280 end | |
281 | |
282 function iq_disco_items(event) | |
283 local origin, stanza = event.origin, event.stanza; | |
284 local reply = data_cache.disco_items; | |
285 if reply == nil then | |
286 reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#items"); | |
287 data_cache.disco_items = reply; | |
288 end | |
289 reply.attr.id = stanza.attr.id; | |
290 reply.attr.to = stanza.attr.from; | |
291 origin.send(reply); | |
292 return true; | |
293 end | |
294 | |
295 function iq_register(event) | |
296 local origin, stanza = event.origin, event.stanza; | |
297 if stanza.attr.type == "get" then | |
298 local reply = data_cache.registration_form; | |
299 if reply == nil then | |
300 reply = st.iq({type='result', from=stanza.attr.to or component_host}) | |
301 :tag("query", { xmlns="jabber:iq:register" }) | |
302 :tag("instructions"):text("Enter your twitter data"):up() | |
303 :tag("username"):up() | |
304 :tag("password"):up(); | |
305 data_cache.registration_form = reply | |
306 end | |
307 reply.attr.id = stanza.attr.id; | |
308 reply.attr.to = stanza.attr.from; | |
309 origin.send(reply); | |
310 elseif stanza.attr.type == "set" then | |
311 local from = {}; | |
312 from.node, from.host, from.resource = jid_split(stanza.attr.from); | |
313 local bjid = from.node.."@"..from.host; | |
314 local username, password = "", ""; | |
315 local reply; | |
316 for _, tag in ipairs(stanza.tags[1].tags) do | |
317 if tag.name == "remove" then | |
318 users[bjid]:signout(); | |
319 iq_success(event); | |
320 return true; | |
321 end | |
322 if tag.name == "username" then | |
323 username = tag[1]; | |
324 end | |
325 if tag.name == "password" then | |
326 password = tag[1]; | |
327 end | |
328 end | |
329 if username ~= nil and password ~= nil then | |
330 users[bjid] = setmetatable({}, user); | |
331 users[bjid].jid = bjid; | |
332 users[bjid].data.login = username; | |
333 users[bjid].data.password = password; | |
334 users[bjid]:signin(); | |
335 users[bjid]:login(); | |
336 end | |
337 iq_success(event); | |
338 return true; | |
339 end | |
340 end | |
341 | |
342 function presence_stanza_handler(event) | |
343 local origin, stanza = event.origin, event.stanza; | |
344 local to = {}; | |
345 local from = {}; | |
346 local pres = {}; | |
347 to.node, to.host, to.resource = jid_split(stanza.attr.to); | |
348 from.node, from.host, from.resource = jid_split(stanza.attr.from); | |
349 pres.type = stanza.attr.type; | |
350 for _, tag in ipairs(stanza.tags) do pres[tag.name] = tag[1]; end | |
351 local from_bjid = nil; | |
352 if from.node ~= nil and from.host ~= nil then | |
353 from_bjid = from.node.."@"..from.host; | |
354 elseif from.host ~= nil then | |
355 from_bjid = from.host; | |
356 end | |
357 if pres.type == nil then | |
358 if users[from_bjid] ~= nil then | |
359 -- Status change | |
360 if pres['status'] ~= nil and users[from_bjid]['data']['sync_status'] then | |
361 users[from_bjid]:twitterAction("UpdateStatus", {status=pres['status']}); | |
362 end | |
363 else | |
364 -- User login request | |
365 users[from_bjid] = setmetatable({}, user); | |
366 users[from_bjid].jid = from_bjid; | |
367 users[from_bjid]:login(); | |
368 end | |
369 origin.send(st.presence({to=from_bjid, from=component_host})); | |
370 elseif pres.type == 'subscribe' and users[from_bjid] ~= nil then | |
371 origin.send(st.presence{to=from_bjid, from=component_host, type='subscribed'}); | |
372 elseif pres.type == 'unsubscribed' and users[from_bjid] ~= nil then | |
373 users[from_bjid]:logout(); | |
374 users[from_bjid]:signout(); | |
375 users[from_bjid] = nil; | |
376 elseif pres.type == 'unavailable' and users[from_bjid] ~= nil then | |
377 users[from_bjid]:logout(); | |
378 users[from_bjid] = nil; | |
379 end | |
380 return true; | |
381 end | |
382 | |
383 function confirm_message_delivery(event) | |
384 local reply = st.message({id=event.stanza.attr.id, to=event.stanza.attr.from, from=event.stanza.attr.to or component_host}):tag("received", {xmlns = "urn:xmpp:receipts"}); | |
385 origin.send(reply); | |
386 return true; | |
387 end | |
388 | |
389 function message_stanza_handler(event) | |
390 local origin, stanza = event.origin, event.stanza; | |
391 local to = {}; | |
392 local from = {}; | |
393 local msg = {}; | |
394 to.node, to.host, to.resource = jid_split(stanza.attr.to); | |
395 from.node, from.host, from.resource = jid_split(stanza.attr.from); | |
396 local bjid = nil; | |
397 if from.node ~= nil and from.host ~= nil then | |
398 from_bjid = from.node.."@"..from.host; | |
399 elseif from.host ~= nil then | |
400 from_bjid = from.host; | |
401 end | |
402 local to_bjid = nil; | |
403 if to.node ~= nil and to.host ~= nil then | |
404 to_bjid = to.node.."@"..to.host; | |
405 elseif to.host ~= nil then | |
406 to_bjid = to.host; | |
407 end | |
408 for _, tag in ipairs(stanza.tags) do | |
409 msg[tag.name] = tag[1]; | |
410 if tag.attr.xmlns == "urn:xmpp:receipts" then | |
411 confirm_message_delivery({origin=origin, stanza=stanza}); | |
412 end | |
413 -- can handle more xmlns | |
414 end | |
415 -- Now parse the message | |
416 if stanza.attr.to == component_host then | |
417 if msg.body == "!myinfo" then | |
418 if users[from_bjid] ~= nil then | |
2887
65082d91950e
Many modules: Simplify st.message(…):tag("body"):text(…):up() into st.message(…, …)
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
983
diff
changeset
|
419 origin.send(st.message({to=stanza.attr.from, from=component_host, type='chat'}, print_r(users[from_bjid]))); |
249 | 420 end |
421 end | |
422 -- Other messages go to twitter | |
423 user:twitterAction("UpdateStatus", {status=msg.body}); | |
424 else | |
425 -- Message to uid@host/resource | |
426 end | |
427 return true; | |
428 end | |
429 | |
430 module:hook("presence/host", presence_stanza_handler); | |
431 module:hook("message/host", message_stanza_handler); | |
432 | |
433 module:hook("iq/host/jabber:iq:register:query", iq_register); | |
434 module:hook("iq/host/http://jabber.org/protocol/disco#info:query", iq_disco_info); | |
435 module:hook("iq/host/http://jabber.org/protocol/disco#items:query", iq_disco_items); | |
436 module:hook("iq/host", function(data) | |
437 -- IQ to a local host recieved | |
438 local origin, stanza = data.origin, data.stanza; | |
439 if stanza.attr.type == "get" or stanza.attr.type == "set" then | |
440 return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); | |
441 else | |
442 module:fire_event("iq/host/"..stanza.attr.id, data); | |
443 return true; | |
444 end | |
445 end); | |
446 | |
447 module.unload = function() | |
448 componentmanager.deregister_component(component_host); | |
449 end | |
450 component = componentmanager.register_component(component_host, function() return; end); |