1 -- XEP-0313: Message Archive Management for Prosody MUC
2 -- Copyright (C) 2011-2017 Kim Alvefur
4 -- This file is MIT/X11 licensed.
6 if module
:get_host_type() ~= "component" then
7 module
:log("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module
.name
);
11 -- Note: Can't implement urn:xmpp:mam:2 because we catch messages after
12 -- they have already been broadcast, so they can no longer be modified.
14 local xmlns_mam
= "urn:xmpp:mam:1";
15 local xmlns_delay
= "urn:xmpp:delay";
16 local xmlns_forward
= "urn:xmpp:forward:0";
17 local xmlns_st_id
= "urn:xmpp:sid:0";
18 local xmlns_muc_user
= "http://jabber.org/protocol/muc#user";
19 local muc_form_enable
= "muc#roomconfig_enablearchiving"
21 local st
= require
"util.stanza";
22 local rsm
= require
"util.rsm";
23 local jid_bare
= require
"util.jid".bare
;
24 local jid_split
= require
"util.jid".split
;
25 local jid_prep
= require
"util.jid".prep
;
26 local dataform
= require
"util.dataforms".new
;
27 local it
= require
"util.iterators";
29 -- Support both old and new MUC code
30 local mod_muc
= module
:depends
"muc";
31 local rooms
= rawget(mod_muc
, "rooms");
33 module
:log("warn", "mod_mam_muc is compatible with Prosody up to 0.10.x, use mod_muc_mam with later versions");
34 module
:depends("muc_mam");
37 local each_room
= function() return it
.values(rooms
); end;
38 local get_room_from_jid
= function (jid
) return rooms
[jid
]; end
40 local is_stanza
= st
.is_stanza
;
41 local tostring = tostring;
42 local time_now
= os
.time
;
43 local m_min
= math
.min;
44 local timestamp
, timestamp_parse
= require
"util.datetime".datetime
, require
"util.datetime".parse
;
45 local default_max_items
, max_max_items
= 20, module
:get_option_number("max_archive_query_results", 50);
47 local default_history_length
= 20;
48 local max_history_length
= module
:get_option_number("max_history_messages", math
.huge
);
50 local function get_historylength(room
)
51 return math
.min(room
._data
.history_length
or default_history_length
, max_history_length
);
54 local log_all_rooms
= module
:get_option_boolean("muc_log_all_rooms", false);
55 local log_by_default
= module
:get_option_boolean("muc_log_by_default", true);
57 local archive_store
= "muc_log";
58 local archive
= module
:open_store(archive_store
, "archive");
60 if archive
.name
== "null" or not archive
.find
then
61 if not archive
.find
then
62 module
:log("error", "Attempt to open archive storage returned a driver without archive API support");
63 module
:log("error", "mod_%s does not support archiving",
64 archive
._provided_by
or archive
.name
and "storage_"..archive
.name
.."(?)" or "<unknown>");
66 module
:log("error", "Attempt to open archive storage returned null driver");
68 module
:log("info", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
72 local function archiving_enabled(room
)
76 local enabled
= room
._data
.archiving
;
77 if enabled
== nil then
78 return log_by_default
;
83 local send_history
, save_to_history
;
85 -- Override history methods for all rooms.
87 module
:hook("muc-room-created", function (event
)
88 local room
= event
.room
;
89 if archiving_enabled(room
) then
90 room
.send_history
= send_history
;
91 room
.save_to_history
= save_to_history
;
95 function module
.load()
96 for room
in each_room() do
97 if archiving_enabled(room
) then
98 room
.send_history
= send_history
;
99 room
.save_to_history
= save_to_history
;
103 function module
.unload()
104 for room
in each_room() do
105 if room
.send_history
== send_history
then
106 room
.send_history
= nil;
107 room
.save_to_history
= nil;
113 if not log_all_rooms
then
114 module
:hook("muc-config-form", function(event
)
115 local room
, form
= event
.room
, event
.form
;
118 name
= muc_form_enable
,
120 label
= "Enable archiving?",
121 value
= archiving_enabled(room
),
126 module
:hook("muc-config-submitted", function(event
)
127 local room
, fields
, changed
= event
.room
, event
.fields
, event
.changed
;
128 local new
= fields
[muc_form_enable
];
129 if new
~= room
._data
.archiving
then
130 room
._data
.archiving
= new
;
131 if type(changed
) == "table" then
132 changed
[muc_form_enable
] = true;
134 event
.changed
= true;
137 room
.send_history
= send_history
;
138 room
.save_to_history
= save_to_history
;
140 room
.send_history
= nil;
141 room
.save_to_history
= nil;
147 -- Note: We ignore the 'with' field as this is internally used for stanza types
148 local query_form
= dataform
{
149 { name
= "FORM_TYPE"; type = "hidden"; value
= xmlns_mam
; };
150 { name
= "with"; type = "jid-single"; };
151 { name
= "start"; type = "text-single" };
152 { name
= "end"; type = "text-single"; };
156 module
:hook("iq-get/bare/"..xmlns_mam
..":query", function(event
)
157 local origin
, stanza
= event
.origin
, event
.stanza
;
158 origin
.send(st
.reply(stanza
):add_child(query_form
:form()));
162 -- Handle archive queries
163 module
:hook("iq-set/bare/"..xmlns_mam
..":query", function(event
)
164 local origin
, stanza
= event
.origin
, event
.stanza
;
165 local room_jid
= stanza
.attr
.to
;
166 local room_node
= jid_split(room_jid
);
167 local orig_from
= stanza
.attr
.from
;
168 local query
= stanza
.tags
[1];
170 local room
= get_room_from_jid(room_jid
);
172 origin
.send(st
.error_reply(stanza
, "cancel", "item-not-found"))
175 local from
= jid_bare(orig_from
);
177 -- Banned or not a member of a members-only room?
178 local from_affiliation
= room
:get_affiliation(from
);
179 if from_affiliation
== "outcast" -- banned
180 or room
:get_members_only() and not from_affiliation
then -- members-only, not a member
181 origin
.send(st
.error_reply(stanza
, "auth", "forbidden"))
185 local qid
= query
.attr
.queryid
;
187 -- Search query parameters
189 local form
= query
:get_child("x", "jabber:x:data");
192 form
, err
= query_form
:data(form
);
194 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", select(2, next(err
))));
197 qstart
, qend
= form
["start"], form
["end"];
200 if qstart
or qend
then -- Validate timestamps
201 local vstart
, vend
= (qstart
and timestamp_parse(qstart
)), (qend
and timestamp_parse(qend
))
202 if (qstart
and not vstart
) or (qend
and not vend
) then
203 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid timestamp"))
206 qstart
, qend
= vstart
, vend
;
209 module
:log("debug", "Archive query id %s from %s until %s)",
211 qstart
and timestamp(qstart
) or "the dawn of time",
212 qend
and timestamp(qend
) or "now");
215 local qset
= rsm
.get(query
);
216 local qmax
= m_min(qset
and qset
.max or default_max_items
, max_max_items
);
217 local reverse
= qset
and qset
.before
or false;
219 local before
, after
= qset
and qset
.before
, qset
and qset
.after
;
220 if type(before
) ~= "string" then before
= nil; end
222 -- Load all the data!
223 local data
, err
= archive
:find(room_node
, {
224 start
= qstart
; ["end"] = qend
; -- Time range
226 before
= before
; after
= after
;
228 with
= "message<groupchat";
232 origin
.send(st
.error_reply(stanza
, "cancel", "internal-server-error"));
235 local total
= tonumber(err
);
237 local msg_reply_attr
= { to
= stanza
.attr
.from
, from
= stanza
.attr
.to
};
241 -- Wrap it in stuff and deliver
244 local complete
= "true";
245 for id
, item
, when
in data
do
251 local fwd_st
= st
.message(msg_reply_attr
)
252 :tag("result", { xmlns
= xmlns_mam
, queryid
= qid
, id
= id
})
253 :tag("forwarded", { xmlns
= xmlns_forward
})
254 :tag("delay", { xmlns
= xmlns_delay
, stamp
= timestamp(when
) }):up();
256 -- Strip <x> tag, containing the original senders JID, unless the room makes this public
257 if room
:get_whois() ~= "anyone" then
258 item
:maptags(function (tag)
259 if tag.name
== "x" and tag.attr
.xmlns
== xmlns_muc_user
then
265 if not is_stanza(item
) then
266 item
= st
.deserialize(item
);
268 item
.attr
.xmlns
= "jabber:client";
269 fwd_st
:add_child(item
);
271 if not first
then first
= id
; end
275 results
[count
] = fwd_st
;
282 for i
= #results
, 1, -1 do
283 origin
.send(results
[i
]);
285 first
, last
= last
, first
;
289 module
:log("debug", "Archive query %s completed", tostring(qid
));
291 origin
.send(st
.reply(stanza
)
292 :tag("fin", { xmlns
= xmlns_mam
, queryid
= qid
, complete
= complete
})
293 :add_child(rsm
.generate
{
294 first
= first
, last
= last
, count
= total
}));
298 module
:hook("muc-get-history", function (event
)
299 local room
= event
.room
;
300 if not archiving_enabled(room
) then return end
301 local room_jid
= room
.jid
;
302 local maxstanzas
= event
.maxstanzas
or math
.huge
;
303 local maxchars
= event
.maxchars
;
304 local since
= event
.since
;
307 -- Load all the data!
309 limit
= math
.min(maxstanzas
, get_historylength(room
));
312 with
= "message<groupchat";
314 local data
, err
= archive
:find(jid_split(room_jid
), query
);
317 module
:log("error", "Could not fetch history: %s", tostring(err
));
321 local history
, i
= {}, 1;
323 for id
, item
, when
in data
do
325 item
:tag("delay", { xmlns
= "urn:xmpp:delay", from
= room_jid
, stamp
= timestamp(when
) }):up(); -- XEP-0203
326 item
:tag("stanza-id", { xmlns
= xmlns_st_id
, by
= room_jid
, id
= id
}):up();
327 if room
:get_whois() ~= "anyone" then
328 item
:maptags(function (tag)
329 if tag.name
== "x" and tag.attr
.xmlns
== xmlns_muc_user
then
336 local chars
= #tostring(item
);
337 if maxchars
- chars
< 0 then
340 maxchars
= maxchars
- chars
;
342 history
[i
], i
= item
, i
+1;
343 -- module:log("debug", tostring(item));
345 function event
.next_stanza()
352 function send_history(self
, to
, stanza
)
353 local maxchars
, maxstanzas
, seconds
, since
;
354 local history_tag
= stanza
:find("{http://jabber.org/protocol/muc}x/history")
356 module
:log("debug", tostring(history_tag
));
357 local history_attr
= history_tag
.attr
;
358 maxchars
= tonumber(history_attr
.maxchars
);
359 maxstanzas
= tonumber(history_attr
.maxstanzas
);
360 seconds
= tonumber(history_attr
.seconds
);
361 since
= history_attr
.since
;
363 since
= timestamp_parse(since
);
366 since
= math
.max(os
.time() - seconds
, since
or 0);
372 to
= to
; -- `to` is required to calculate the character count for `maxchars`
373 maxchars
= maxchars
, maxstanzas
= maxstanzas
, since
= since
;
374 next_stanza
= function() end; -- events should define this iterator
377 for msg
in event
.next_stanza
, event
do
378 self
:_route_stanza(msg
);
383 function save_to_history(self
, stanza
)
384 local room_node
, room_host
= jid_split(self
.jid
);
386 -- Filter out <stanza-id> that claim to be from us
387 stanza
:maptags(function (tag)
388 if tag.name
== "stanza-id" and tag.attr
.xmlns
== xmlns_st_id
389 and jid_prep(tag.attr
.by
) == self
.jid
then
392 if tag.name
== "x" and tag.attr
.xmlns
== xmlns_muc_user
then
398 local stored_stanza
= stanza
;
400 if stanza
.name
== "message" and self
:get_whois() == "anyone" then
401 stored_stanza
= st
.clone(stanza
);
402 local actor
= jid_bare(self
._occupants
[stanza
.attr
.from
].jid
);
403 local affiliation
= self
:get_affiliation(actor
) or "none";
404 local role
= self
:get_role(actor
) or self
:get_default_role(affiliation
);
405 stored_stanza
:add_direct_child(st
.stanza("x", { xmlns
= xmlns_muc_user
})
406 :tag("item", { affiliation
= affiliation
; role
= role
; jid
= actor
}));
410 if not archiving_enabled(self
) then -- Don't log
411 module
:log("debug", "Not archiving %s", stanza
:top_tag());
414 module
:log("debug", "Archiving %s", stanza
:top_tag());
417 local with
= stanza
.name
418 if stanza
.attr
.type then
419 with
= with
.. "<" .. stanza
.attr
.type
422 archive
:append(room_node
, nil, stored_stanza
, time_now(), with
);
425 if not archive
.delete
then
426 module
:log("warn", "Storage driver %s does not support deletion", archive
._provided_by
);
427 module
:log("warn", "Archived message will persist after a room has been destroyed");
429 module
:hook("muc-room-destroyed", function(event
)
430 local room_node
= jid_split(event
.room
.jid
);
431 archive
:delete(room_node
);
435 -- And role/affiliation changes?
437 module
:add_feature(xmlns_mam
);
439 module
:hook("muc-disco#info", function(event
)
440 event
.reply
:tag("feature", {var
=xmlns_mam
}):up();