Software /
code /
prosody-modules
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 |
parent | 1580:63571115302f |
child | 1582: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 }; |