1 |
|
%%%---------------------------------------------------------------------------- |
2 |
|
%%% @copyright (C) 2020, Erlang Solutions Ltd. |
3 |
|
%%% @doc |
4 |
|
%%% This module implements storage of the latest chat markers |
5 |
|
%%% sent by the users. This can be used to optimize mod_offline |
6 |
|
%%% functionality, or to implement custom fetching protocol and |
7 |
|
%%% avoid storage of chat markers in MAM. |
8 |
|
%%% |
9 |
|
%%% Please be aware of the next implementation details: |
10 |
|
%%% |
11 |
|
%%% 1) Current implementation is based on user_send_packet hook. |
12 |
|
%%% It doesn't work for s2s connections, but usage of another |
13 |
|
%%% hook (e.g. filter_local_packet) makes implementation harder |
14 |
|
%%% and results in multiple processing of one and the same |
15 |
|
%%% chat marker notification (sent to different users by MUC). |
16 |
|
%%% However that is the only possible way to deal with group |
17 |
|
%%% chat messages sent from the room to the user over s2s. |
18 |
|
%%% |
19 |
|
%%% ``` |
20 |
|
%%% S2S |
21 |
|
%%% + |
22 |
|
%%% | |
23 |
|
%%% +--------------------+ | |
24 |
|
%%% | | | filter |
25 |
|
%%% | +---------------> |
26 |
|
%%% send | | | filter |
27 |
|
%%% +------->+ ROOM +---------------> |
28 |
|
%%% | | | filter |
29 |
|
%%% | +---------------> |
30 |
|
%%% | | | |
31 |
|
%%% +--------------------+ | |
32 |
|
%%% | |
33 |
|
%%% + |
34 |
|
%%% ''' |
35 |
|
%%% |
36 |
|
%%% 2) DB backend requires us to provide host information, and |
37 |
|
%%% the host is always the recipient's server in case one2one |
38 |
|
%%% messages, and a master domain of the MUC service in case |
39 |
|
%%% of groupchat. |
40 |
|
%%% |
41 |
|
%%% 3) It is the client application's responsibility to ensure that |
42 |
|
%%% chat markers move only forward. There is no verification of |
43 |
|
%%% chat markers in this module, it just stores the latest chat |
44 |
|
%%% marker information sent by the user. |
45 |
|
%%% |
46 |
|
%%% 4) MUC light doesn't have message serialization! So it doesn't |
47 |
|
%%% guarantee one and the same message order for different users. |
48 |
|
%%% This can result in a race condition situation when different |
49 |
|
%%% users track (and mark) different messages as the last in a |
50 |
|
%%% chat history. However, this is a rare situation, and it self |
51 |
|
%%% recovers on the next message in the room. Anyway storing chat |
52 |
|
%%% markers in MAM doesn't fix this problem. |
53 |
|
%%% |
54 |
|
%%% @end |
55 |
|
%%%---------------------------------------------------------------------------- |
56 |
|
-module(mod_smart_markers). |
57 |
|
|
58 |
|
-include("jlib.hrl"). |
59 |
|
|
60 |
|
-xep([{xep, 333}, {version, "0.3"}]). |
61 |
|
-behaviour(gen_mod). |
62 |
|
|
63 |
|
%% gen_mod API |
64 |
|
-export([start/2]). |
65 |
|
-export([stop/1]). |
66 |
|
-export([supported_features/0]). |
67 |
|
|
68 |
|
%% gen_mod API |
69 |
|
-export([get_chat_markers/3]). |
70 |
|
|
71 |
|
%% Hook handlers |
72 |
|
-export([user_send_packet/4]). |
73 |
|
|
74 |
|
-ignore_xref([ |
75 |
|
behaviour_info/1, user_send_packet/4 |
76 |
|
]). |
77 |
|
|
78 |
|
%%-------------------------------------------------------------------- |
79 |
|
%% Type declarations |
80 |
|
%%-------------------------------------------------------------------- |
81 |
|
-type maybe_thread() :: undefined | binary(). |
82 |
|
-type chat_type() :: one2one | groupchat. |
83 |
|
|
84 |
|
-type chat_marker() :: #{from := jid:jid(), |
85 |
|
to := jid:jid(), |
86 |
|
thread := maybe_thread(), % it is not optional! |
87 |
|
type := mongoose_chat_markers:chat_marker_type(), |
88 |
|
timestamp := integer(), % microsecond |
89 |
|
id := binary()}. |
90 |
|
|
91 |
|
-export_type([chat_marker/0]). |
92 |
|
|
93 |
|
%%-------------------------------------------------------------------- |
94 |
|
%% gen_mod API |
95 |
|
%%-------------------------------------------------------------------- |
96 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any(). |
97 |
|
start(HostType, Opts) -> |
98 |
:-( |
mod_smart_markers_backend:init(HostType, Opts), |
99 |
:-( |
ejabberd_hooks:add(hooks(HostType)). |
100 |
|
|
101 |
|
-spec stop(mongooseim:host_type()) -> ok. |
102 |
|
stop(HostType) -> |
103 |
:-( |
ejabberd_hooks:delete(hooks(HostType)). |
104 |
|
|
105 |
|
-spec supported_features() -> [atom()]. |
106 |
|
supported_features() -> |
107 |
:-( |
[dynamic_domains]. |
108 |
|
|
109 |
|
%%-------------------------------------------------------------------- |
110 |
|
%% Hook handlers |
111 |
|
%%-------------------------------------------------------------------- |
112 |
|
-spec hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()]. |
113 |
|
hooks(HostType) -> |
114 |
:-( |
[{user_send_packet, HostType, ?MODULE, user_send_packet, 90}]. |
115 |
|
|
116 |
|
-spec user_send_packet(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> |
117 |
|
mongoose_acc:t(). |
118 |
|
user_send_packet(Acc, From, To, Packet = #xmlel{name = <<"message">>}) -> |
119 |
:-( |
case has_valid_markers(Acc, From, To, Packet) of |
120 |
|
{true, HostType, Markers} -> |
121 |
:-( |
update_chat_markers(Acc, HostType, Markers); |
122 |
:-( |
false -> Acc |
123 |
|
end; |
124 |
|
user_send_packet(Acc, _From, _To, _Packet) -> |
125 |
:-( |
Acc. |
126 |
|
|
127 |
|
%%-------------------------------------------------------------------- |
128 |
|
%% Other API |
129 |
|
%%-------------------------------------------------------------------- |
130 |
|
-spec get_chat_markers(jid:jid(), maybe_thread(), integer()) -> |
131 |
|
[chat_marker()]. |
132 |
|
get_chat_markers(#jid{lserver = LServer} = To, Thread, TS) -> |
133 |
|
%% internal API, no room access rights verification here! |
134 |
:-( |
{ok, HostType} = mongoose_domain_api:get_host_type(LServer), |
135 |
:-( |
mod_smart_markers_backend:get_chat_markers(HostType, To, Thread, TS). |
136 |
|
|
137 |
|
%%-------------------------------------------------------------------- |
138 |
|
%% Local functions |
139 |
|
%%-------------------------------------------------------------------- |
140 |
|
-spec update_chat_markers(mongoose_acc:t(), mongooseim:host_type(), [chat_marker()]) -> |
141 |
|
mongoose_acc:t(). |
142 |
|
update_chat_markers(Acc, HostType, Markers) -> |
143 |
:-( |
TS = mongoose_acc:timestamp(Acc), |
144 |
:-( |
[mod_smart_markers_backend:update_chat_marker(HostType, CM) || CM <- Markers], |
145 |
:-( |
mongoose_acc:set_permanent(?MODULE, timestamp, TS, Acc). |
146 |
|
|
147 |
|
-spec has_valid_markers(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> |
148 |
|
false | {true, mongooseim:host_type(), Markers :: [chat_marker()]}. |
149 |
|
has_valid_markers(Acc, From, To, Packet) -> |
150 |
:-( |
case extract_chat_markers(Acc, From, To, Packet) of |
151 |
:-( |
[] -> false; |
152 |
|
Markers -> |
153 |
:-( |
case is_valid_host(Acc, From, To) of |
154 |
:-( |
false -> false; |
155 |
:-( |
{true, HostType} -> {true, HostType, Markers} |
156 |
|
end |
157 |
|
end. |
158 |
|
|
159 |
|
-spec is_valid_host(mongoose_acc:t(), jid:jid(), jid:jid()) -> |
160 |
|
false | {true, mongooseim:host_type()}. |
161 |
|
is_valid_host(Acc, From, To) -> |
162 |
:-( |
case mongoose_acc:stanza_type(Acc) of |
163 |
:-( |
<<"groupchat">> -> get_host(Acc, From, To, groupchat); |
164 |
:-( |
_ -> get_host(Acc, From, To, one2one) |
165 |
|
end. |
166 |
|
|
167 |
|
-spec extract_chat_markers(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> |
168 |
|
[chat_marker()]. |
169 |
|
extract_chat_markers(Acc, From, To, Packet) -> |
170 |
:-( |
TS = mongoose_acc:timestamp(Acc), |
171 |
:-( |
case mongoose_chat_markers:list_chat_markers(Packet) of |
172 |
:-( |
[] -> []; |
173 |
|
ChatMarkers -> |
174 |
:-( |
CM = #{from => From, to => To, thread => get_thread(Packet), timestamp => TS}, |
175 |
:-( |
[CM#{type => Type, id => Id} || {Type, Id} <- ChatMarkers] |
176 |
|
end. |
177 |
|
|
178 |
|
-spec get_thread(exml:element()) -> maybe_thread(). |
179 |
|
get_thread(El) -> |
180 |
:-( |
case exml_query:path(El, [{element, <<"thread">>}, cdata]) of |
181 |
:-( |
Thread when Thread =/= <<>> -> Thread; |
182 |
:-( |
_ -> undefined |
183 |
|
end. |
184 |
|
|
185 |
|
-spec get_host(mongoose_acc:t(), jid:jid(), jid:jid(), chat_type()) -> |
186 |
|
false | {true, mongooseim:host_type()}. |
187 |
|
get_host(Acc, From, To, groupchat) -> |
188 |
:-( |
HostType = mod_muc_light_utils:room_jid_to_host_type(To), |
189 |
:-( |
can_access_room(HostType, Acc, From, To) andalso {true, HostType}; |
190 |
|
get_host(_Acc, _From, To, one2one) -> |
191 |
:-( |
LServer = To#jid.lserver, |
192 |
:-( |
case mongoose_domain_api:get_domain_host_type(LServer) of |
193 |
:-( |
{ok, HostType} -> {true, HostType}; |
194 |
:-( |
{error, not_found} -> false |
195 |
|
end. |
196 |
|
|
197 |
|
-spec can_access_room(HostType :: mongooseim:host_type(), |
198 |
|
Acc :: mongoose_acc:t(), |
199 |
|
User :: jid:jid(), |
200 |
|
Room :: jid:jid()) -> boolean(). |
201 |
|
can_access_room(HostType, Acc, User, Room) -> |
202 |
:-( |
mongoose_hooks:can_access_room(HostType, Acc, Room, User). |