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