Mercurial > prosody-modules
view mod_twitter/mod_twitter.lua @ 5149:fa56ed2bacab
mod_unified_push: Add support for multiple token backends, including stoage
Now that we have ACLs by default, it is no longer necessary to be completely
stateless. On 0.12, using storage has benefits over JWT, because it does not
expose client JIDs to the push apps/services. In trunk, PASETO is stateless
and does not expose client JIDs.
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Sat, 14 Jan 2023 14:31:37 +0000 |
parents | 65082d91950e |
children |
line wrap: on
line source
-- for Prosody -- via dersd if module:get_host_type() ~= "component" then error(module.name.." should be loaded as a component, check out http://prosody.im/doc/components", 0); end local jid_split = require "util.jid".split; local st = require "util.stanza"; local componentmanager = require "core.componentmanager"; local datamanager = require "util.datamanager"; local timer = require "util.timer"; local http = require "net.http"; local json = require "util.json"; local base64 = require "util.encodings".base64; local component_host = module:get_host(); local component_name = module.name; local data_cache = {}; function print_r(obj) return require("util.serialization").serialize(obj); end function dmsg(jid, msg) module:log("debug", msg or "nil"); if jid ~= nil then module:send(st.message({to=jid, from=component_host, type='chat'}, msg or "nil")); end end function substring(string, start_string, ending_string) local s_value_start, s_value_finish = nil, nil; if start_string ~= nil then _, s_value_start = string:find(start_string); if s_value_start == nil then -- error return nil; end else return nil; end if ending_string ~= nil then _, s_value_finish = string:find(ending_string, s_value_start+1); if s_value_finish == nil then -- error return nil; end else s_value_finish = string:len()+1; end return string:sub(s_value_start+1, s_value_finish-1); end local http_timeout = 30; local http_queue = setmetatable({}, { __mode = "k" }); -- auto-cleaning nil elements data_cache['prosody_os'] = prosody.platform; data_cache['prosody_version'] = prosody.version; local http_headers = { ["User-Agent"] = "Prosody ("..data_cache['prosody_version'].."; "..data_cache['prosody_os']..")" --"ELinks (0.4pre5; Linux 2.4.27 i686; 80x25)", }; function http_action_callback(response, code, request, xcallback) if http_queue == nil or http_queue[request] == nil then return; end local id = http_queue[request]; http_queue[request] = nil; if xcallback == nil then dmsg(nil, "http_action_callback reports that xcallback is nil"); else xcallback(id, response, request); end return true; end function http_add_action(tid, url, method, post, fcallback) 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); http_queue[request] = tid; timer.add_task(http_timeout, function() http.destroy_request(request); end); return true; end local users = setmetatable({}, {__mode="k"}); local user = {}; user.__index = user; user.dosync = false; user.valid = false; user.data = {}; function user:login() userdata = datamanager.load(self.jid, component_host, "data"); if userdata ~= nil then self.data = userdata; if self.data['_twitter_sess'] ~= nil then http_headers['Cookie'] = "_twitter_sess="..self.data['_twitter_sess']..";"; end module:send(st.presence({to=self.jid, from=component_host})); self:twitterAction("VerifyCredentials"); if self.data.dosync == 1 then self.dosync = true; timer.add_task(self.data.refreshrate, function() return users[self.jid]:sync(); end) end else module:send(st.message({to=self.jid, from=component_host, type='chat'}, "You are not signed in.")); end end function user:logout() datamanager.store(self.jid, component_host, "data", self.data); self.dosync = false; module:send(st.presence({to=self.jid, from=component_host, type='unavailable'})); end function user:sync() if self.dosync then table.foreach(self.data.synclines, function(ind, line) self:twitterAction(line.name, {sinceid=line.sinceid}) end); return self.data.refreshrate; end end function user:signin() if datamanager.load(self.jid, component_host, "data") == nil then 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}) module:send(st.presence{to=self.jid, from=component_host, type='subscribe'}); module:send(st.presence{to=self.jid, from=component_host, type='subscribed'}); end end function user:signout() if datamanager.load(self.jid, component_host, "data") ~= nil then datamanager.store(self.jid, component_host, "data", nil); module:send(st.presence({to=self.jid, from=component_host, type='unavailable'})); module:send(st.presence({to=self.jid, from=component_host, type='unsubscribe'})); module:send(st.presence({to=self.jid, from=component_host, type='unsubscribed'})); end end local twitterApiUrl = "http://api.twitter.com"; local twitterApiVersion = "1"; local twitterApiDataType = "json"; local twitterActionUrl = function(action) return twitterApiUrl.."/"..twitterApiVersion.."/"..action.."."..twitterApiDataType end; local twitterActionMap = { PublicTimeline = { url = twitterActionUrl("statuses/public_timeline"), method = "GET", needauth = false, }, HomeTimeline = { url = twitterActionUrl("statuses/home_timeline"), method = "GET", needauth = true, }, FriendsTimeline = { url = twitterActionUrl("statuses/friends_timeline"), method = "GET", needauth = true, }, UserTimeline = { url = twitterActionUrl("statuses/friends_timeline"), method = "GET", needauth = true, }, VerifyCredentials = { url = twitterActionUrl("account/verify_credentials"), method = "GET", needauth = true, }, UpdateStatus = { url = twitterActionUrl("statuses/update"), method = "POST", needauth = true, }, Retweet = { url = twitterActionUrl("statuses/retweet/%tweetid"), method = "POST", needauth = true, } } function user:twitterAction(line, params) local action = twitterActionMap[line]; if action then local url = action.url; local post = {}; --if action.needauth and not self.valid and line ~= "VerifyCredentials" then -- return --end if action.needauth then http_headers['Authorization'] = "Basic "..base64.encode(self.data.login..":"..self.data.password); --url = string.gsub(url, "http\:\/\/", string.format("http://%s:%s@", self.data.login, self.data.password)); end if params and type(params) == "table" then post = params; end if action.method == "GET" and post ~= {} then url = url.."?"..http.formencode(post); end http_add_action(line, url, action.method, post, function(...) self:twitterActionResult(...) end); else module:send(st.message({to=self.jid, from=component_host, type='chat'}, "Wrong twitter action!")); end end local twitterActionResultMap = { PublicTimeline = {exec=function(jid, response) --module:send(st.message({to=jid, from=component_host, type='chat'}, print_r(response))); return end}, HomeTimeline = {exec=function(jid, response) --module:send(st.message({to=jid, from=component_host, type='chat'}, print_r(response))); return end}, FriendsTimeline = {function(jid, response) return end}, UserTimeline = {exec=function(jid, response) return end}, VerifyCredentials = {exec=function(jid, response) if response ~= nil and response.id ~= nil then users[jid].valid = true; users[jid].id = response.id; end return end}, UpdateStatus = {exec=function(jid, response) return end}, Retweet = {exec=function(jid, response) return end} } function user:twitterActionResult(id, response, request) if request ~= nil and request.responseheaders['set-cookie'] ~= nil and request.responseheaders['location'] ~= nil then --self.data['_twitter_sess'] = substring(request.responseheaders['set-cookie'], "_twitter_sess=", ";"); --http_add_action(id, request.responseheaders['location'], "GET", {}, function(...) self:twitterActionResult(...) end); return true; end local result, tmp_json = pcall(function() json.decode(response or "{}") end); if result and id ~= nil then twitterActionResultMap[id]:exec(self.jid, tmp_json); end return true; end function iq_success(event) local origin, stanza = event.origin, event.stanza; local reply = data_cache.success; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}); data_cache.success = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end function iq_disco_info(event) local origin, stanza = event.origin, event.stanza; local from = {}; from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = from.node.."@"..from.host; local reply = data_cache.disco_info; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#info") :tag("identity", {category='gateway', type='chat', name=component_name}):up(); reply = reply:tag("feature", {var="urn:xmpp:receipts"}):up(); reply = reply:tag("feature", {var="http://jabber.org/protocol/commands"}):up(); reply = reply:tag("feature", {var="jabber:iq:register"}):up(); --reply = reply:tag("feature", {var="jabber:iq:time"}):up(); --reply = reply:tag("feature", {var="jabber:iq:version"}):up(); --reply = reply:tag("feature", {var="http://jabber.org/protocol/stats"}):up(); data_cache.disco_info = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end function iq_disco_items(event) local origin, stanza = event.origin, event.stanza; local reply = data_cache.disco_items; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#items"); data_cache.disco_items = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end function iq_register(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "get" then local reply = data_cache.registration_form; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}) :tag("query", { xmlns="jabber:iq:register" }) :tag("instructions"):text("Enter your twitter data"):up() :tag("username"):up() :tag("password"):up(); data_cache.registration_form = reply end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); elseif stanza.attr.type == "set" then local from = {}; from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = from.node.."@"..from.host; local username, password = "", ""; local reply; for _, tag in ipairs(stanza.tags[1].tags) do if tag.name == "remove" then users[bjid]:signout(); iq_success(event); return true; end if tag.name == "username" then username = tag[1]; end if tag.name == "password" then password = tag[1]; end end if username ~= nil and password ~= nil then users[bjid] = setmetatable({}, user); users[bjid].jid = bjid; users[bjid].data.login = username; users[bjid].data.password = password; users[bjid]:signin(); users[bjid]:login(); end iq_success(event); return true; end end function presence_stanza_handler(event) local origin, stanza = event.origin, event.stanza; local to = {}; local from = {}; local pres = {}; to.node, to.host, to.resource = jid_split(stanza.attr.to); from.node, from.host, from.resource = jid_split(stanza.attr.from); pres.type = stanza.attr.type; for _, tag in ipairs(stanza.tags) do pres[tag.name] = tag[1]; end local from_bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end if pres.type == nil then if users[from_bjid] ~= nil then -- Status change if pres['status'] ~= nil and users[from_bjid]['data']['sync_status'] then users[from_bjid]:twitterAction("UpdateStatus", {status=pres['status']}); end else -- User login request users[from_bjid] = setmetatable({}, user); users[from_bjid].jid = from_bjid; users[from_bjid]:login(); end origin.send(st.presence({to=from_bjid, from=component_host})); elseif pres.type == 'subscribe' and users[from_bjid] ~= nil then origin.send(st.presence{to=from_bjid, from=component_host, type='subscribed'}); elseif pres.type == 'unsubscribed' and users[from_bjid] ~= nil then users[from_bjid]:logout(); users[from_bjid]:signout(); users[from_bjid] = nil; elseif pres.type == 'unavailable' and users[from_bjid] ~= nil then users[from_bjid]:logout(); users[from_bjid] = nil; end return true; end function confirm_message_delivery(event) 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"}); origin.send(reply); return true; end function message_stanza_handler(event) local origin, stanza = event.origin, event.stanza; local to = {}; local from = {}; local msg = {}; to.node, to.host, to.resource = jid_split(stanza.attr.to); from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end local to_bjid = nil; if to.node ~= nil and to.host ~= nil then to_bjid = to.node.."@"..to.host; elseif to.host ~= nil then to_bjid = to.host; end for _, tag in ipairs(stanza.tags) do msg[tag.name] = tag[1]; if tag.attr.xmlns == "urn:xmpp:receipts" then confirm_message_delivery({origin=origin, stanza=stanza}); end -- can handle more xmlns end -- Now parse the message if stanza.attr.to == component_host then if msg.body == "!myinfo" then if users[from_bjid] ~= nil then origin.send(st.message({to=stanza.attr.from, from=component_host, type='chat'}, print_r(users[from_bjid]))); end end -- Other messages go to twitter user:twitterAction("UpdateStatus", {status=msg.body}); else -- Message to uid@host/resource end return true; end module:hook("presence/host", presence_stanza_handler); module:hook("message/host", message_stanza_handler); module:hook("iq/host/jabber:iq:register:query", iq_register); module:hook("iq/host/http://jabber.org/protocol/disco#info:query", iq_disco_info); module:hook("iq/host/http://jabber.org/protocol/disco#items:query", iq_disco_items); module:hook("iq/host", function(data) -- IQ to a local host recieved local origin, stanza = data.origin, data.stanza; if stanza.attr.type == "get" or stanza.attr.type == "set" then return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); else module:fire_event("iq/host/"..stanza.attr.id, data); return true; end end); module.unload = function() componentmanager.deregister_component(component_host); end component = componentmanager.register_component(component_host, function() return; end);