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 local xmlns_mam
= "urn:xmpp:mam:2";
12 local xmlns_delay
= "urn:xmpp:delay";
13 local xmlns_forward
= "urn:xmpp:forward:0";
14 local xmlns_st_id
= "urn:xmpp:sid:0";
15 local xmlns_muc_user
= "http://jabber.org/protocol/muc#user";
16 local muc_form_enable
= "muc#roomconfig_enablearchiving"
18 local st
= require
"util.stanza";
19 local rsm
= require
"util.rsm";
20 local jid_bare
= require
"util.jid".bare
;
21 local jid_split
= require
"util.jid".split
;
22 local jid_prep
= require
"util.jid".prep
;
23 local dataform
= require
"util.dataforms".new
;
25 local mod_muc
= module
:depends
"muc";
26 local get_room_from_jid
= mod_muc
.get_room_from_jid
;
28 local is_stanza
= st
.is_stanza
;
29 local tostring = tostring;
30 local time_now
= os
.time
;
31 local m_min
= math
.min;
32 local timestamp
, timestamp_parse
= require
"util.datetime".datetime
, require
"util.datetime".parse
;
33 local default_max_items
, max_max_items
= 20, module
:get_option_number("max_archive_query_results", 50);
35 local default_history_length
= 20;
36 local max_history_length
= module
:get_option_number("max_history_messages", math
.huge
);
38 local function get_historylength(room
)
39 return math
.min(room
._data
.history_length
or default_history_length
, max_history_length
);
42 local log_all_rooms
= module
:get_option_boolean("muc_log_all_rooms", false);
43 local log_by_default
= module
:get_option_boolean("muc_log_by_default", true);
45 local archive_store
= "muc_log";
46 local archive
= module
:open_store(archive_store
, "archive");
48 if archive
.name
== "null" or not archive
.find
then
49 if not archive
.find
then
50 module
:log("error", "Attempt to open archive storage returned a driver without archive API support");
51 module
:log("error", "mod_%s does not support archiving",
52 archive
._provided_by
or archive
.name
and "storage_"..archive
.name
.."(?)" or "<unknown>");
54 module
:log("error", "Attempt to open archive storage returned null driver");
56 module
:log("info", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
60 local function archiving_enabled(room
)
64 local enabled
= room
._data
.archiving
;
65 if enabled
== nil then
66 return log_by_default
;
71 if not log_all_rooms
then
72 module
:hook("muc-config-form", function(event
)
73 local room
, form
= event
.room
, event
.form
;
76 name
= muc_form_enable
,
78 label
= "Enable archiving?",
79 value
= archiving_enabled(room
),
84 module
:hook("muc-config-submitted/"..muc_form_enable
, function(event
)
85 event
.room
._data
.archiving
= event
.value
;
86 event
.status_codes
[event
.value
and "170" or "171"] = true;
90 -- Note: We ignore the 'with' field as this is internally used for stanza types
91 local query_form
= dataform
{
92 { name
= "FORM_TYPE"; type = "hidden"; value
= xmlns_mam
; };
93 { name
= "with"; type = "jid-single"; };
94 { name
= "start"; type = "text-single" };
95 { name
= "end"; type = "text-single"; };
99 module
:hook("iq-get/bare/"..xmlns_mam
..":query", function(event
)
100 local origin
, stanza
= event
.origin
, event
.stanza
;
101 origin
.send(st
.reply(stanza
):tag("query", { xmlns
= xmlns_mam
}):add_child(query_form
:form()));
105 -- Handle archive queries
106 module
:hook("iq-set/bare/"..xmlns_mam
..":query", function(event
)
107 local origin
, stanza
= event
.origin
, event
.stanza
;
108 local room_jid
= stanza
.attr
.to
;
109 local room_node
= jid_split(room_jid
);
110 local orig_from
= stanza
.attr
.from
;
111 local query
= stanza
.tags
[1];
113 local room
= get_room_from_jid(room_jid
);
115 origin
.send(st
.error_reply(stanza
, "cancel", "item-not-found"))
118 local from
= jid_bare(orig_from
);
120 -- Banned or not a member of a members-only room?
121 local from_affiliation
= room
:get_affiliation(from
);
122 if from_affiliation
== "outcast" -- banned
123 or room
:get_members_only() and not from_affiliation
then -- members-only, not a member
124 origin
.send(st
.error_reply(stanza
, "auth", "forbidden"))
128 local qid
= query
.attr
.queryid
;
130 -- Search query parameters
132 local form
= query
:get_child("x", "jabber:x:data");
135 form
, err
= query_form
:data(form
);
137 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", select(2, next(err
))));
140 qstart
, qend
= form
["start"], form
["end"];
143 if qstart
or qend
then -- Validate timestamps
144 local vstart
, vend
= (qstart
and timestamp_parse(qstart
)), (qend
and timestamp_parse(qend
))
145 if (qstart
and not vstart
) or (qend
and not vend
) then
146 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid timestamp"))
149 qstart
, qend
= vstart
, vend
;
152 module
:log("debug", "Archive query id %s from %s until %s)",
154 qstart
and timestamp(qstart
) or "the dawn of time",
155 qend
and timestamp(qend
) or "now");
158 local qset
= rsm
.get(query
);
159 local qmax
= m_min(qset
and qset
.max or default_max_items
, max_max_items
);
160 local reverse
= qset
and qset
.before
or false;
162 local before
, after
= qset
and qset
.before
, qset
and qset
.after
;
163 if type(before
) ~= "string" then before
= nil; end
165 -- Load all the data!
166 local data
, err
= archive
:find(room_node
, {
167 start
= qstart
; ["end"] = qend
; -- Time range
169 before
= before
; after
= after
;
171 with
= "message<groupchat";
175 origin
.send(st
.error_reply(stanza
, "cancel", "internal-server-error"));
178 local total
= tonumber(err
);
180 local msg_reply_attr
= { to
= stanza
.attr
.from
, from
= stanza
.attr
.to
};
184 -- Wrap it in stuff and deliver
187 local complete
= "true";
188 for id
, item
, when
in data
do
194 local fwd_st
= st
.message(msg_reply_attr
)
195 :tag("result", { xmlns
= xmlns_mam
, queryid
= qid
, id
= id
})
196 :tag("forwarded", { xmlns
= xmlns_forward
})
197 :tag("delay", { xmlns
= xmlns_delay
, stamp
= timestamp(when
) }):up();
199 -- Strip <x> tag, containing the original senders JID, unless the room makes this public
200 if room
:get_whois() ~= "anyone" then
201 item
:maptags(function (tag)
202 if tag.name
== "x" and tag.attr
.xmlns
== xmlns_muc_user
then
208 if not is_stanza(item
) then
209 item
= st
.deserialize(item
);
211 item
.attr
.xmlns
= "jabber:client";
212 fwd_st
:add_child(item
);
214 if not first
then first
= id
; end
218 results
[count
] = fwd_st
;
225 for i
= #results
, 1, -1 do
226 origin
.send(results
[i
]);
228 first
, last
= last
, first
;
232 module
:log("debug", "Archive query %s completed", tostring(qid
));
234 origin
.send(st
.reply(stanza
)
235 :tag("fin", { xmlns
= xmlns_mam
, queryid
= qid
, complete
= complete
})
236 :add_child(rsm
.generate
{
237 first
= first
, last
= last
, count
= total
}));
241 module
:hook("muc-get-history", function (event
)
242 local room
= event
.room
;
243 if not archiving_enabled(room
) then return end
244 local room_jid
= room
.jid
;
245 local maxstanzas
= event
.maxstanzas
;
246 local maxchars
= event
.maxchars
;
247 local since
= event
.since
;
250 if maxstanzas
== 0 or maxchars
== 0 then
251 return -- No history requested
254 if not maxstanzas
or maxstanzas
> get_historylength(room
) then
255 maxstanzas
= get_historylength(room
);
258 if room
._history
and #room
._history
>= maxstanzas
then
259 return -- It can deal with this itself
262 -- Load all the data!
267 with
= "message<groupchat";
269 local data
, err
= archive
:find(jid_split(room_jid
), query
);
272 module
:log("error", "Could not fetch history: %s", tostring(err
));
276 local history
, i
= {}, 1;
278 for id
, item
, when
in data
do
280 item
:tag("delay", { xmlns
= "urn:xmpp:delay", from
= room_jid
, stamp
= timestamp(when
) }):up(); -- XEP-0203
281 item
:tag("stanza-id", { xmlns
= xmlns_st_id
, by
= room_jid
, id
= id
}):up();
282 if room
:get_whois() ~= "anyone" then
283 item
:maptags(function (tag)
284 if tag.name
== "x" and tag.attr
.xmlns
== xmlns_muc_user
then
291 local chars
= #tostring(item
);
292 if maxchars
- chars
< 0 then
295 maxchars
= maxchars
- chars
;
297 history
[i
], i
= item
, i
+1;
298 -- module:log("debug", tostring(item));
300 function event
.next_stanza()
307 module
:hook("muc-broadcast-messages", function (event
)
308 local room
, stanza
= event
.room
, event
.stanza
;
310 -- Filter out <stanza-id> that claim to be from us
311 stanza
:maptags(function (tag)
312 if tag.name
== "stanza-id" and tag.attr
.xmlns
== xmlns_st_id
313 and jid_prep(tag.attr
.by
) == room
.jid
then
316 if tag.name
== "x" and tag.attr
.xmlns
== xmlns_muc_user
then
325 local function save_to_history(self
, stanza
)
326 local room_node
, room_host
= jid_split(self
.jid
);
328 local stored_stanza
= stanza
;
330 if stanza
.name
== "message" and self
:get_whois() == "anyone" then
331 stored_stanza
= st
.clone(stanza
);
332 local actor
= jid_bare(self
._occupants
[stanza
.attr
.from
].jid
);
333 local affiliation
= self
:get_affiliation(actor
) or "none";
334 local role
= self
:get_role(actor
) or self
:get_default_role(affiliation
);
335 stored_stanza
:add_direct_child(st
.stanza("x", { xmlns
= xmlns_muc_user
})
336 :tag("item", { affiliation
= affiliation
; role
= role
; jid
= actor
}));
340 if not archiving_enabled(self
) then return end -- Don't log
343 local with
= stanza
.name
344 if stanza
.attr
.type then
345 with
= with
.. "<" .. stanza
.attr
.type
348 local id
= archive
:append(room_node
, nil, stored_stanza
, time_now(), with
);
351 stanza
:add_direct_child(st
.stanza("stanza-id", { xmlns
= xmlns_st_id
, by
= self
.jid
, id
= id
}));
355 module
:hook("muc-add-history", function (event
)
356 local room
, stanza
= event
.room
, event
.stanza
;
357 save_to_history(room
, stanza
);
360 if module
:get_option_boolean("muc_log_presences", false) then
361 module
:hook("muc-occupant-joined", function (event
)
362 save_to_history(event
.room
, st
.stanza("presence", { from
= event
.nick
}):tag("x", { xmlns
= "http://jabber.org/protocol/muc" }));
364 module
:hook("muc-occupant-left", function (event
)
365 save_to_history(event
.room
, st
.stanza("presence", { type = "unavailable", from
= event
.nick
}));
369 if not archive
.delete
then
370 module
:log("warn", "Storage driver %s does not support deletion", archive
._provided_by
);
371 module
:log("warn", "Archived message will persist after a room has been destroyed");
373 module
:hook("muc-room-destroyed", function(event
)
374 local room_node
= jid_split(event
.room
.jid
);
375 archive
:delete(room_node
);
379 -- And role/affiliation changes?
381 module
:add_feature(xmlns_mam
);
383 module
:hook("muc-disco#info", function(event
)
384 event
.reply
:tag("feature", {var
=xmlns_mam
}):up();