1 |
|
%%%---------------------------------------------------------------------------- |
2 |
|
%%% @copyright (C) 2020, Erlang Solutions Ltd. |
3 |
|
%%% @doc |
4 |
|
%%% This module optimizes offline storage for chat markers in the next way: |
5 |
|
%%% |
6 |
|
%%% 1) It filters out chat marker packets processed by mod_smart_markers: |
7 |
|
%%% |
8 |
|
%%% * These packets can be identified by the extra permanent Acc |
9 |
|
%%% timestamp field added by mod_smart_markers. |
10 |
|
%%% |
11 |
|
%%% * These packets are not going to mod_offline (notice the |
12 |
|
%%% difference in priorities for the offline_message_hook handlers) |
13 |
|
%%% |
14 |
|
%%% * The information about these chat markers is stored in DB, |
15 |
|
%%% timestamp added by mod_smart_markers is important here! |
16 |
|
%%% |
17 |
|
%%% 2) After all the offline messages are inserted by mod_offline (notice |
18 |
|
%%% the difference in priorities for the resend_offline_messages_hook |
19 |
|
%%% handlers), this module adds the latest chat markers as the last |
20 |
|
%%% offline messages: |
21 |
|
%%% |
22 |
|
%%% * It extracts chat markers data stored for the user in the DB |
23 |
|
%%% (with timestamps) |
24 |
|
%%% |
25 |
|
%%% * Requests cached chat markers from mod_smart_markers that has |
26 |
|
%%% timestamp older or equal to the stored one. |
27 |
|
%%% |
28 |
|
%%% * Generates and inserts chat markers as the last offline messages |
29 |
|
%%% |
30 |
|
%%% @end |
31 |
|
%%%---------------------------------------------------------------------------- |
32 |
|
-module(mod_offline_chatmarkers). |
33 |
|
-xep([{xep, 160}, {version, "1.0"}]). |
34 |
|
-behaviour(gen_mod). |
35 |
|
-behaviour(mongoose_module_metrics). |
36 |
|
|
37 |
|
%% gen_mod handlers |
38 |
|
%% gen_mod API |
39 |
|
-export([start/2]). |
40 |
|
-export([stop/1]). |
41 |
|
-export([deps/2]). |
42 |
|
-export([supported_features/0]). |
43 |
|
-export([config_spec/0]). |
44 |
|
|
45 |
|
%% Hook handlers |
46 |
|
-export([inspect_packet/4, |
47 |
|
remove_user/3, |
48 |
|
pop_offline_messages/2]). |
49 |
|
|
50 |
|
-ignore_xref([ |
51 |
|
behaviour_info/1, inspect_packet/4, pop_offline_messages/2, remove_user/3 |
52 |
|
]). |
53 |
|
|
54 |
|
-include("jlib.hrl"). |
55 |
|
-include_lib("exml/include/exml.hrl"). |
56 |
|
-include("mongoose_config_spec.hrl"). |
57 |
|
|
58 |
|
%% gen_mod callbacks |
59 |
|
%% ------------------------------------------------------------------ |
60 |
|
|
61 |
|
-spec supported_features() -> [atom()]. |
62 |
|
supported_features() -> |
63 |
:-( |
[dynamic_domains]. |
64 |
|
|
65 |
|
-spec deps(mongooseim:host_type(), gen_mod:module_opts()) -> gen_mod_deps:deps(). |
66 |
|
deps(_, _)-> |
67 |
:-( |
[]. %% TODO: this need to be marked as required-to-be-configured |
68 |
|
% [{mod_smart_markers, [], hard}]. |
69 |
|
|
70 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
71 |
|
start(HostType, Opts) -> |
72 |
:-( |
mod_offline_chatmarkers_backend:init(HostType, Opts), |
73 |
:-( |
ejabberd_hooks:add(hooks(HostType)), |
74 |
:-( |
ok. |
75 |
|
|
76 |
|
-spec stop(mongooseim:host_type()) -> ok. |
77 |
|
stop(HostType) -> |
78 |
:-( |
ejabberd_hooks:delete(hooks(HostType)), |
79 |
:-( |
ok. |
80 |
|
|
81 |
|
hooks(HostType) -> |
82 |
:-( |
DefaultHooks = [ |
83 |
|
{offline_message_hook, HostType, ?MODULE, inspect_packet, 40}, |
84 |
|
{resend_offline_messages_hook, HostType, ?MODULE, pop_offline_messages, 60}, |
85 |
|
{remove_user, HostType, ?MODULE, remove_user, 50} |
86 |
|
], |
87 |
:-( |
case gen_mod:get_module_opt(HostType, ?MODULE, store_groupchat_messages) of |
88 |
|
true -> |
89 |
:-( |
GroupChatHook = {offline_groupchat_message_hook, |
90 |
|
HostType, ?MODULE, inspect_packet, 40}, |
91 |
:-( |
[GroupChatHook | DefaultHooks]; |
92 |
:-( |
_ -> DefaultHooks |
93 |
|
end. |
94 |
|
|
95 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
96 |
|
config_spec() -> |
97 |
166 |
#section{ |
98 |
|
items = #{<<"backend">> => #option{type = atom, |
99 |
|
validate = {module, ?MODULE}}, |
100 |
|
<<"store_groupchat_messages">> => #option{type = boolean} |
101 |
|
}, |
102 |
|
defaults = #{<<"store_groupchat_messages">> => false, |
103 |
|
<<"backend">> => rdbms |
104 |
|
}, |
105 |
|
format_items = map |
106 |
|
}. |
107 |
|
|
108 |
|
remove_user(Acc, User, Server) -> |
109 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
110 |
:-( |
mod_offline_chatmarkers_backend:remove_user(HostType, jid:make(User, Server, <<>>)), |
111 |
:-( |
Acc. |
112 |
|
|
113 |
|
pop_offline_messages(Acc, JID) -> |
114 |
:-( |
mongoose_acc:append(offline, messages, offline_chatmarkers(Acc, JID), Acc). |
115 |
|
|
116 |
|
inspect_packet(Acc, From, To, Packet) -> |
117 |
:-( |
case maybe_store_chat_marker(Acc, From, To, Packet) of |
118 |
|
true -> |
119 |
:-( |
{stop, mongoose_acc:set(offline, stored, true, Acc)}; |
120 |
|
false -> |
121 |
:-( |
Acc |
122 |
|
end. |
123 |
|
|
124 |
|
maybe_store_chat_marker(Acc, From, To, Packet) -> |
125 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
126 |
:-( |
case mongoose_acc:get(mod_smart_markers, timestamp, undefined, Acc) of |
127 |
:-( |
undefined -> false; |
128 |
|
Timestamp when is_integer(Timestamp) -> |
129 |
:-( |
Room = get_room(Acc, From), |
130 |
:-( |
Thread = get_thread(Packet), |
131 |
:-( |
mod_offline_chatmarkers_backend:maybe_store(HostType, To, Thread, Room, Timestamp), |
132 |
:-( |
true |
133 |
|
end. |
134 |
|
|
135 |
|
get_room(Acc, From) -> |
136 |
:-( |
case mongoose_acc:stanza_type(Acc) of |
137 |
:-( |
<<"groupchat">> -> From; |
138 |
:-( |
_ -> undefined |
139 |
|
end. |
140 |
|
|
141 |
|
get_thread(El) -> |
142 |
:-( |
case exml_query:path(El, [{element, <<"thread">>}, cdata]) of |
143 |
:-( |
Thread when Thread =/= <<>> -> Thread; |
144 |
:-( |
_ -> undefined |
145 |
|
end. |
146 |
|
|
147 |
|
offline_chatmarkers(Acc, JID) -> |
148 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
149 |
:-( |
{ok, Rows} = mod_offline_chatmarkers_backend:get(HostType, JID), |
150 |
:-( |
mod_offline_chatmarkers_backend:remove_user(HostType, JID), |
151 |
:-( |
lists:concat([process_row(Acc, JID, R) || R <- Rows]). |
152 |
|
|
153 |
|
process_row(Acc, Jid, {Thread, undefined, TS}) -> |
154 |
:-( |
ChatMarkers = mod_smart_markers:get_chat_markers(Jid, Thread, TS), |
155 |
:-( |
[build_one2one_chatmarker_msg(Acc, CM) || CM <- ChatMarkers]; |
156 |
|
process_row(Acc, Jid, {Thread, Room, TS}) -> |
157 |
:-( |
ChatMarkers = mod_smart_markers:get_chat_markers(Room, Thread, TS), |
158 |
:-( |
[build_room_chatmarker_msg(Acc, Jid, CM) || CM <- ChatMarkers]. |
159 |
|
|
160 |
|
build_one2one_chatmarker_msg(Acc, CM) -> |
161 |
:-( |
#{from := From, to := To, thread := Thread, |
162 |
|
type := Type, id := Id, timestamp := TS} = CM, |
163 |
:-( |
Children = thread(Thread) ++ marker(Type, Id), |
164 |
:-( |
Attributes = [{<<"from">>, jid:to_binary(From)}, |
165 |
|
{<<"to">>, jid:to_binary(To)}], |
166 |
:-( |
Packet = #xmlel{name = <<"message">>, attrs = Attributes, children = Children}, |
167 |
:-( |
make_route_item(Acc, From, To, TS, Packet). |
168 |
|
|
169 |
|
build_room_chatmarker_msg(Acc, To, CM) -> |
170 |
:-( |
#{from := FromUser, to := Room, thread := Thread, |
171 |
|
type := Type, id := Id, timestamp := TS} = CM, |
172 |
:-( |
FromUserBin = jid:to_binary(jid:to_lus(FromUser)), |
173 |
:-( |
From = jid:make(Room#jid.luser, Room#jid.lserver, FromUserBin), |
174 |
:-( |
FromBin = jid:to_binary(From), |
175 |
:-( |
Children = thread(Thread) ++ marker(Type, Id), |
176 |
:-( |
Attributes = [{<<"from">>, FromBin}, |
177 |
|
{<<"to">>, jid:to_binary(To)}, |
178 |
|
{<<"type">>, <<"groupchat">>}], |
179 |
:-( |
Packet = #xmlel{name = <<"message">>, attrs = Attributes, children = Children}, |
180 |
:-( |
make_route_item(Acc, From, To, TS, Packet). |
181 |
|
|
182 |
|
make_route_item(Acc, From, To, TS, Packet) -> |
183 |
:-( |
NewStanzaParams = #{element => Packet, from_jid => From, to_jid => To}, |
184 |
:-( |
Acc1 = mongoose_acc:update_stanza(NewStanzaParams, Acc), |
185 |
:-( |
Acc2 = mongoose_acc:set_permanent(mod_smart_markers, timestamp, TS, Acc1), |
186 |
:-( |
{route, From, To, Acc2}. |
187 |
|
|
188 |
|
marker(Type, Id) -> |
189 |
:-( |
[#xmlel{name = atom_to_binary(Type, latin1), |
190 |
|
attrs = [{<<"xmlns">>, <<"urn:xmpp:chat-markers:0">>}, |
191 |
|
{<<"id">>, Id}], children = []}]. |
192 |
|
|
193 |
:-( |
thread(undefined) -> []; |
194 |
|
thread(Thread) -> |
195 |
:-( |
[#xmlel{name = <<"thread">>, attrs = [], |
196 |
|
children = [#xmlcdata{content = Thread}]}]. |