aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/mod_mam/mod_mam.lua
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/mod_mam/mod_mam.lua')
-rw-r--r--plugins/mod_mam/mod_mam.lua151
1 files changed, 123 insertions, 28 deletions
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index e7d89a95..4fe714eb 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -25,6 +25,7 @@ local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local jid_prepped_split = require "util.jid".prepped_split;
local dataform = require "util.dataforms".new;
+local get_form_type = require "util.dataforms".get_type;
local host = module.host;
local rm_load_roster = require "core.rostermanager".load_roster;
@@ -40,6 +41,11 @@ local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://ja
local archive_store = module:get_option_string("archive_store", "archive");
local archive = module:open_store(archive_store, "archive");
+local cleanup_after = module:get_option_string("archive_expires_after", "1w");
+local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
+local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000);
+local archive_truncate = math.floor(archive_item_limit * 0.99);
+
if not archive.find then
error("mod_"..(archive._provided_by or archive.name and "storage_"..archive.name).." does not support archiving\n"
.."See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
@@ -98,7 +104,14 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
local qwith, qstart, qend;
local form = query:get_child("x", "jabber:x:data");
if form then
- local err;
+ local form_type, err = get_form_type(form);
+ if not form_type then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
+ return true;
+ elseif form_type ~= xmlns_mam then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_mam.."'"));
+ return true;
+ end
form, err = query_form:data(form);
if err then
origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
@@ -117,10 +130,12 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
qstart, qend = vstart, vend;
end
- module:log("debug", "Archive query, id %s with %s from %s until %s",
- tostring(qid), qwith or "anyone",
- qstart and timestamp(qstart) or "the dawn of time",
- qend and timestamp(qend) or "now");
+ module:log("debug", "Archive query by %s id=%s with=%s when=%s...%s",
+ origin.username,
+ qid or stanza.attr.id,
+ qwith or "*",
+ qstart and timestamp(qstart) or "",
+ qend and timestamp(qend) or "");
-- RSM stuff
local qset = rsm.get(query);
@@ -128,6 +143,9 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
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
+ if qset then
+ module:log("debug", "Archive query id=%s rsm=%q", qid or stanza.attr.id, qset);
+ end
-- Load all the data!
local data, err = archive:find(origin.username, {
@@ -140,7 +158,12 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
});
if not data then
- origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+ module:log("debug", "Archive query id=%s failed: %s", qid or stanza.attr.id, err);
+ if err == "item-not-found" then
+ origin.send(st.error_reply(stanza, "modify", "item-not-found"));
+ else
+ origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
+ end
return true;
end
local total = tonumber(err);
@@ -189,13 +212,13 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
first, last = last, first;
end
- -- That's all folks!
- module:log("debug", "Archive query %s completed", tostring(qid));
-
origin.send(st.reply(stanza)
:tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
:add_child(rsm.generate {
first = first, last = last, count = total }));
+
+ -- That's all folks!
+ module:log("debug", "Archive query id=%s completed, %d items returned", qid or stanza.attr.id, complete and count or count - 1);
return true;
end);
@@ -213,13 +236,13 @@ local function shall_store(user, who)
end
local prefs = get_prefs(user);
local rule = prefs[who];
- module:log("debug", "%s's rule for %s is %s", user, who, tostring(rule));
+ module:log("debug", "%s's rule for %s is %s", user, who, rule);
if rule ~= nil then
return rule;
end
-- Below could be done by a metatable
local default = prefs[false];
- module:log("debug", "%s's default rule is %s", user, tostring(default));
+ module:log("debug", "%s's default rule is %s", user, default);
if default == "roster" then
return has_in_roster(user, who);
end
@@ -242,11 +265,70 @@ local function strip_stanza_id(stanza, user)
return stanza;
end
+local function should_store(stanza, c2s) --> boolean, reason: string
+ local st_type = stanza.attr.type or "normal";
+ -- FIXME pass direction of stanza and use that along with bare/full JID addressing
+ -- for more accurate MUC / type=groupchat check
+
+ if st_type == "headline" then
+ -- Headline messages are ephemeral by definition
+ return false, "headline";
+ end
+ if st_type == "error" and not c2s then
+ -- Store delivery failure notifications so you know if your own messages were not delivered
+ return true, "bounce";
+ end
+ if st_type == "groupchat" then
+ -- MUC messages always go to the full JID, usually archived by the MUC
+ return false, "groupchat";
+ end
+ if stanza:get_child("no-store", "urn:xmpp:hints")
+ or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then
+ -- XXX Experimental XEP
+ return false, "hint";
+ end
+ if stanza:get_child("store", "urn:xmpp:hints") then
+ return true, "hint";
+ end
+ if stanza:get_child("body") then
+ return true, "body";
+ end
+ if stanza:get_child("subject") then
+ -- XXX Who would send a message with a subject but without a body?
+ return true, "subject";
+ end
+ if stanza:get_child("encryption", "urn:xmpp:eme:0") then
+ -- Since we can't know what an encrypted message contains, we assume it's important
+ -- XXX Experimental XEP
+ return true, "encrypted";
+ end
+ if stanza:get_child(nil, "urn:xmpp:receipts") then
+ -- If it's important enough to ask for a receipt then it's important enough to archive
+ -- and the same applies to the receipt
+ return true, "receipt";
+ end
+ if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then
+ -- XXX Experimental XEP
+ return true, "marker";
+ end
+ if stanza:get_child("x", "jabber:x:conference")
+ or stanza:find("{http://jabber.org/protocol/muc#user}x/invite") then
+ return true, "invite";
+ end
+ if stanza:get_child(nil, "urn:xmpp:jingle-message:0") then
+ -- XXX Experimental XEP stuck in Proposed for almost a year at the time of this comment
+ return true, "jingle call";
+ end
+
+ -- The IM-NG thing to do here would be to return `not st_to_full`
+ -- One day ...
+ return false, "default";
+end
+
-- Handle messages
local function message_handler(event, c2s)
local origin, stanza = event.origin, event.stanza;
local log = c2s and origin.log or module._log;
- local orig_type = stanza.attr.type or "normal";
local orig_from = stanza.attr.from;
local orig_to = stanza.attr.to or orig_from;
-- Stanza without 'to' are treated as if it was to their own bare jid
@@ -259,21 +341,12 @@ local function message_handler(event, c2s)
-- Filter out <stanza-id> that claim to be from us
event.stanza = strip_stanza_id(stanza, store_user);
- -- We store chat messages or normal messages that have a body
- if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then
- log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
+ local should, why = should_store(stanza, c2s);
+ if not should then
+ log("debug", "Not archiving stanza: %s (%s)", stanza:top_tag(), why);
return;
end
- -- or if hints suggest we shouldn't
- if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
- if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
- or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
- log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
- return;
- end
- end
-
local clone_for_storage;
if not strip_tags:empty() then
clone_for_storage = st.clone(stanza);
@@ -294,10 +367,31 @@ local function message_handler(event, c2s)
-- Check with the users preferences
if shall_store(store_user, with) then
- log("debug", "Archiving stanza: %s", stanza:top_tag());
+ log("debug", "Archiving stanza: %s (%s)", stanza:top_tag(), why);
-- And stash it
- local ok, err = archive:append(store_user, nil, clone_for_storage, time_now(), with);
+ local time = time_now();
+ local ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
+ if not ok and err == "quota-limit" then
+ if type(cleanup_after) == "number" then
+ module:log("debug", "User '%s' over quota, cleaning archive", store_user);
+ local cleaned = archive:delete(store_user, {
+ ["end"] = (os.time() - cleanup_after);
+ });
+ if cleaned then
+ ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
+ end
+ end
+ if not ok and (archive.caps and archive.caps.truncate) then
+ module:log("debug", "User '%s' over quota, truncating archive", store_user);
+ local truncated = archive:delete(store_user, {
+ truncate = archive_truncate;
+ });
+ if truncated then
+ ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
+ end
+ end
+ end
if ok then
local clone_for_other_handlers = st.clone(stanza);
local id = ok;
@@ -325,8 +419,6 @@ end
module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1);
module:hook("pre-message/full", strip_stanza_id_after_other_events, -1);
-local cleanup_after = module:get_option_string("archive_expires_after", "1w");
-local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
if cleanup_after ~= "never" then
local cleanup_storage = module:open_store("archive_cleanup");
local cleanup_map = module:open_store("archive_cleanup", "map");
@@ -361,9 +453,11 @@ if cleanup_after ~= "never" then
last_date:set(username, date);
end
end
+ local cleanup_time = module:measure("cleanup", "times");
local async = require "util.async";
cleanup_runner = async.runner(function ()
+ local cleanup_done = cleanup_time();
local users = {};
local cut_off = datestamp(os.time() - cleanup_after);
for date in cleanup_storage:users() do
@@ -397,6 +491,7 @@ if cleanup_after ~= "never" then
wait();
end
module:log("info", "Deleted %d expired messages for %d users", sum, num_users);
+ cleanup_done();
end);
cleanup_task = module:add_timer(1, function ()