| 1 | local io, string = io, string; |
| 2 | local error = error; |
| 3 | local print = print; |
| 4 | local t_insert = table.insert; |
| 5 | local ipairs = ipairs; |
| 6 | |
| 7 | local http = require "socket.http"; |
| 8 | local st = require "stanza"; |
| 9 | local new_stream = require "xmlstream".new; |
| 10 | |
| 11 | local xmlns_atom = "http://www.w3.org/2005/Atom"; |
| 12 | |
| 13 | --[[ |
| 14 | By popular request; a fluffy bunny: |
| 15 | |
| 16 | (\ /) |
| 17 | (. .) |
| 18 | c(")(") |
| 19 | |
| 20 | --]] |
| 21 | |
| 22 | module "feeds" |
| 23 | |
| 24 | local translate_entry = {}; |
| 25 | |
| 26 | function translate_entry.atom(feed, stanza) |
| 27 | if stanza.name == "entry" then |
| 28 | feed[#feed+1] = stanza; |
| 29 | elseif stanza.name == "author" then |
| 30 | feed.author = {}; |
| 31 | for i,a in ipairs(stanza) do |
| 32 | if a.name and a[1] then |
| 33 | feed.author[a.name] = a[1]; |
| 34 | end |
| 35 | end |
| 36 | elseif stanza.name == "link" then |
| 37 | if not feed.links then feed.links = {}; end |
| 38 | if stanza.attr.href then |
| 39 | if stanza.attr.rel then |
| 40 | feed.links[stanza.attr.rel] = stanza.attr.href; |
| 41 | else |
| 42 | t_insert(feed.links, stanza.attr.href) |
| 43 | end |
| 44 | end |
| 45 | else |
| 46 | feed[stanza.name] = stanza:get_text(); |
| 47 | end |
| 48 | end |
| 49 | |
| 50 | -- RSS->Atom translator |
| 51 | |
| 52 | -- Helpers to translate item child elements |
| 53 | local rss2atom = {}; |
| 54 | function rss2atom.title(atom_entry, tag) |
| 55 | atom_entry:tag("title"):text(tag:get_text()):up(); |
| 56 | end |
| 57 | |
| 58 | function rss2atom.link(atom_entry, tag) |
| 59 | atom_entry:tag("link", { href = tag:get_text() }):up(); |
| 60 | end |
| 61 | |
| 62 | function rss2atom.author(atom_entry, tag) |
| 63 | atom_entry:tag("author") |
| 64 | :tag("email"):text(tag:get_text()):up() |
| 65 | :up(); |
| 66 | end |
| 67 | |
| 68 | function rss2atom.guid(atom_entry, tag) |
| 69 | atom_entry:tag("id"):text(tag:get_text()):up(); |
| 70 | end |
| 71 | |
| 72 | function rss2atom.category(atom_entry, tag) |
| 73 | atom_entry:tag("category", { term = tag:get_text(), scheme = tag.attr.domain }):up(); |
| 74 | end |
| 75 | |
| 76 | function rss2atom.description(atom_entry, tag) |
| 77 | atom_entry:tag("summary"):text(tag:get_text()):up(); |
| 78 | end |
| 79 | |
| 80 | local months = { |
| 81 | jan = "01", feb = "02", mar = "03", apr = "04", may = "05", jun = "06"; |
| 82 | jul = "07", aug = "08", sep = "09", oct = "10", nov = "11", dec = "12"; |
| 83 | }; |
| 84 | |
| 85 | function rss2atom.pubDate(atom_entry, tag) |
| 86 | local pubdate = tag:get_text():gsub("^%a+,", ""):gsub("^%s*", ""); |
| 87 | local date, month, year, hour, minute, second, zone = |
| 88 | pubdate:match("^(%d%d?) (%a+) (%d+) (%d+):(%d+):?(%d*) ?(.*)$"); |
| 89 | if not date then return; end |
| 90 | if #date == 1 then |
| 91 | date = "0"..date; |
| 92 | end |
| 93 | month = months[month:sub(1,3):lower()]; |
| 94 | if #year == 2 then -- GAH! |
| 95 | if tonumber(year) > 80 then |
| 96 | year = "19"..year; |
| 97 | else |
| 98 | year = "20"..year; |
| 99 | end |
| 100 | end |
| 101 | if zone == "UT" or zone == "GMT" then zone = "Z"; end |
| 102 | if #second == 0 then |
| 103 | second = "00"; |
| 104 | end |
| 105 | local date_string = string.format("%s-%s-%sT%s:%s:%s%s", year, month, date, hour, minute, second, zone); |
| 106 | atom_entry:tag("published"):text(date_string):up(); |
| 107 | end |
| 108 | |
| 109 | -- Translate a single item to atom |
| 110 | function translate_entry.rss(feed, stanza) |
| 111 | if stanza.name == "item" then |
| 112 | local atom_entry = st.stanza("entry", { xmlns = xmlns_atom }); |
| 113 | for tag in stanza:childtags() do |
| 114 | local translator = rss2atom[tag.name]; |
| 115 | if translator then |
| 116 | translator(atom_entry, tag); |
| 117 | end |
| 118 | end |
| 119 | translate_entry.atom(feed, atom_entry:reset()); |
| 120 | else |
| 121 | translate_entry.atom(feed, stanza); |
| 122 | end |
| 123 | end |
| 124 | |
| 125 | local function new_feed_stream(feed) |
| 126 | local callbacks = { |
| 127 | streamopened = function (feed, attr, name) |
| 128 | if name == "feed" and attr.xmlns == xmlns_atom then |
| 129 | feed.type = "atom"; |
| 130 | feed.xmlns = xmlns_atom; |
| 131 | feed.notopen = nil; |
| 132 | elseif name == "rss" and attr.xmlns == "" then |
| 133 | feed.type = "rss"; |
| 134 | feed.xmlns = ""; |
| 135 | -- Don't open until channel |
| 136 | elseif feed.type == "rss" and name == "channel" and attr.xmlns == feed.xmlns then |
| 137 | feed.notopen = nil; |
| 138 | else |
| 139 | error("Unsupported feed type: <" |
| 140 | ..name |
| 141 | ..(attr.xmlns and (" xmlns='"..attr.xmlns.."'") or "") |
| 142 | ..">" |
| 143 | ); |
| 144 | end |
| 145 | end; |
| 146 | |
| 147 | streamclosed = function (feed) |
| 148 | end; |
| 149 | |
| 150 | handlestanza = function (feed, stanza) |
| 151 | -- Skip tags not in the feed's default namespace |
| 152 | if stanza.attr.xmlns ~= feed.xmlns then |
| 153 | return; |
| 154 | end |
| 155 | |
| 156 | translate_entry[feed.type](feed, stanza); |
| 157 | end; |
| 158 | |
| 159 | error = function (feed, err, d) |
| 160 | error(err..": "..d); |
| 161 | end; |
| 162 | }; |
| 163 | |
| 164 | return new_stream(feed, callbacks); |
| 165 | end |
| 166 | |
| 167 | function feed_from_string(data) |
| 168 | local feed = {notopen = true}; |
| 169 | |
| 170 | local stream = new_feed_stream(feed); |
| 171 | stream:feed(data); |
| 172 | |
| 173 | return feed; |
| 174 | end |
| 175 | |
| 176 | function open(url) |
| 177 | if url:match("^file://") then |
| 178 | return open_file(url); |
| 179 | elseif url:match("^https?://") then |
| 180 | return open_http(url); |
| 181 | else |
| 182 | return false, "Could not understand URL: "..url; |
| 183 | end |
| 184 | end |
| 185 | |
| 186 | function open_file(filename) |
| 187 | local file, err = io.open((filename:gsub("^file://", ""))); |
| 188 | if not file then |
| 189 | return file, err; |
| 190 | end |
| 191 | |
| 192 | local feed = feed_from_string(file:read("*a")); |
| 193 | |
| 194 | file:close(); |
| 195 | |
| 196 | return feed; |
| 197 | end |
| 198 | |
| 199 | function open_http(url) |
| 200 | local data, err = http.request(url); |
| 201 | if not data then |
| 202 | return data, err; |
| 203 | end |
| 204 | |
| 205 | local feed = feed_from_string(data); |
| 206 | |
| 207 | return feed; |
| 208 | end |