Mercurial > prosody-modules
comparison mod_pubsub_feed/mod_pubsub_feed.lua @ 322:637dc0a04052
mod_pubsub_feed: Implement PubSubHubbub subscriber
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Mon, 31 Jan 2011 03:37:16 +0100 |
| parents | b81e4f86a231 |
| children | 433bf7dc3e7a |
comparison
equal
deleted
inserted
replaced
| 321:661f64627fed | 322:637dc0a04052 |
|---|---|
| 1 -- Fetches Atom feeds and publishes to PubSub nodes | 1 -- Fetches Atom feeds and publishes to PubSub nodes |
| 2 -- | |
| 3 -- Depends: http://code.matthewwild.co.uk/lua-feeds | |
| 2 -- | 4 -- |
| 3 -- Config: | 5 -- Config: |
| 4 -- Component "pubsub.example.com" "pubsub" | 6 -- Component "pubsub.example.com" "pubsub" |
| 5 -- modules_enabled = { | 7 -- modules_enabled = { |
| 6 -- "pubsub_feed"; | 8 -- "pubsub_feed"; |
| 7 -- } | 9 -- } |
| 8 -- feeds = { -- node -> url | 10 -- feeds = { -- node -> url |
| 9 -- prosody_blog = "http://blog.prosody.im/feed/atom.xml"; | 11 -- prosody_blog = "http://blog.prosody.im/feed/atom.xml"; |
| 10 -- } | 12 -- } |
| 11 -- feed_pull_interval = 20 -- minutes | 13 -- feed_pull_interval = 20 -- minutes |
| 14 -- | |
| 15 -- Reference | |
| 16 -- http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html | |
| 12 | 17 |
| 13 local modules = hosts[module.host].modules; | 18 local modules = hosts[module.host].modules; |
| 14 if not modules.pubsub then | 19 if not modules.pubsub then |
| 20 --FIXME Should this throw an error() instead? | |
| 15 module:log("warn", "Pubsub needs to be loaded on this host"); | 21 module:log("warn", "Pubsub needs to be loaded on this host"); |
| 16 end | 22 end |
| 23 | |
| 24 | |
| 25 local t_insert = table.insert; | |
| 17 local add_task = require "util.timer".add_task; | 26 local add_task = require "util.timer".add_task; |
| 18 local date, time = os.date, os.time; | 27 local date, time = os.date, os.time; |
| 19 local dt_parse, dt_datetime = require "util.datetime".parse, require "util.datetime".datetime; | 28 local dt_parse, dt_datetime = require "util.datetime".parse, require "util.datetime".datetime; |
| 20 local http = require "net.http"; | 29 local http = require "net.http"; |
| 21 local parse_feed = require "feeds".feed_from_string; | 30 local parse_feed = require "feeds".feed_from_string; |
| 22 local st = require "util.stanza"; | 31 local st = require "util.stanza"; |
| 32 local httpserver = require "net.httpserver"; | |
| 33 local formencode = require "net.http".formencode; | |
| 34 local dump = require "util.serialization".serialize; | |
| 35 | |
| 36 local urldecode = require "net.http".urldecode; | |
| 37 local urlencode = require "net.http".urlencode; | |
| 38 local urlparams = --require "net.http".getQueryParams or whatever MattJ names it, FIXME | |
| 39 function(s) | |
| 40 if not s:match("=") then return urldecode(s); end | |
| 41 local r = {} | |
| 42 s:gsub("([^=&]*)=([^&]*)", function(k,v) | |
| 43 r[ urldecode(k) ] = urldecode(v); | |
| 44 return nil | |
| 45 end) | |
| 46 return r | |
| 47 end; | |
| 23 | 48 |
| 24 local config = module:get_option("feeds") or { | 49 local config = module:get_option("feeds") or { |
| 25 planet_jabber = "http://planet.jabber.org/atom.xml"; | 50 planet_jabber = "http://planet.jabber.org/atom.xml"; |
| 26 prosody_blog = "http://blog.prosody.im/feed/atom.xml"; | 51 prosody_blog = "http://blog.prosody.im/feed/atom.xml"; |
| 27 }; | 52 }; |
| 28 local refresh_interval = (module:get_option("feed_pull_interval") or 15) * 60; | 53 local refresh_interval = module:get_option_number("feed_pull_interval", 15) * 60; |
| 54 local use_pubsubhubub = module:get_option_boolean("use_pubsubhubub", true); -- HTTP by default or not? | |
| 29 local feed_list = { } | 55 local feed_list = { } |
| 30 for node, url in pairs(config) do | 56 for node, url in pairs(config) do |
| 31 feed_list[node] = { url = url }; | 57 feed_list[node] = { url = url; node = node; last_update = 0 }; |
| 32 end | 58 end |
| 33 | 59 |
| 34 local function update(item, callback) | 60 local response_codes = { |
| 61 ["202"] = "Accepted"; | |
| 62 ["400"] = "Bad Request"; | |
| 63 ["501"] = "Not Implemented"; | |
| 64 }; | |
| 65 | |
| 66 local function http_response(code, headers, body) | |
| 67 return { | |
| 68 status = (type(code) == "number" and code .. " " .. response_codes[tostring(code)]) or code; | |
| 69 headers = headers or {}; | |
| 70 body = body or "<h1>" .. response_codes[tostring(code)] .. "</h1>\n"; | |
| 71 }; | |
| 72 end | |
| 73 | |
| 74 local actor = module.host.."/"..module.name; | |
| 75 | |
| 76 function update_entry(item) | |
| 77 local node = item.node; | |
| 78 --module:log("debug", "parsing %d bytes of data in node %s", #item.data or 0, node) | |
| 79 local feed = parse_feed(item.data); | |
| 80 module:log("debug", "updating node %s", node); | |
| 81 for _, entry in ipairs(feed) do | |
| 82 entry.attr.xmlns = "http://www.w3.org/2005/Atom"; | |
| 83 | |
| 84 local e_published = entry:get_child("published"); | |
| 85 e_published = e_published and e_published:get_text(); | |
| 86 e_published = e_published and dt_parse(e_published); | |
| 87 local e_updated = entry:get_child("updated"); | |
| 88 e_updated = e_updated and e_updated:get_text(); | |
| 89 e_updated = e_updated and dt_parse(e_updated); | |
| 90 | |
| 91 local timestamp = e_updated or e_published or nil; | |
| 92 --module:log("debug", "timestamp is %s, item.last_update is %s", tostring(timestamp), tostring(item.last_update)); | |
| 93 if not timestamp or not item.last_update or timestamp > item.last_update then | |
| 94 local id = entry:get_child("id"); | |
| 95 id = id and id:get_text() or item.url.."#"..dt_datetime(timestamp); -- Missing id, so make one up | |
| 96 local xitem = st.stanza("item", { id = id }):add_child(entry); | |
| 97 -- TODO Put data from /feed into item/source | |
| 98 | |
| 99 module:log("debug", "publishing to %s, id %s", node, id); | |
| 100 local ok, err = modules.pubsub.service:publish(node, actor, id, xitem); | |
| 101 if not ok then | |
| 102 if err == "item-not-found" then -- try again | |
| 103 module:log("debug", "got item-not-found, creating %s and trying again", node); | |
| 104 local ok, err = modules.pubsub.service:create(node, actor); | |
| 105 if not ok then | |
| 106 module:log("error", "could not create node %s: %s", node, err); | |
| 107 return; | |
| 108 end | |
| 109 local ok, err = modules.pubsub.service:publish(node, actor, id, xitem); | |
| 110 if not ok then | |
| 111 module:log("error", "could not create or publish node %s: %s", node, err); | |
| 112 return | |
| 113 end | |
| 114 else | |
| 115 module:log("error", "publishing %s failed: %s", node, err); | |
| 116 end | |
| 117 end | |
| 118 end | |
| 119 end | |
| 120 | |
| 121 if use_pubsubhubub and not item.subscription then | |
| 122 module:log("debug", "check if %s has a hub", item.node); | |
| 123 local hub = feed.links and feed.links.hub; | |
| 124 if hub then | |
| 125 item.hub = hub; | |
| 126 module:log("debug", "%s has a hub: %s", item.node, item.hub); | |
| 127 subscribe(item); | |
| 128 end | |
| 129 end | |
| 130 end | |
| 131 | |
| 132 function fetch(item, callback) -- HTTP Pull | |
| 35 local headers = { }; | 133 local headers = { }; |
| 36 if item.data and item.last_update then | 134 if item.data and item.last_update then |
| 37 headers["If-Modified-Since"] = date("!%a, %d %b %Y %T %Z", item.last_update); | 135 headers["If-Modified-Since"] = date("!%a, %d %b %Y %T %Z", item.last_update); |
| 38 end | 136 end |
| 39 http.request(item.url, {headers = headers}, function(data, code, req) | 137 http.request(item.url, { headers = headers }, function(data, code, req) |
| 40 if code == 200 then | 138 if code == 200 then |
| 41 item.data = data; | 139 item.data = data; |
| 42 callback(item) | 140 if callback then callback(item) end |
| 43 item.last_update = time(); | 141 item.last_update = time(); |
| 44 end | 142 end |
| 45 if code == 304 then | 143 if code == 304 then |
| 46 item.last_update = time(); | 144 item.last_update = time(); |
| 47 end | 145 end |
| 48 end); | 146 end); |
| 49 end | 147 end |
| 50 | 148 |
| 51 local actor = module.host.."/"..module.name; | 149 function refresh_feeds() |
| 52 | 150 --module:log("debug", "Refreshing feeds"); |
| 53 local function refresh_feeds() | |
| 54 for node, item in pairs(feed_list) do | 151 for node, item in pairs(feed_list) do |
| 55 update(item, function(item) | 152 --FIXME Don't fetch feeds which have a subscription |
| 56 local feed = parse_feed(item.data); | 153 -- Otoho, what if the subscription expires or breaks? |
| 57 module:log("debug", "node: %s", node); | 154 if item.last_update + refresh_interval < time() then |
| 58 for _, entry in ipairs(feed) do | 155 module:log("debug", "checking %s", item.node); |
| 59 entry.attr.xmlns = "http://www.w3.org/2005/Atom"; | 156 fetch(item, update_entry); |
| 60 | 157 end |
| 61 local e_published = entry:get_child("published"); | 158 end |
| 62 e_published = e_published and e_published[1]; | 159 return refresh_interval; |
| 63 e_published = e_published and dt_parse(e_published); | 160 end |
| 64 local e_updated = entry:get_child("updated"); | 161 |
| 65 e_updated = e_updated and e_updated[1]; | 162 function subscribe(feed, challenge) |
| 66 e_updated = e_updated and dt_parse(e_updated); | 163 local _body, body = { |
| 67 | 164 ["hub.callback"] = "http://"..module.host..":5280/callback?node=" .. urlencode(feed.node); --FIXME figure out your own hostname reliably? |
| 68 local timestamp = e_updated or e_published or nil; | 165 ["hub.mode"] = "subscribe"; --TODO unsubscribe |
| 69 module:log("debug", "timestamp is %s, item.last_update is %s", tostring(timestamp), tostring(item.last_update)); | 166 ["hub.topic"] = feed.url; |
| 70 if not timestamp or not item.last_update or timestamp > item.last_update then | 167 ["hub.verify"] = "async"; |
| 71 local id = entry:get_child("id"); | 168 ["hub.verify_token"] = challenge; |
| 72 id = id[1] or item.url.."#"..dt_datetime(timestamp); -- Missing id, so make one up | 169 --["hub.lease_seconds"] = ""; |
| 73 local item = st.stanza("item", { id = id }):add_child(entry); | 170 }, { }; |
| 74 | 171 for name, value in pairs(_body) do |
| 75 module:log("debug", "publishing to %s, id %s", node, id); | 172 t_insert(body, { name = name, value = value }); |
| 76 local ok, err = modules.pubsub.service:publish(node, actor, id, item); | 173 end --FIXME Why do I have to do this? |
| 77 if not ok then | 174 body = formencode(body); |
| 78 if err == "item-not-found" then -- try again | 175 |
| 79 module:log("debug", "got item-not-found, creating %s and trying again", node); | 176 --module:log("debug", "subscription request, body: %s", body); |
| 80 local ok, err = modules.pubsub.service:create(node, actor); | 177 |
| 81 if not ok then | 178 --FIXME The subscription states and related stuff |
| 82 module:log("error", "could not create node: %s", err); | 179 --feed.subscription = challenge and "asked" or "asking"; |
| 83 return; | 180 feed.subscription = "asking"; |
| 84 end | 181 http.request(feed.hub, { body = body }, function(data, code, req) |
| 85 local ok, err = modules.pubsub.service:publish(node, actor, id, item); | 182 local code = tostring(code); |
| 86 if not ok then | 183 module:log("debug", "subscription to %s submitted, staus %s", feed.node, code); |
| 87 module:log("error", "still could not create node: %s", err); | 184 if code == '202' then |
| 88 return | 185 if challenge then |
| 89 end | 186 module:log("debug", "subscribe to %s confirmed", feed.node); |
| 90 else | 187 feed.subscription = "active"; |
| 91 module:log("error", "publish failed: %s", err); | 188 else |
| 92 end | 189 module:log("debug", "subscription to %s submitted", feed.node); |
| 93 end | 190 --feed.subscription = "incomplete"; |
| 94 end | |
| 95 end | 191 end |
| 96 end); | 192 end |
| 97 end | 193 end); |
| 98 return refresh_interval; | 194 end |
| 195 | |
| 196 function handle_http_request(method, body, request) | |
| 197 --module:log("debug", "%s request to %s%s with body %s", method, request.url.path, request.url.query and "?" .. request.url.query or "", #body > 0 and body or "empty"); | |
| 198 local query = request.url.query; | |
| 199 if query and type(query) == "string" then | |
| 200 query = urlparams(query); | |
| 201 --module:log("debug", "GET data: %s", dump(query)); | |
| 202 end | |
| 203 | |
| 204 -- TODO http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html#authednotify | |
| 205 | |
| 206 if method == "GET" then | |
| 207 if query.node and feed_list[query.node] then | |
| 208 local feed = feed_list[query.node]; | |
| 209 local challenge = query["hub.challenge"]; | |
| 210 if challenge and feed.subscription == "asking" then | |
| 211 module:log("debug", "got a challenge for %s: %s", feed.node, challenge); | |
| 212 subscribe(feed, challenge); | |
| 213 return http_response(202); | |
| 214 end | |
| 215 end | |
| 216 return http_response(400); | |
| 217 elseif method == "POST" then | |
| 218 if #body > 0 and feed_list[query.node] then | |
| 219 module:log("debug", "got %d bytes PuSHed for %s", #body, query.node); | |
| 220 local feed = feed_list[query.node]; | |
| 221 feed.data = body; | |
| 222 update_entry(feed); | |
| 223 feed.last_update = time(); | |
| 224 return http_response(202); | |
| 225 end | |
| 226 return http_response(400); | |
| 227 end | |
| 228 return http_response(501); | |
| 99 end | 229 end |
| 100 | 230 |
| 101 function init() | 231 function init() |
| 232 module:log("debug", "initiating", module.name); | |
| 233 if use_pubsubhubub then | |
| 234 httpserver.new{ port = 5280, base = "callback", handler = handle_http_request } | |
| 235 end | |
| 102 add_task(0, refresh_feeds); | 236 add_task(0, refresh_feeds); |
| 103 end | 237 end |
| 104 | 238 |
| 105 if prosody.start_time then -- already started | 239 if prosody.start_time then -- already started |
| 106 init(); | 240 init(); |
| 107 else | 241 else |
| 108 prosody.events.add_handler("server-started", init); | 242 prosody.events.add_handler("server-started", init); |
| 109 end | 243 end |
| 110 |
