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 |
:-( |
[dynamic_domains]. |
61 |
|
|
62 |
|
-spec deps(mongooseim:host_type(), gen_mod:module_opts()) -> gen_mod_deps:deps(). |
63 |
|
deps(_, _)-> |
64 |
:-( |
[]. %% 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 |
:-( |
mod_offline_chatmarkers_backend:init(HostType, Opts), |
70 |
:-( |
ok. |
71 |
|
|
72 |
|
-spec stop(mongooseim:host_type()) -> ok. |
73 |
|
stop(_HostType) -> |
74 |
:-( |
ok. |
75 |
|
|
76 |
|
-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). |
77 |
|
hooks(HostType) -> |
78 |
:-( |
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 |
:-( |
case gen_mod:get_module_opt(HostType, ?MODULE, store_groupchat_messages) of |
84 |
|
true -> |
85 |
:-( |
GroupChatHook = {offline_groupchat_message_hook, |
86 |
|
HostType, fun ?MODULE:inspect_packet/3, #{}, 40}, |
87 |
:-( |
[GroupChatHook | DefaultHooks]; |
88 |
:-( |
_ -> DefaultHooks |
89 |
|
end. |
90 |
|
|
91 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
92 |
|
config_spec() -> |
93 |
200 |
#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 |
:-( |
{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 |
:-( |
case maybe_store_chat_marker(Acc, From, To, Packet) of |
124 |
|
true -> |
125 |
:-( |
{stop, mongoose_acc:set(offline, stored, true, Acc)}; |
126 |
|
false -> |
127 |
:-( |
{ok, Acc} |
128 |
|
end. |
129 |
|
|
130 |
|
maybe_store_chat_marker(Acc, From, To, Packet) -> |
131 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
132 |
:-( |
case mongoose_acc:get(mod_smart_markers, timestamp, undefined, Acc) of |
133 |
:-( |
undefined -> false; |
134 |
|
Timestamp when is_integer(Timestamp) -> |
135 |
:-( |
Room = get_room(Acc, From), |
136 |
:-( |
Thread = get_thread(Packet), |
137 |
:-( |
mod_offline_chatmarkers_backend:maybe_store(HostType, To, Thread, Room, Timestamp), |
138 |
:-( |
true |
139 |
|
end. |
140 |
|
|
141 |
|
get_room(Acc, From) -> |
142 |
:-( |
case mongoose_acc:stanza_type(Acc) of |
143 |
:-( |
<<"groupchat">> -> From; |
144 |
:-( |
_ -> undefined |
145 |
|
end. |
146 |
|
|
147 |
|
get_thread(El) -> |
148 |
:-( |
case exml_query:path(El, [{element, <<"thread">>}, cdata]) of |
149 |
:-( |
Thread when Thread =/= <<>> -> Thread; |
150 |
:-( |
_ -> undefined |
151 |
|
end. |
152 |
|
|
153 |
|
offline_chatmarkers(Acc, JID) -> |
154 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
155 |
:-( |
{ok, Rows} = mod_offline_chatmarkers_backend:get(HostType, JID), |
156 |
:-( |
mod_offline_chatmarkers_backend:remove_user(HostType, JID), |
157 |
:-( |
lists:concat([process_row(Acc, JID, R) || R <- Rows]). |
158 |
|
|
159 |
|
process_row(Acc, Jid, {Thread, undefined, TS}) -> |
160 |
:-( |
ChatMarkers = mod_smart_markers:get_chat_markers(Jid, Thread, TS), |
161 |
:-( |
[build_one2one_chatmarker_msg(Acc, CM) || CM <- ChatMarkers]; |
162 |
|
process_row(Acc, Jid, {Thread, Room, TS}) -> |
163 |
:-( |
ChatMarkers = mod_smart_markers:get_chat_markers(Room, Thread, TS), |
164 |
:-( |
[build_room_chatmarker_msg(Acc, Jid, CM) || CM <- ChatMarkers]. |
165 |
|
|
166 |
|
build_one2one_chatmarker_msg(Acc, CM) -> |
167 |
:-( |
#{from := From, to := To, thread := Thread, |
168 |
|
type := Type, id := Id, timestamp := TS} = CM, |
169 |
:-( |
Children = thread(Thread) ++ marker(Type, Id), |
170 |
:-( |
Attributes = [{<<"from">>, jid:to_binary(From)}, |
171 |
|
{<<"to">>, jid:to_binary(To)}], |
172 |
:-( |
Packet = #xmlel{name = <<"message">>, attrs = Attributes, children = Children}, |
173 |
:-( |
make_route_item(Acc, From, To, TS, Packet). |
174 |
|
|
175 |
|
build_room_chatmarker_msg(Acc, To, CM) -> |
176 |
:-( |
#{from := FromUser, to := Room, thread := Thread, |
177 |
|
type := Type, id := Id, timestamp := TS} = CM, |
178 |
:-( |
FromUserBin = jid:to_bare_binary(FromUser), |
179 |
:-( |
From = jid:make(Room#jid.luser, Room#jid.lserver, FromUserBin), |
180 |
:-( |
FromBin = jid:to_binary(From), |
181 |
:-( |
Children = thread(Thread) ++ marker(Type, Id), |
182 |
:-( |
Attributes = [{<<"from">>, FromBin}, |
183 |
|
{<<"to">>, jid:to_binary(To)}, |
184 |
|
{<<"type">>, <<"groupchat">>}], |
185 |
:-( |
Packet = #xmlel{name = <<"message">>, attrs = Attributes, children = Children}, |
186 |
:-( |
make_route_item(Acc, From, To, TS, Packet). |
187 |
|
|
188 |
|
make_route_item(Acc, From, To, TS, Packet) -> |
189 |
:-( |
NewStanzaParams = #{element => Packet, from_jid => From, to_jid => To}, |
190 |
:-( |
Acc1 = mongoose_acc:update_stanza(NewStanzaParams, Acc), |
191 |
:-( |
Acc2 = mongoose_acc:set_permanent(mod_smart_markers, timestamp, TS, Acc1), |
192 |
:-( |
{route, From, To, Acc2}. |
193 |
|
|
194 |
|
marker(Type, Id) -> |
195 |
:-( |
[#xmlel{name = atom_to_binary(Type, latin1), |
196 |
|
attrs = [{<<"xmlns">>, <<"urn:xmpp:chat-markers:0">>}, |
197 |
|
{<<"id">>, Id}], children = []}]. |
198 |
|
|
199 |
:-( |
thread(undefined) -> []; |
200 |
|
thread(Thread) -> |
201 |
:-( |
[#xmlel{name = <<"thread">>, attrs = [], |
202 |
|
children = [#xmlcdata{content = Thread}]}]. |