feeds.lua

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