Software /
code /
prosody-modules
File
mod_mam_archive/mod_mam_archive.lua @ 5173:460f78654864
mod_muc_rtbl: also filter messages
This was a bit tricky because we don't want to run the JIDs
through SHA256 on each message. Took a while to come up with this
simple plan of just caching the SHA256 of the JIDs on the
occupants.
This will leave some dirt in the occupants after unloading the
module, but that should be ok; once they cycle the room, the
hashes will be gone.
This is direly needed, otherwise, there is a tight race between
the moderation activities and the actors joining the room.
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Tue, 21 Feb 2023 21:37:27 +0100 |
parent | 2706:3e97dae28215 |
child | 5926:8ff308fad9fd |
line wrap: on
line source
-- Prosody IM -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local get_prefs = module:require"mod_mam/mamprefs".get; local set_prefs = module:require"mod_mam/mamprefs".set; local rsm = require "util.rsm"; local jid_bare = require "util.jid".bare; local jid_prep = require "util.jid".prep; local date_parse = require "util.datetime".parse; local date_format = require "util.datetime".datetime; local st = require "util.stanza"; local archive_store = "archive2"; local archive = module:open_store(archive_store, "archive"); local global_default_policy = module:get_option("default_archive_policy", false); local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); local conversation_interval = tonumber(module:get_option_number("archive_conversation_interval", 86400)); local resolve_relative_path = require "core.configmanager".resolve_relative_path; -- Feature discovery local xmlns_archive = "urn:xmpp:archive" local feature_archive = st.stanza("feature", {xmlns=xmlns_archive}):tag("optional"); if(global_default_policy) then feature_archive:tag("default"); end module:add_extension(feature_archive); module:add_feature("urn:xmpp:archive:auto"); module:add_feature("urn:xmpp:archive:manage"); module:add_feature("urn:xmpp:archive:pref"); module:add_feature("http://jabber.org/protocol/rsm"); -- -------------------------------------------------- local function prefs_to_stanza(prefs) local prefstanza = st.stanza("pref", { xmlns="urn:xmpp:archive" }); local default = prefs[false] ~= nil and prefs[false] or global_default_policy; prefstanza:tag("default", {otr="oppose", save=default and "true" or "false"}):up(); prefstanza:tag("method", {type="auto", use="concede"}):up(); prefstanza:tag("method", {type="local", use="concede"}):up(); prefstanza:tag("method", {type="manual", use="concede"}):up(); for jid, choice in pairs(prefs) do if jid then prefstanza:tag("item", {jid=jid, otr="prefer", save=choice and "message" or "false" }):up() end end return prefstanza; end local function prefs_from_stanza(stanza, username) local current_prefs = get_prefs(username); -- "default" | "item" | "session" | "method" for elem in stanza:children() do if elem.name == "default" then current_prefs[false] = elem.attr["save"] == "true"; elseif elem.name == "item" then current_prefs[elem.attr["jid"]] = not elem.attr["save"] == "false"; elseif elem.name == "session" then module:log("info", "element is not supported: " .. tostring(elem)); -- local found = false; -- for child in data:children() do -- if child.name == elem.name and child.attr["thread"] == elem.attr["thread"] then -- for k, v in pairs(elem.attr) do -- child.attr[k] = v; -- end -- found = true; -- break; -- end -- end -- if not found then -- data:tag(elem.name, elem.attr):up(); -- end elseif elem.name == "method" then module:log("info", "element is not supported: " .. tostring(elem)); -- local newpref = stanza.tags[1]; -- iq:pref -- for _, e in ipairs(newpref.tags) do -- -- if e.name ~= "method" then continue end -- local found = false; -- for child in data:children() do -- if child.name == "method" and child.attr["type"] == e.attr["type"] then -- child.attr["use"] = e.attr["use"]; -- found = true; -- break; -- end -- end -- if not found then -- data:tag(e.name, e.attr):up(); -- end -- end end end end ------------------------------------------------------------ -- Preferences ------------------------------------------------------------ local function preferences_handler(event) local origin, stanza = event.origin, event.stanza; local user = origin.username; local reply = st.reply(stanza); if stanza.attr.type == "get" then reply:add_child(prefs_to_stanza(get_prefs(user))); end if stanza.attr.type == "set" then local new_prefs = stanza:get_child("pref", xmlns_archive); if not new_prefs then return false; end local prefs = prefs_from_stanza(stanza, origin.username); local ok, err = set_prefs(user, prefs); if not ok then return origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err))); end end return origin.send(reply); end local function auto_handler(event) local origin, stanza = event.origin, event.stanza; if not stanza.attr["type"] == "set" then return false; end local user = origin.username; local prefs = get_prefs(user); local auto = stanza:get_child("auto", xmlns_archive); prefs[false] = auto.attr["save"] ~= nil and auto.attr["save"] == "true" or false; set_prefs(user, prefs); return origin.send(st.reply(stanza)); end -- excerpt from mod_storage_sql2 local function get_db() local mod_sql = module:require("sql"); local params = module:get_option("sql"); local engine; params = params or { driver = "SQLite3" }; if params.driver == "SQLite3" then params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); end assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); engine = mod_sql:create_engine(params); engine:set_encoding(); return engine; end ------------------------------------------------------------ -- Collections. In our case there is one conversation with each contact for the whole day for simplicity ------------------------------------------------------------ local function list_stanza_to_query(origin, list_el) local sql = "SELECT `with`, `when` / ".. conversation_interval .." as `day`, COUNT(0) FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=? "; local args = {origin.host, origin.username, archive_store}; local with = list_el.attr["with"]; if with ~= nil then sql = sql .. "AND `with` = ? "; table.insert(args, jid_bare(with)); end local after = list_el.attr["start"]; if after ~= nil then sql = sql .. "AND `when` >= ? "; table.insert(args, date_parse(after)); end local before = list_el.attr["end"]; if before ~= nil then sql = sql .. "AND `when` <= ? "; table.insert(args, date_parse(before)); end sql = sql .. "GROUP BY `with`, `when` / ".. conversation_interval .." ORDER BY `when` / ".. conversation_interval .." ASC "; local qset = rsm.get(list_el); local limit = math.min(qset and qset.max or default_max_items, max_max_items); sql = sql.."LIMIT ?"; table.insert(args, limit); table.insert(args, 1, sql); return args; end local function list_handler(event) local db = get_db(); local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza); local query = list_stanza_to_query(origin, stanza.tags[1]); local list = reply:tag("list", {xmlns=xmlns_archive}); for row in db:select(unpack(query)) do list:tag("chat", { xmlns=xmlns_archive, with=row[1], start=date_format(row[2] * conversation_interval), version=row[3] }):up(); end origin.send(reply); return true; end ------------------------------------------------------------ -- Message archive retrieval ------------------------------------------------------------ local function retrieve_handler(event) local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza); local retrieve = stanza:get_child("retrieve", xmlns_archive); local qwith = retrieve.attr["with"]; local qstart = retrieve.attr["start"]; module:log("debug", "Archive query, with %s from %s)", qwith or "anyone", qstart or "the dawn of time"); if qstart then -- Validate timestamps local vstart = (qstart and date_parse(qstart)); if (qstart and not vstart) then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp")) return true end qstart = vstart; end if qwith then -- Validate the "with" jid local pwith = qwith and jid_prep(qwith); if pwith and not qwith then -- it failed prepping origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid JID")) return true end qwith = jid_bare(pwith); end -- RSM stuff local qset = rsm.get(retrieve); local qmax = math.min(qset and qset.max or default_max_items, max_max_items); local reverse = qset and qset.before or false; local before, after = qset and qset.before, qset and qset.after; if type(before) ~= "string" then before = nil; end -- Load all the data! local data, err = archive:find(origin.username, { start = qstart; ["end"] = qstart + conversation_interval; with = qwith; limit = qmax; before = before; after = after; reverse = reverse; total = true; }); if not data then return origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err)); end local count = err; local chat = reply:tag("chat", {xmlns=xmlns_archive, with=qwith, start=date_format(qstart), version=count}); local first, last; module:log("debug", "Count "..count); for id, item, when in data do if not getmetatable(item) == st.stanza_mt then item = st.deserialize(item); end module:log("debug", tostring(item)); local tag = jid_bare(item.attr["from"]) == jid_bare(origin.full_jid) and "to" or "from"; tag = chat:tag(tag, {secs = when - qstart}); tag:add_child(item:get_child("body")):up(); if not first then first = id; end last = id; end reply:add_child(rsm.generate{ first = first, last = last, count = count }) origin.send(reply); return true; end local function not_implemented(event) local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza):tag("error", {type="cancel"}); reply:tag("feature-not-implemented", {xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); origin.send(reply); end -- Preferences module:hook("iq/self/urn:xmpp:archive:pref", preferences_handler); module:hook("iq/self/urn:xmpp:archive:auto", auto_handler); module:hook("iq/self/urn:xmpp:archive:itemremove", not_implemented); module:hook("iq/self/urn:xmpp:archive:sessionremove", not_implemented); -- Message Archive Management module:hook("iq/self/urn:xmpp:archive:list", list_handler); module:hook("iq/self/urn:xmpp:archive:retrieve", retrieve_handler); module:hook("iq/self/urn:xmpp:archive:remove", not_implemented); -- manual archiving module:hook("iq/self/urn:xmpp:archive:save", not_implemented); -- replication module:hook("iq/self/urn:xmpp:archive:modified", not_implemented);