./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.1"}]).
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([hooks/1]).
42 -export([deps/2]).
43 -export([supported_features/0]).
44 -export([config_spec/0]).
45
46 %% Hook handlers
47 -export([inspect_packet/3,
48 remove_user/3,
49 pop_offline_messages/3]).
50
51 -include("jlib.hrl").
52 -include_lib("exml/include/exml.hrl").
53 -include("mongoose_config_spec.hrl").
54
55 %% gen_mod callbacks
56 %% ------------------------------------------------------------------
57
58 -spec supported_features() -> [atom()].
59 supported_features() ->
60 1 [dynamic_domains].
61
62 -spec deps(mongooseim:host_type(), gen_mod:module_opts()) -> gen_mod_deps:deps().
63 deps(_, _)->
64 5 []. %% TODO: this need to be marked as required-to-be-configured
65 % [{mod_smart_markers, [], hard}].
66
67 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
68 start(HostType, Opts) ->
69 1 mod_offline_chatmarkers_backend:init(HostType, Opts),
70 1 ok.
71
72 -spec stop(mongooseim:host_type()) -> ok.
73 stop(_HostType) ->
74 1 ok.
75
76 -spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
77 hooks(HostType) ->
78 2 DefaultHooks = [
79 {offline_message_hook, HostType, fun ?MODULE:inspect_packet/3, #{}, 40},
80 {resend_offline_messages_hook, HostType, fun ?MODULE:pop_offline_messages/3, #{}, 60},
81 {remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50}
82 ],
83 2 case gen_mod:get_module_opt(HostType, ?MODULE, store_groupchat_messages) of
84 true ->
85 2 GroupChatHook = {offline_groupchat_message_hook,
86 HostType, fun ?MODULE:inspect_packet/3, #{}, 40},
87 2 [GroupChatHook | DefaultHooks];
88
:-(
_ -> DefaultHooks
89 end.
90
91 -spec config_spec() -> mongoose_config_spec:config_section().
92 config_spec() ->
93 186 #section{
94 items = #{<<"backend">> => #option{type = atom,
95 validate = {module, ?MODULE}},
96 <<"store_groupchat_messages">> => #option{type = boolean}
97 },
98 defaults = #{<<"store_groupchat_messages">> => false,
99 <<"backend">> => rdbms
100 }
101 }.
102
103 -spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
104 Acc :: mongoose_acc:t(),
105 Params :: map(),
106 Extra :: gen_hook:extra().
107 remove_user(Acc, #{jid := #jid{luser = User, lserver = Server}}, #{host_type := HostType}) ->
108
:-(
mod_offline_chatmarkers_backend:remove_user(HostType, jid:make_bare(User, Server)),
109
:-(
{ok, Acc}.
110
111 -spec pop_offline_messages(Acc, Params, Extra) -> {ok, Acc} when
112 Acc :: mongoose_acc:t(),
113 Params :: map(),
114 Extra :: gen_hook:extra().
115 pop_offline_messages(Acc, #{jid := JID}, _Extra) ->
116 6 {ok, mongoose_acc:append(offline, messages, offline_chatmarkers(Acc, JID), Acc)}.
117
118 -spec inspect_packet(Acc, Params, Extra) -> {ok | stop, Acc} when
119 Acc :: mongoose_acc:t(),
120 Params :: map(),
121 Extra :: gen_hook:extra().
122 inspect_packet(Acc, #{from := From, to := To, packet := Packet}, _Extra) ->
123 12 case maybe_store_chat_marker(Acc, From, To, Packet) of
124 true ->
125 10 {stop, mongoose_acc:set(offline, stored, true, Acc)};
126 false ->
127 2 {ok, Acc}
128 end.
129
130 maybe_store_chat_marker(Acc, From, To, Packet) ->
131 12 HostType = mongoose_acc:host_type(Acc),
132 12 case mongoose_acc:get(mod_smart_markers, timestamp, undefined, Acc) of
133 2 undefined -> false;
134 Timestamp when is_integer(Timestamp) ->
135 10 Room = get_room(Acc, From),
136 10 Thread = get_thread(Packet),
137 10 mod_offline_chatmarkers_backend:maybe_store(HostType, To, Thread, Room, Timestamp),
138 10 true
139 end.
140
141 get_room(Acc, From) ->
142 10 case mongoose_acc:stanza_type(Acc) of
143 5 <<"groupchat">> -> From;
144 5 _ -> undefined
145 end.
146
147 get_thread(El) ->
148 10 case exml_query:path(El, [{element, <<"thread">>}, cdata]) of
149 10 Thread when Thread =/= <<>> -> Thread;
150
:-(
_ -> undefined
151 end.
152
153 offline_chatmarkers(Acc, JID) ->
154 6 HostType = mongoose_acc:host_type(Acc),
155 6 {ok, Rows} = mod_offline_chatmarkers_backend:get(HostType, JID),
156 6 mod_offline_chatmarkers_backend:remove_user(HostType, JID),
157 6 lists:concat([process_row(Acc, JID, R) || R <- Rows]).
158
159 process_row(Acc, Jid, {Thread, undefined, TS}) ->
160 2 ChatMarkers = mod_smart_markers:get_chat_markers(Jid, Thread, TS),
161 2 [build_one2one_chatmarker_msg(Acc, CM) || CM <- ChatMarkers];
162 process_row(Acc, Jid, {Thread, Room, TS}) ->
163 2 ChatMarkers = mod_smart_markers:get_chat_markers(Room, Thread, TS),
164 2 [build_room_chatmarker_msg(Acc, Jid, CM) || CM <- ChatMarkers].
165
166 build_one2one_chatmarker_msg(Acc, CM) ->
167 3 #{from := From, to := To, thread := Thread,
168 type := Type, id := Id, timestamp := TS} = CM,
169 3 Children = thread(Thread) ++ marker(Type, Id),
170 3 Attributes = [{<<"from">>, jid:to_binary(From)},
171 {<<"to">>, jid:to_binary(To)}],
172 3 Packet = #xmlel{name = <<"message">>, attrs = Attributes, children = Children},
173 3 make_route_item(Acc, From, To, TS, Packet).
174
175 build_room_chatmarker_msg(Acc, To, CM) ->
176 3 #{from := FromUser, to := Room, thread := Thread,
177 type := Type, id := Id, timestamp := TS} = CM,
178 3 FromUserBin = jid:to_bare_binary(FromUser),
179 3 From = jid:make(Room#jid.luser, Room#jid.lserver, FromUserBin),
180 3 FromBin = jid:to_binary(From),
181 3 Children = thread(Thread) ++ marker(Type, Id),
182 3 Attributes = [{<<"from">>, FromBin},
183 {<<"to">>, jid:to_binary(To)},
184 {<<"type">>, <<"groupchat">>}],
185 3 Packet = #xmlel{name = <<"message">>, attrs = Attributes, children = Children},
186 3 make_route_item(Acc, From, To, TS, Packet).
187
188 make_route_item(Acc, From, To, TS, Packet) ->
189 6 NewStanzaParams = #{element => Packet, from_jid => From, to_jid => To},
190 6 Acc1 = mongoose_acc:update_stanza(NewStanzaParams, Acc),
191 6 Acc2 = mongoose_acc:set_permanent(mod_smart_markers, timestamp, TS, Acc1),
192 6 {route, From, To, Acc2}.
193
194 marker(Type, Id) ->
195 6 [#xmlel{name = atom_to_binary(Type, latin1),
196 attrs = [{<<"xmlns">>, <<"urn:xmpp:chat-markers:0">>},
197 {<<"id">>, Id}], children = []}].
198
199 4 thread(undefined) -> [];
200 thread(Thread) ->
201 2 [#xmlel{name = <<"thread">>, attrs = [],
202 children = [#xmlcdata{content = Thread}]}].
Line Hits Source