comparison mod_pubsub_feeds/mod_pubsub_feeds.lua @ 5653:62c6e17a5e9d

Merge
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Mon, 18 Sep 2023 08:24:19 -0500
parents bc292c84f56c
children 3f75ac4311bf
comparison
equal deleted inserted replaced
5652:eade7ff9f52c 5653:62c6e17a5e9d
1 -- Fetches Atom feeds and publishes to PubSub nodes 1 -- Fetches Atom feeds and publishes to PubSub nodes
2 --
3 -- Config:
4 -- Component "pubsub.example.com" "pubsub"
5 -- modules_enabled = {
6 -- "pubsub_feeds";
7 -- }
8 -- feeds = { -- node -> url
9 -- prosody_blog = "http://blog.prosody.im/feed/atom.xml";
10 -- }
11 -- feed_pull_interval = 20 -- minutes
12 --
13 -- Reference
14 -- http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.4.html
15 2
16 local pubsub = module:depends"pubsub"; 3 local pubsub = module:depends"pubsub";
17 4
18 local time = os.time; 5 local time = os.time;
19 local dt_parse, dt_datetime = require "util.datetime".parse, require "util.datetime".datetime; 6 local dt_parse, dt_datetime = require "util.datetime".parse, require "util.datetime".datetime;
34 return translate_rss(feed); 21 return translate_rss(feed);
35 end 22 end
36 return nil, "unsupported-format"; 23 return nil, "unsupported-format";
37 end 24 end
38 25
39 local use_pubsubhubub = module:get_option_boolean("use_pubsubhubub", true); 26 local use_pubsubhubub = module:get_option_boolean("use_pubsubhubub", false);
40 if use_pubsubhubub then 27 if use_pubsubhubub then
41 module:depends"http"; 28 module:depends"http";
42 end 29 end
43 30
44 local http = require "net.http"; 31 local http = require "net.http";
45 local formdecode = http.formdecode; 32 local formdecode = http.formdecode;
46 local formencode = http.formencode; 33 local formencode = http.formencode;
47 34
48 local feed_list = module:shared("feed_list"); 35 local feed_list = module:shared("feed_list");
49 local refresh_interval = module:get_option_number("feed_pull_interval", 15) * 60; 36 local legacy_refresh_interval = module:get_option_number("feed_pull_interval", 15);
37 local refresh_interval = module:get_option_number("feed_pull_interval_seconds", legacy_refresh_interval*60);
50 local lease_length = tostring(math.floor(module:get_option_number("feed_lease_length", 86400))); 38 local lease_length = tostring(math.floor(module:get_option_number("feed_lease_length", 86400)));
51 39
52 function module.load() 40 function module.load()
53 local config = module:get_option("feeds", { }); 41 local config = module:get_option("feeds", { });
54 local ok, nodes = pubsub.service:get_nodes(true); 42 local ok, nodes = pubsub.service:get_nodes(true);
58 if type(node) == "number" then 46 if type(node) == "number" then
59 node = url; 47 node = url;
60 end 48 end
61 new_feed_list[node] = true; 49 new_feed_list[node] = true;
62 if not feed_list[node] then 50 if not feed_list[node] then
63 feed_list[node] = { url = url; node = node; last_update = 0 }; 51 local ok, err = pubsub.service:create(node, true);
52 if ok or err == "conflict" then
53 feed_list[node] = { url = url; node = node; last_update = 0 };
54 else
55 module:log("error", "Could not create node %s: %s", node, err);
56 end
64 else 57 else
65 feed_list[node].url = url; 58 feed_list[node].url = url;
66 end 59 end
67 if not nodes[node] then 60 if not nodes[node] then
68 feed_list[node].last_update = 0; 61 feed_list[node].last_update = 0;
73 feed_list[node] = nil; 66 feed_list[node] = nil;
74 end 67 end
75 end 68 end
76 end 69 end
77 70
78 function update_entry(item) 71 function update_entry(item, data)
79 local node = item.node; 72 local node = item.node;
80 module:log("debug", "parsing %d bytes of data in node %s", #item.data or 0, node) 73 module:log("debug", "parsing %d bytes of data in node %s", #data or 0, node)
81 local feed, err = parse_feed(item.data); 74 local feed, err = parse_feed(data);
82 if not feed then 75 if not feed then
83 module:log("error", "Could not parse feed %q: %s", item.url, err); 76 module:log("error", "Could not parse feed %q: %s", item.url, err);
84 module:log("debug", "Feed data:\n%s\n.", item.data); 77 module:log("debug", "Feed data:\n%s\n.", data);
85 return; 78 return;
86 end 79 end
87 local entries = {}; 80 local entries = {};
88 for entry in feed:childtags("entry") do 81 for entry in feed:childtags("entry") do
89 table.insert(entries, entry); 82 table.insert(entries, entry);
90 end 83 end
91 local ok, items = pubsub.service:get_items(node, true); 84 local ok, last_id = pubsub.service:get_last_item(node, true);
92 if not ok then 85 if not ok then
93 local ok, err = pubsub.service:create(node, true); 86 module:log("error", "PubSub node %q missing: %s", node, last_id);
94 if not ok then 87 return
95 module:log("error", "Could not create node %s: %s", node, err); 88 end
96 return; 89
97 end 90 local start_from = #entries;
98 items = {}; 91 for i, entry in ipairs(entries) do
99 end 92 local id = entry:get_child_text("id");
100 for i = #entries, 1, -1 do -- Feeds are usually in reverse order 93 if not id then
94 local link = entry:get_child("link");
95 if link then
96 module:log("debug", "Feed %q item %s is missing an id, using <link> instead", item.url, entry:top_tag());
97 id = link and link.attr.href;
98 else
99 module:log("error", "Feed %q item %s is missing both id and link, this feed is unusable", item.url, entry:top_tag());
100 return;
101 end
102 entry:text_tag("id", id);
103 end
104
105 if last_id == id then
106 -- This should be the first item that we already have.
107 start_from = i-1;
108 break
109 end
110 end
111
112 for i = start_from, 1, -1 do -- Feeds are usually in reverse order
101 local entry = entries[i]; 113 local entry = entries[i];
102 entry.attr.xmlns = xmlns_atom; 114 entry.attr.xmlns = xmlns_atom;
103 115
104 local e_published = entry:get_child_text("published"); 116 local id = entry:get_child_text("id");
105 e_published = e_published and dt_parse(e_published); 117
106 local e_updated = entry:get_child_text("updated"); 118 local timestamp = dt_parse(entry:get_child_text("published"));
107 e_updated = e_updated and dt_parse(e_updated); 119 if not timestamp then
108 120 timestamp = time();
109 local timestamp = e_updated or e_published or nil; 121 entry:text_tag("published", dt_datetime(timestamp));
110 --module:log("debug", "timestamp is %s, item.last_update is %s", tostring(timestamp), tostring(item.last_update)); 122 end
123
111 if not timestamp or not item.last_update or timestamp > item.last_update then 124 if not timestamp or not item.last_update or timestamp > item.last_update then
112 local id = entry:get_child_text("id"); 125 local xitem = st.stanza("item", { id = id, xmlns = "http://jabber.org/protocol/pubsub" }):add_child(entry);
113 if not id then 126 -- TODO Put data from /feed into item/source
114 local link = entry:get_child("link"); 127
115 id = link and link.attr.href; 128 local ok, err = pubsub.service:publish(node, true, id, xitem);
116 end 129 if not ok then
117 if not id then 130 module:log("error", "Publishing to node %s failed: %s", node, err);
118 -- Sigh, no link? 131 elseif timestamp then
119 id = feed.url .. "#" .. hmac_sha1(feed.url, tostring(entry), true) .. "@" .. dt_datetime(timestamp); 132 item.last_update = timestamp;
120 end
121 if not items[id] then
122 local xitem = st.stanza("item", { id = id, xmlns = "http://jabber.org/protocol/pubsub" }):add_child(entry);
123 -- TODO Put data from /feed into item/source
124
125 --module:log("debug", "publishing to %s, id %s", node, id);
126 local ok, err = pubsub.service:publish(node, true, id, xitem);
127 if not ok then
128 module:log("error", "Publishing to node %s failed: %s", node, err);
129 end
130 end 133 end
131 end 134 end
132 end 135 end
133 136
134 if item.lease_expires and item.lease_expires > time() then 137 if item.lease_expires and item.lease_expires > time() then
146 end 149 end
147 end 150 end
148 end 151 end
149 152
150 function fetch(item, callback) -- HTTP Pull 153 function fetch(item, callback) -- HTTP Pull
151 local headers = { }; 154 local headers = {
152 if item.data and item.etag then 155 ["If-None-Match"] = item.etag;
153 headers["If-None-Match"] = item.etag; 156 ["Accept"] = "application/atom+xml, application/x-rss+xml, application/xml";
154 end 157 };
155 http.request(item.url, { headers = headers }, function(data, code, resp) 158 http.request(item.url, { headers = headers }, function(data, code, resp)
156 if code == 200 then 159 if code == 200 then
157 item.data = data; 160 if callback then callback(item, data) end
158 if callback then callback(item) end
159 item.last_update = time();
160 if resp.headers then 161 if resp.headers then
161 item.etag = resp.headers.etag 162 item.etag = resp.headers.etag
162 end 163 end
163 elseif code == 304 then 164 elseif code == 304 then
164 item.last_update = time(); 165 module:log("debug", "No updates to %q", item.url);
165 elseif code == 301 and resp.headers.location then 166 elseif code == 301 and resp.headers.location then
166 module:log("info", "Feed %q has moved to %q", item.url, resp.headers.location); 167 module:log("info", "Feed %q has moved to %q", item.url, resp.headers.location);
167 elseif code <= 100 then 168 elseif code <= 100 then
168 module:log("error", "Error fetching %q: %q[%d]", item.url, data, code); 169 module:log("error", "Error fetching %q: %q[%d]", item.url, data, code);
169 else 170 else
266 module:log("debug", "Invalid signature, got %s but wanted %s", tostring(signature), tostring(localsig)); 267 module:log("debug", "Invalid signature, got %s but wanted %s", tostring(signature), tostring(localsig));
267 return 401; 268 return 401;
268 end 269 end
269 module:log("debug", "Valid signature"); 270 module:log("debug", "Valid signature");
270 end 271 end
271 feed.data = body; 272 update_entry(feed, body);
272 update_entry(feed);
273 feed.last_update = time();
274 return 202; 273 return 202;
275 end 274 end
276 return 400; 275 return 400;
277 end 276 end
278 return 501; 277 return 501;