1 |
|
%%%---------------------------------------------------------------------------- |
2 |
|
%%% @doc |
3 |
|
%%% RDBMS backend for mod_smart_markers |
4 |
|
%%% @end |
5 |
|
%%% @copyright (C) 2020-2022, Erlang Solutions Ltd. |
6 |
|
%%%---------------------------------------------------------------------------- |
7 |
|
-module(mod_smart_markers_rdbms). |
8 |
|
-author("denysgonchar"). |
9 |
|
-behavior(mod_smart_markers_backend). |
10 |
|
|
11 |
|
-include("jlib.hrl"). |
12 |
|
-include("mongoose_logger.hrl"). |
13 |
|
|
14 |
|
-export([init/2, update_chat_marker/2, get_chat_markers/4]). |
15 |
|
-export([get_conv_chat_marker/6]). |
16 |
|
-export([remove_domain/2, remove_user/2, remove_to/2, remove_to_for_user/3]). |
17 |
|
-export([encode_jid/1, encode_thread/1, encode_type/1, verify/2]). |
18 |
|
|
19 |
|
%%-------------------------------------------------------------------- |
20 |
|
%% API |
21 |
|
%%-------------------------------------------------------------------- |
22 |
|
-spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
23 |
|
init(HostType, _) -> |
24 |
8 |
KeyFields = [<<"lserver">>, <<"luser">>, <<"to_jid">>, <<"thread">>, <<"type">>], |
25 |
8 |
UpdateFields = [<<"msg_id">>, <<"timestamp">>], |
26 |
8 |
InsertFields = KeyFields ++ UpdateFields, |
27 |
8 |
rdbms_queries:prepare_upsert(HostType, smart_markers_upsert, smart_markers, |
28 |
|
InsertFields, UpdateFields, KeyFields, <<"timestamp">>), |
29 |
8 |
mongoose_rdbms:prepare(smart_markers_select_conv, smart_markers, |
30 |
|
[lserver, luser, to_jid, thread, timestamp], |
31 |
|
<<"SELECT lserver, luser, to_jid, thread, type, msg_id, timestamp FROM smart_markers " |
32 |
|
"WHERE lserver = ? AND luser = ? AND to_jid = ? AND thread = ? AND timestamp >= ?">>), |
33 |
8 |
mongoose_rdbms:prepare(smart_markers_select, smart_markers, |
34 |
|
[to_jid, thread, timestamp], |
35 |
|
<<"SELECT lserver, luser, type, msg_id, timestamp FROM smart_markers " |
36 |
|
"WHERE to_jid = ? AND thread = ? AND timestamp >= ?">>), |
37 |
8 |
mongoose_rdbms:prepare(markers_remove_domain, smart_markers, |
38 |
|
[lserver], <<"DELETE FROM smart_markers WHERE lserver=?">>), |
39 |
8 |
mongoose_rdbms:prepare(markers_remove_user, smart_markers, |
40 |
|
[lserver, luser], <<"DELETE FROM smart_markers WHERE lserver=? AND luser=?">>), |
41 |
8 |
mongoose_rdbms:prepare(markers_remove_to, smart_markers, |
42 |
|
[to_jid], <<"DELETE FROM smart_markers WHERE to_jid=?">>), |
43 |
8 |
mongoose_rdbms:prepare(markers_remove_to_for_user, smart_markers, |
44 |
|
[lserver, luser, to_jid], |
45 |
|
<<"DELETE FROM smart_markers WHERE lserver=? AND luser=? AND to_jid=?">>), |
46 |
8 |
ok. |
47 |
|
|
48 |
|
%%% @doc |
49 |
|
%%% 'from', 'to', 'thread' and 'type' keys of the ChatMarker map serve |
50 |
|
%%% as a composite database key. If key is not available in the database, |
51 |
|
%%% then chat marker must be added. Otherwise this function must update |
52 |
|
%%% chat marker record for that composite key. |
53 |
|
%%% @end |
54 |
|
-spec update_chat_marker(mongooseim:host_type(), |
55 |
|
mod_smart_markers:chat_marker()) -> ok. |
56 |
|
update_chat_marker(HostType, #{from := #jid{luser = LU, lserver = LS}, |
57 |
|
to := To, thread := Thread, |
58 |
|
type := Type, timestamp := TS, id := Id} = Marker) -> |
59 |
30 |
ToEncoded = encode_jid(To), |
60 |
30 |
ThreadEncoded = encode_thread(Thread), |
61 |
30 |
TypeEncoded = encode_type(Type), |
62 |
30 |
KeyValues = [LS, LU, ToEncoded, ThreadEncoded, TypeEncoded], |
63 |
30 |
UpdateValues = [Id, TS], |
64 |
30 |
InsertValues = KeyValues ++ UpdateValues, |
65 |
30 |
Res = rdbms_queries:execute_upsert(HostType, smart_markers_upsert, |
66 |
|
InsertValues, UpdateValues, KeyValues), |
67 |
30 |
verify(Res, Marker). |
68 |
|
|
69 |
|
-spec get_conv_chat_marker(HostType :: mongooseim:host_type(), |
70 |
|
From :: jid:jid(), |
71 |
|
To :: jid:jid(), |
72 |
|
Thread :: mod_smart_markers:maybe_thread(), |
73 |
|
Timestamp :: integer(), |
74 |
|
Private :: boolean()) -> [mod_smart_markers:chat_marker()]. |
75 |
|
get_conv_chat_marker(HostType, From, To = #jid{lserver = ToLServer}, Thread, TS, Private) -> |
76 |
|
% If To is a room, we'll want to check just the room |
77 |
24 |
case mongoose_domain_api:get_subdomain_host_type(ToLServer) of |
78 |
|
{error, not_found} -> |
79 |
17 |
one2one_get_conv_chat_marker(HostType, From, To, Thread, TS, Private); |
80 |
|
{ok, _} -> |
81 |
7 |
groupchat_get_conv_chat_marker(HostType, From, To, Thread, TS, Private) |
82 |
|
end. |
83 |
|
|
84 |
|
one2one_get_conv_chat_marker(HostType, |
85 |
|
From = #jid{luser = FromLUser, lserver = FromLServer}, |
86 |
|
To = #jid{luser = ToLUser, lserver = ToLServer}, |
87 |
|
Thread, TS, Private) -> |
88 |
17 |
{selected, ChatMarkersFrom} = mongoose_rdbms:execute_successfully( |
89 |
|
HostType, smart_markers_select_conv, |
90 |
|
[FromLServer, FromLUser, encode_jid(To), encode_thread(Thread), TS]), |
91 |
17 |
ChatMarkers = case Private of |
92 |
5 |
true -> ChatMarkersFrom; |
93 |
|
false -> |
94 |
12 |
{selected, ChatMarkersTo} = mongoose_rdbms:execute_successfully( |
95 |
|
HostType, smart_markers_select_conv, |
96 |
|
[ToLServer, ToLUser, encode_jid(From), encode_thread(Thread), TS]), |
97 |
12 |
ChatMarkersFrom ++ ChatMarkersTo |
98 |
|
end, |
99 |
17 |
[ decode_chat_marker(Tuple) || Tuple <- ChatMarkers]. |
100 |
|
|
101 |
|
groupchat_get_conv_chat_marker(HostType, _From, To, Thread, TS, false) -> |
102 |
2 |
get_chat_markers(HostType, To, Thread, TS); |
103 |
|
groupchat_get_conv_chat_marker(HostType, #jid{luser = FromLUser, lserver = FromLServer}, To, Thread, TS, true) -> |
104 |
5 |
{selected, ChatMarkers} = mongoose_rdbms:execute_successfully( |
105 |
|
HostType, smart_markers_select_conv, |
106 |
|
[FromLServer, FromLUser, encode_jid(To), encode_thread(Thread), TS]), |
107 |
5 |
[ decode_chat_marker(Tuple) || Tuple <- ChatMarkers]. |
108 |
|
|
109 |
|
%%% @doc |
110 |
|
%%% This function must return the latest chat markers sent to the |
111 |
|
%%% user/room (with or w/o thread) later than provided timestamp. |
112 |
|
%%% @end |
113 |
|
-spec get_chat_markers(HostType :: mongooseim:host_type(), |
114 |
|
To :: jid:jid(), |
115 |
|
Thread :: mod_smart_markers:maybe_thread(), |
116 |
|
Timestamp :: integer()) -> [mod_smart_markers:chat_marker()]. |
117 |
|
get_chat_markers(HostType, To, Thread, TS) -> |
118 |
29 |
{selected, ChatMarkers} = mongoose_rdbms:execute_successfully( |
119 |
|
HostType, smart_markers_select, |
120 |
|
[encode_jid(To), encode_thread(Thread), TS]), |
121 |
29 |
[ #{from => jid:make_noprep(CLUser, CLServer, <<>>), |
122 |
|
to => To, |
123 |
|
thread => Thread, |
124 |
|
type => decode_type(CType), |
125 |
|
timestamp => decode_timestamp(CTS), |
126 |
|
id => CMsgId} |
127 |
29 |
|| {CLServer, CLUser, CType, CMsgId, CTS} <- ChatMarkers]. |
128 |
|
|
129 |
|
|
130 |
|
-spec remove_domain(mongooseim:host_type(), jid:lserver()) -> mongoose_rdbms:query_result(). |
131 |
|
remove_domain(HostType, Domain) -> |
132 |
1 |
mongoose_rdbms:execute_successfully(HostType, markers_remove_domain, [Domain]). |
133 |
|
|
134 |
|
-spec remove_user(mongooseim:host_type(), jid:jid()) -> mongoose_rdbms:query_result(). |
135 |
|
remove_user(HostType, #jid{luser = LU, lserver = LS}) -> |
136 |
66 |
mongoose_rdbms:execute_successfully(HostType, markers_remove_user, [LS, LU]). |
137 |
|
|
138 |
|
-spec remove_to(mongooseim:host_type(), jid:jid()) -> mongoose_rdbms:query_result(). |
139 |
|
remove_to(HostType, To) -> |
140 |
4 |
mongoose_rdbms:execute_successfully(HostType, markers_remove_to, [encode_jid(To)]). |
141 |
|
|
142 |
|
-spec remove_to_for_user(mongooseim:host_type(), From :: jid:jid(), To :: jid:jid()) -> |
143 |
|
mongoose_rdbms:query_result(). |
144 |
|
remove_to_for_user(HostType, #jid{luser = LU, lserver = LS}, To) -> |
145 |
2 |
mongoose_rdbms:execute_successfully(HostType, markers_remove_to_for_user, [LS, LU, encode_jid(To)]). |
146 |
|
|
147 |
|
-spec verify(term(), mod_smart_markers:chat_marker()) -> ok. |
148 |
|
verify(Answer, Marker) -> |
149 |
49 |
case check_upsert_result(Answer) of |
150 |
|
{error, Reason} -> |
151 |
:-( |
?LOG_WARNING(#{what => smart_marker_insert_failed, reason => Reason, |
152 |
:-( |
marker => Marker}); |
153 |
49 |
_ -> ok |
154 |
|
end. |
155 |
|
|
156 |
|
%%-------------------------------------------------------------------- |
157 |
|
%% local functions |
158 |
|
%%-------------------------------------------------------------------- |
159 |
118 |
encode_jid(JID) -> jid:to_bare_binary(JID). |
160 |
|
|
161 |
90 |
encode_thread(undefined) -> <<>>; |
162 |
22 |
encode_thread(Thread) -> Thread. |
163 |
|
|
164 |
8 |
encode_type(received) -> <<"R">>; |
165 |
41 |
encode_type(displayed) -> <<"D">>; |
166 |
:-( |
encode_type(acknowledged) -> <<"A">>. |
167 |
|
|
168 |
|
%% MySQL returns 1 when an upsert is an insert |
169 |
|
%% and 2, when an upsert acts as update |
170 |
:-( |
check_upsert_result({updated, 0}) -> ok; |
171 |
49 |
check_upsert_result({updated, 1}) -> ok; |
172 |
:-( |
check_upsert_result({updated, 2}) -> ok; |
173 |
|
check_upsert_result(Result) -> |
174 |
:-( |
{error, {bad_result, Result}}. |
175 |
|
|
176 |
|
decode_chat_marker({LS, LU, ToJid, MsgThread, Type, MsgId, MsgTS}) -> |
177 |
19 |
#{from => jid:make_noprep(LU, LS, <<>>), |
178 |
|
to => decode_jid(ToJid), |
179 |
|
thread => decode_thread(MsgThread), |
180 |
|
type => decode_type(Type), |
181 |
|
timestamp => decode_timestamp(MsgTS), |
182 |
|
id => MsgId}. |
183 |
|
|
184 |
19 |
decode_jid(EncodedJID) -> jid:from_binary(EncodedJID). |
185 |
|
|
186 |
15 |
decode_thread(<<>>) -> undefined; |
187 |
4 |
decode_thread(Thread) -> Thread. |
188 |
|
|
189 |
4 |
decode_type(<<"R">>) -> received; |
190 |
34 |
decode_type(<<"D">>) -> displayed; |
191 |
:-( |
decode_type(<<"A">>) -> acknowledged. |
192 |
|
|
193 |
|
decode_timestamp(EncodedTS) -> |
194 |
38 |
mongoose_rdbms:result_to_integer(EncodedTS). |