./ct_report/coverage/mod_offline_chatmarkers.COVER.html

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}]}].
Line Hits Source