Mercurial > prosody-modules
view mod_http_muc_log/mod_http_muc_log.lua @ 2662:2d5e0e2938d1
mod_mam: Remove extraneous assert when opening the store
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Sun, 02 Apr 2017 19:44:59 +0100 |
parents | b61b0ff1c0f9 |
children | cd5781ca782d |
line wrap: on
line source
local mt = require"util.multitable"; local datetime = require"util.datetime"; local jid_split = require"util.jid".split; local nodeprep = require"util.encodings".stringprep.nodeprep; local uuid = require"util.uuid".generate; local it = require"util.iterators"; local gettime = require"socket".gettime; local url = require"socket.url"; local os_time, os_date = os.time, os.date; local render = require"util.interpolation".new("%b{}", require"util.stanza".xml_escape); local archive = module:open_store("muc_log", "archive"); -- Support both old and new MUC code local mod_muc = module:depends"muc"; local rooms = rawget(mod_muc, "rooms"); local each_room = rawget(mod_muc, "each_room") or function() return it.values(rooms); end; local new_muc = not rooms; if new_muc then rooms = module:shared"muc/rooms"; end local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or function (jid) return rooms[jid]; end local function get_room(name) local jid = name .. '@' .. module.host; return get_room_from_jid(jid); end module:depends"http"; local template; do local template_file = module:get_option_string(module.name .. "_template", module.name .. ".html"); template_file = assert(module:load_resource(template_file)); template = template_file:read("*a"); template_file:close(); end -- local base_url = module:http_url() .. '/'; -- TODO: Generate links in a smart way local get_link do local link, path = { path = '/' }, { "", "", is_directory = true }; function get_link(room, date) path[1], path[2] = room, date; path.is_directory = not date; link.path = url.build_path(path); return url.build(link); end end local function public_room(room) -- : boolean if type(room) == "string" then room = get_room(room); end return (room and not (room.get_hidden or room.is_hidden)(room) and not (room.get_members_only or room.is_members_only)(room) and room._data.logging == true); end local function sort_Y(a,b) return a.year > b.year end local function sort_m(a,b) return a.n > b.n end -- Time zone hack? local t_diff = os_time(os_date("*t")) - os_time(os_date("!*t")); local function time(t) return os_time(t) + t_diff; end -- Fetch one item local function find_once(room, query, retval) if query then query.limit = 1; else query = { limit = 1 }; end local iter, err = archive:find(room, query); if not iter then return iter, err; end if retval then return select(retval, iter()); end return iter(); end -- Produce the calendar view local function years_page(event, path) local response = event.response; local room = nodeprep(path:match("^(.*)/$")); if not room or not public_room(room) then return end -- Collect each date that has messages -- convert it to a year / month / day tree local date_list = archive.dates and archive:dates(room); local dates = mt.new(); if date_list then for _, date in ipairs(date_list) do local when = datetime.parse(date.."T00:00:00Z"); local t = os_date("!*t", when); dates:set(t.year, t.month, t.day, when); end else -- Collect date the hard way module:log("debug", "Find all dates with messages"); local next_day; repeat local when = find_once(room, { start = next_day; }, 3); if not when then break; end local t = os_date("!*t", when); dates:set(t.year, t.month, t.day, when ); next_day = when + (86400 - (when % 86400)); until not next_day; end local years = {}; -- Wrangle Y/m/d tree into year / month / week / day tree for calendar view for current_year, months_t in pairs(dates.data) do local t = { year = current_year, month = 1, day = 1 }; local months = { }; local year = { year = current_year, months = months }; years[#years+1] = year; for current_month, days_t in pairs(months_t) do t.day = 1; t.month = current_month; local tmp = os_date("!*t", time(t)); local days = {}; local week = { days = days } local weeks = { week }; local month = { year = year.year, month = os_date("!%B", time(t)), n = current_month, weeks = weeks }; months[#months+1] = month; local current_day = 1; for _=1, (tmp.wday+5)%7 do days[current_day], current_day = {}, current_day+1; end for i = 1, 31 do t.day = i; tmp = os_date("!*t", time(t)); if tmp.month ~= current_month then break end if i > 1 and tmp.wday == 2 then days = {}; weeks[#weeks+1] = { days = days }; current_day = 1; end days[current_day], current_day = { wday = tmp.wday, day = i, href = days_t[i] and datetime.date(days_t[i]) }, current_day+1; end end table.sort(year, sort_m); end table.sort(years, sort_Y); -- Phew, all wrangled, all that's left is rendering it with the template response.headers.content_type = "text/html; charset=utf-8"; return render(template, { title = get_room(room):get_name(); jid = get_room(room).jid; years = years; links = { { href = "../", rel = "up", text = "Back to room list" }, }; }); end -- Produce the chat log view local function logs_page(event, path) local response = event.response; -- FIXME In the year, 252525, if MUC is still alive, -- if Prosody can survive... Enjoy this Y10k bug local room, date = path:match("^(.-)/(%d%d%d%d%-%d%d%-%d%d)$"); room = nodeprep(room); if not room then return years_page(event, path); end if not public_room(room) then return end local logs, i = {}, 1; local iter, err = archive:find(room, { ["start"] = datetime.parse(date.."T00:00:00Z"); ["end"] = datetime.parse(date.."T23:59:59Z"); }); if not iter then module:log("warn", "Could not search archive: %s", err or "no error"); return 500; end local first, last; for key, item, when in iter do local body = item:get_child_text("body"); local subject = item:get_child_text("subject"); local verb = nil; if subject then verb, body = "set the topic to", subject; elseif body and body:sub(1,4) == "/me " then verb, body = body:sub(5), nil; elseif item.name == "presence" then verb = item.attr.type == "unavailable" and "has left" or "has joined"; end if body or verb then logs[i], i = { key = key; datetime = datetime.datetime(when); time = datetime.time(when); verb = verb; body = body; nick = select(3, jid_split(item.attr.from)); st_name = item.name; st_type = item.attr.type; }, i + 1; end first = first or key; last = key; end if i == 1 then return end -- No items local next_when, prev_when = "", ""; local date_list = archive.dates and archive:dates(room); if date_list then for j = 1, #date_list do if date_list[j] == date then next_when = date_list[j+1] or ""; prev_when = date_list[j-1] or ""; break; end end else module:log("debug", "Find next date with messages"); next_when = find_once(room, { after = last }, 3); if next_when then next_when = datetime.date(next_when); module:log("debug", "Next message: %s", next_when); else next_when = ""; end module:log("debug", "Find prev date with messages"); prev_when = find_once(room, { before = first, reverse = true }, 3); if prev_when then prev_when = datetime.date(prev_when); module:log("debug", "Previous message: %s", prev_when); else prev_when = ""; end end response.headers.content_type = "text/html; charset=utf-8"; return render(template, { title = ("%s - %s"):format(get_room(room):get_name(), date); jid = get_room(room).jid; lines = logs; links = { { href = "./", rel = "up", text = "Back to calendar" }, { href = prev_when, rel = "prev", text = prev_when}, { href = next_when, rel = "next", text = next_when}, }; }); end local function list_rooms(event) local response = event.response; local room_list, i = {}, 1; for room in each_room() do if public_room(room) then room_list[i], i = { href = get_link(jid_split(room.jid), nil); name = room:get_name(); description = room:get_description(); }, i + 1; end end response.headers.content_type = "text/html; charset=utf-8"; return render(template, { title = module:get_option_string("name", "Prosody Chatrooms"); jid = module.host; rooms = room_list; }); end local cache = setmetatable({}, {__mode = 'v'}); local function with_cache(f) return function (event, path) local request, response = event.request, event.response; local ckey = path or ""; local cached = cache[ckey]; if cached then local etag = cached.etag; local if_none_match = request.headers.if_none_match; if etag == if_none_match then module:log("debug", "Client cache hit"); return 304; end module:log("debug", "Server cache hit"); response.headers.etag = etag; response.headers.content_type = "text/html; charset=utf-8"; return cached[1]; end local start = gettime(); local rendered = f(event, path); module:log("debug", "Rendering took %dms", math.floor( (gettime() - start) * 1000 + 0.5)); if type(rendered) == "string" then local etag = uuid(); cached = { rendered, etag = etag, date = datetime.date() }; response.headers.etag = etag; cache[ckey] = cached; end response.headers.content_type = "text/html; charset=utf-8"; return rendered; end end -- How is cache invalidation a hard problem? ;) module:hook("muc-broadcast-message", function (event) local room = event.room; local room_name = jid_split(room.jid); local today = datetime.date(); cache[get_link(room_name)] = nil; cache[get_link(room_name, today)] = nil; end); module:provides("http", { route = { ["GET /"] = list_rooms; ["GET /*"] = with_cache(logs_page); }; });