comparison mod_http_muc_log/mod_http_muc_log.lua @ 1581:9f6cd252d233

mod_http_muc_log: Revamp template system
author Kim Alvefur <zash@zash.se>
date Tue, 02 Dec 2014 15:12:01 +0100
parents 63571115302f
children 8e282eb0c70c
comparison
equal deleted inserted replaced
1580:63571115302f 1581:9f6cd252d233
3 local jid_split = require"util.jid".split; 3 local jid_split = require"util.jid".split;
4 local nodeprep = require"util.encodings".stringprep.nodeprep; 4 local nodeprep = require"util.encodings".stringprep.nodeprep;
5 local uuid = require"util.uuid".generate; 5 local uuid = require"util.uuid".generate;
6 local it = require"util.iterators"; 6 local it = require"util.iterators";
7 local gettime = require"socket".gettime; 7 local gettime = require"socket".gettime;
8 local url = require"socket.url";
9 local xml_escape = st.xml_escape;
10 local t_concat = table.concat;
8 11
9 local archive = module:open_store("muc_log", "archive"); 12 local archive = module:open_store("muc_log", "archive");
10 13
11 -- Support both old and new MUC code 14 -- Support both old and new MUC code
12 local mod_muc = module:depends"muc"; 15 local mod_muc = module:depends"muc";
26 return get_room_from_jid(jid); 29 return get_room_from_jid(jid);
27 end 30 end
28 31
29 module:depends"http"; 32 module:depends"http";
30 33
31 local function template(data) 34 local function render(template, values)
32 --[[ DOC 35 --[[ DOC
33 Like util.template, but deals with plain text
34 Returns a closure that is called with a table of values
35 {name} is substituted for values["name"] and is XML escaped 36 {name} is substituted for values["name"] and is XML escaped
36 {name!} is substituted without XML escaping 37 {name!} is substituted without XML escaping
37 {name?} is optional and is replaced with an empty string if no value exists 38 {name?} is optional and is replaced with an empty string if no value exists
39 {name# sub-template } renders a sub-template using an array of values
38 ]] 40 ]]
39 return function(values) 41 return (template:gsub("%b{}", function (block)
40 return (data:gsub("{([^}]-)(%p?)}", function (name, opt) 42 local name, opt, e = block:sub(2, -2):match("([%a_][%w_]*)(%p?)()");
41 local value = values[name]; 43 local value = values[name];
42 if value then 44 if opt == '#' then
43 if opt ~= "!" then 45 if not value or not value[1] then return ""; end
44 return st.xml_escape(value); 46 local out, subtpl = {}, block:sub(e+1, -2);
45 end 47 for i=1, #value do
46 return value; 48 out[i] = render(subtpl, value[i]);
47 elseif opt == "?" then
48 return "";
49 end 49 end
50 end)); 50 return t_concat(out);
51 end 51 end
52 end 52 if value ~= nil then
53 53 if type(value) ~= "string" then
54 -- TODO Move templates into files 54 value = tostring(value);
55 local base = template(template[[ 55 end
56 if opt ~= '!' then
57 return xml_escape(value);
58 end
59 return value;
60 elseif opt == '?' then
61 return block:sub(e+1, -2);
62 end
63 end));
64 end
65
66 -- TODO Move template into a file
67 local template = [=[
56 <!DOCTYPE html> 68 <!DOCTYPE html>
57 <html> 69 <html>
58 <head> 70 <head>
59 <meta charset="utf-8"> 71 <meta charset="utf-8">
60 <meta name="viewport" content="width=device-width, initial-scale=1"> 72 <meta name="viewport" content="width=device-width, initial-scale=1">
61 <link rel="canonical" href="{canonical}">
62 <title>{title}</title> 73 <title>{title}</title>
63 <style> 74 <style>
75 :link,:visited{text-decoration:none;color:#2e3436;text-decoration:none;}
76 :link:hover,:visited:hover{color:#3465a4;}
64 body{background-color:#eeeeec;margin:1ex 0;padding-bottom:3em;font-family:Arial,Helvetica,sans-serif;} 77 body{background-color:#eeeeec;margin:1ex 0;padding-bottom:3em;font-family:Arial,Helvetica,sans-serif;}
65 header,footer{margin:1ex 1em;}
66 footer{font-size:smaller;color:#babdb6;}
67 .content{background-color:white;padding:1em;list-style-position:inside;}
68 nav{font-size:large;margin:1ex 1ex;clear:both;line-height:1.5em;}
69 nav a{padding: 1ex;text-decoration:none;}
70 nav a.up{font-size:smaller;}
71 nav a.prev{float:left;}
72 nav a.next{float:right;}
73 nav a.next::after{content:" →";}
74 nav a.prev::before{content:"← ";}
75 nav a:empty::after,nav a:empty::before{content:""}
76 @media screen and (min-width: 460px) {
77 nav{font-size:x-large;margin:1ex 1em;}
78 }
79 a:link,a:visited{color:#2e3436;text-decoration:none;}
80 a:link:hover,a:visited:hover{color:#3465a4;}
81 ul,ol{padding:0;} 78 ul,ol{padding:0;}
82 li{list-style:none;} 79 li{list-style:none;}
83 hr{visibility:hidden;clear:both;} 80 hr{visibility:hidden;clear:both;}
84 br{clear:both;} 81 br{clear:both;}
85 li time{float:right;font-size:small;opacity:0.2;} 82 header,footer{margin:1ex 1em;}
86 li:hover time{opacity:1;} 83 footer{font-size:smaller;color:#babdb6;}
87 .room-list .description{font-size:smaller;} 84 nav{font-size:large;margin:1ex 1ex;clear:both;line-height:1.5em;}
88 q.body::before,q.body::after{content:"";} 85 footer nav .up{display:none;}
86 @media screen and (min-width: 460px) {
87 nav {font-size:x-large;margin:1ex 1em;}
88 }
89 nav a{padding: 1ex;}
90 nav .up{font-size:smaller;display:block;clear:both;}
91 nav .prev{float:left;}
92 nav .next{float:right;}
93 nav .next::after{content:" →";}
94 nav .prev::before{content:"← ";}
95 nav :empty::after,nav :empty::before{content:""}
96 .content{background-color:white;padding:1em;list-style-position:inside;}
97 .time{float:right;font-size:small;opacity:0.2;}
98 li:hover .time{opacity:1;}
99 .description{font-size:smaller;}
100 .body{white-space:pre-line;}
101 .body::before,.body::after{content:"";}
89 .presence .verb{font-style:normal;color:#30c030;} 102 .presence .verb{font-style:normal;color:#30c030;}
90 .presence.unavailable .verb{color:#c03030;} 103 .unavailable .verb{color:#c03030;}
91 </style> 104 </style>
92 </head> 105 </head>
93 <body> 106 <body>
94 <header> 107 <header>
95 <h1>{title}</h1> 108 <h1 title="xmpp:{jid?}">{title}</h1>
96 {header!} 109 <nav>{links#
110 <a class="{rel?}" href="{href}" rel="{rel?}">{text}</a>}
111 </nav>
97 </header> 112 </header>
98 <hr> 113 <hr>
99 <div class="content"> 114 <div class="content">
100 {body!} 115 <nav>
116 <dl class="room-list">
117 {rooms#
118 <dt class="name"><a href="{href}">{name}</a></dt>
119 <dd class="description">{description?}</dd>}
120 </dl>
121 <ul class="dates">{dates#
122 <li><a href="{date}">{date}</a></li>}
123 </ul>
124 </nav>
125 <ol class="chat-logs">{lines#
126 <li class="{st_name} {st_type?}" id="{key}">
127 <a class="time" href="#{key}"><time datetime="{datetime}">{time}</time></a>
128 <b class="nick">{nick}</b>
129 <em class="verb">{verb?}</em>
130 <q class="body">{body?}</q>
131 </li>}
132 </ol>
101 </div> 133 </div>
102 <hr> 134 <hr>
103 <footer> 135 <footer>
104 {footer!} 136 <nav>{links#
137 <a class="{rel?}" href="{href}" rel="{rel?}">{text}</a>}
138 </nav>
105 <br> 139 <br>
106 <div class="powered-by">Prosody {prosody_version?}</div> 140 <div class="powered-by">Prosody</div>
107 </footer> 141 </footer>
108 </body>
109 </html>
110 ]] { prosody_version = prosody.version });
111
112 local dates_template = template(base{
113 title = "Logs for room {room}";
114 header = [[
115 <nav>
116 <a href=".." class="up">Back to room list</a>
117 </nav>
118 ]];
119 body = [[
120 <nav>
121 <ul class="dates">
122 {lines!}</ul>
123 </nav>
124 ]];
125 footer = "";
126 })
127
128 local date_line_template = template[[
129 <li><a href="{date}">{date}</a></li>
130 ]];
131
132 local page_template = template(base{
133 title = "Logs for room {room} on {date}";
134 header = [[
135 <nav>
136 <a class="up" href=".">Back to date list</a>
137 <br>
138 <a class="prev" href="{prev}">{prev}</a>
139 <a class="next" href="{next}">{next}</a>
140 </nav>
141 ]];
142 body = [[
143 <ol class="chat-logs">
144 {logs!}</ol>
145 ]];
146 footer = [[
147 <nav>
148 <div>
149 <a class="prev" href="{prev}">{prev}</a>
150 <a class="next" href="{next}">{next}</a>
151 </div>
152 </nav>
153 <script> 142 <script>
154 /* 143 /*
155 * Local timestamps 144 * Local timestamps
156 */ 145 */
157 (function () { 146 (function () {
165 tag.setAttribute("title", date.toString()); 154 tag.setAttribute("title", date.toString());
166 } 155 }
167 } 156 }
168 })(); 157 })();
169 </script> 158 </script>
170 ]]; 159 </body>
171 }); 160 </html>
172 161 ]=];
173 local line_template = template[[ 162
174 <li class="{st_name} {st_type?}" id="{key}"> 163 local base_url = module:http_url() .. '/';
175 <span class="time"> 164 local get_link do
176 <a href="#{key}"><time datetime="{datetime}">{time}</time></a> 165 local link, path = { path = '/' }, { "", "", is_directory = true };
177 </span> 166 function get_link(room, date)
178 <b class="nick">{nick}</b> 167 path[1], path[2] = room, date;
179 <em class="verb">{verb?}</em> 168 path.is_directory = not date;
180 <q class="body">{body?}</q> 169 link.path = url.build_path(path);
181 </li> 170 return url.build(link);
182 ]]; 171 end
183 172 end
184 local room_list_template = template(base{
185 title = "Rooms on {host}";
186 header = "";
187 body = [[
188 <nav>
189 <dl class="room-list">
190 {rooms!}
191 </dl>
192 </nav>
193 ]];
194 footer = "";
195 });
196
197 local room_item_template = template[[
198 <dt class="name"><a href="{room}/">{name}</a></dt>
199 <dd class="description">{description?}</dd>
200 ]];
201 173
202 local function public_room(room) 174 local function public_room(room)
203 if type(room) == "string" then 175 if type(room) == "string" then
204 room = get_room(room); 176 room = get_room(room);
205 end 177 end
227 }) 199 })
228 if not iter then break end 200 if not iter then break end
229 next_day = nil; 201 next_day = nil;
230 for key, message, when in iter do 202 for key, message, when in iter do
231 next_day = datetime.date(when); 203 next_day = datetime.date(when);
232 dates[i], i = date_line_template{ 204 dates[i], i = {
233 date = next_day; 205 date = next_day;
234 }, i + 1; 206 }, i + 1;
235 next_day = datetime.parse(next_day .. "T23:59:59Z") + 1; 207 next_day = datetime.parse(next_day .. "T23:59:59Z") + 1;
236 break; 208 break;
237 end 209 end
238 until not next_day; 210 until not next_day;
239 211
240 response.headers.content_type = "text/html; charset=utf-8"; 212 response.headers.content_type = "text/html; charset=utf-8";
241 return dates_template{ 213 return render(template, {
242 host = module.host; 214 title = get_room(room):get_name();
243 canonical = module:http_url() .. "/" .. path; 215 jid = get_room(room).jid;
244 room = room; 216 dates = dates;
245 lines = table.concat(dates); 217 links = {
246 }; 218 { href = "../", rel = "up", text = "Back to room list" },
219 };
220 });
247 end 221 end
248 222
249 local function logs_page(event, path) 223 local function logs_page(event, path)
250 local request, response = event.request, event.response; 224 local request, response = event.request, event.response;
251 225
276 verb, body = body:sub(5), nil; 250 verb, body = body:sub(5), nil;
277 elseif item.name == "presence" then 251 elseif item.name == "presence" then
278 verb = item.attr.type == "unavailable" and "has left" or "has joined"; 252 verb = item.attr.type == "unavailable" and "has left" or "has joined";
279 end 253 end
280 if body or verb then 254 if body or verb then
281 logs[i], i = line_template { 255 logs[i], i = {
282 key = key; 256 key = key;
283 datetime = datetime.datetime(when); 257 datetime = datetime.datetime(when);
284 time = datetime.time(when); 258 time = datetime.time(when);
285 verb = verb; 259 verb = verb;
286 body = body; 260 body = body;
315 prev_when = datetime.date(when); 289 prev_when = datetime.date(when);
316 module:log("debug", "Previous message: %s", datetime.datetime(when)); 290 module:log("debug", "Previous message: %s", datetime.datetime(when));
317 end 291 end
318 292
319 response.headers.content_type = "text/html; charset=utf-8"; 293 response.headers.content_type = "text/html; charset=utf-8";
320 return page_template{ 294 return render(template, {
321 canonical = module:http_url() .. "/" .. path; 295 title = ("%s - %s"):format(get_room(room):get_name(), date);
322 host = module.host; 296 jid = get_room(room).jid;
323 room = room; 297 lines = logs;
324 date = date; 298 links = {
325 logs = table.concat(logs); 299 { href = "./", rel = "up", text = "Back to date list" },
326 next = next_when; 300 { href = prev_when, rel = "prev", text = prev_when},
327 prev = prev_when; 301 { href = next_when, rel = "next", text = next_when},
328 }; 302 };
303 });
329 end 304 end
330 305
331 local function list_rooms(event) 306 local function list_rooms(event)
307 local request, response = event.request, event.response;
332 local room_list, i = {}, 1; 308 local room_list, i = {}, 1;
333 for room in each_room() do 309 for room in each_room() do
334 if public_room(room) then 310 if public_room(room) then
335 room_list[i], i = room_item_template { 311 room_list[i], i = {
336 room = jid_split(room.jid); 312 href = get_link(jid_split(room.jid), nil);
337 name = room:get_name(); 313 name = room:get_name();
338 description = room:get_description(); 314 description = room:get_description();
339 }, i + 1; 315 }, i + 1;
340 end 316 end
341 end 317 end
342 318
343 event.response.headers.content_type = "text/html; charset=utf-8"; 319 response.headers.content_type = "text/html; charset=utf-8";
344 return room_list_template { 320 return render(template, {
345 host = module.host; 321 title = module:get_option_string("name", "Prosody Chatrooms");
346 canonical = module:http_url() .. "/"; 322 jid = module.host;
347 rooms = table.concat(room_list); 323 rooms = room_list;
348 }; 324 });
349 end 325 end
350 326
351 local cache = setmetatable({}, {__mode = 'v'}); 327 local cache = setmetatable({}, {__mode = 'v'});
352 328
353 local function with_cache(f) 329 local function with_cache(f)
388 -- How is cache invalidation a hard problem? ;) 364 -- How is cache invalidation a hard problem? ;)
389 module:hook("muc-broadcast-message", function (event) 365 module:hook("muc-broadcast-message", function (event)
390 local room = event.room; 366 local room = event.room;
391 local room_name = jid_split(room.jid); 367 local room_name = jid_split(room.jid);
392 local today = datetime.date(); 368 local today = datetime.date();
393 cache[ room_name .. "/" .. today ] = nil; 369 cache[get_link(room_name)] = nil;
394 if cache[room_name] and cache[room_name].date ~= today then 370 cache[get_link(room_name, today)] = nil;
395 cache[room_name] = nil;
396 end
397 end); 371 end);
398 372
399 module:log("info", module:http_url());
400 module:provides("http", { 373 module:provides("http", {
401 route = { 374 route = {
402 ["GET /"] = list_rooms; 375 ["GET /"] = list_rooms;
403 ["GET /*"] = with_cache(logs_page); 376 ["GET /*"] = with_cache(logs_page);
404 }; 377 };