%%%---------------------------------------------------------------------- %%% File : mod_push_sql.erl %%% Author : Evgeniy Khramtsov %%% Purpose : %%% Created : 26 Oct 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2017-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(mod_push_sql). -behaviour(mod_push). %% API -export([init/2, store_session/6, lookup_session/4, lookup_session/3, lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, delete_session/3, delete_old_sessions/2, export/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("mod_push.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> XML = encode_xdata(XData), TS = misc:now_to_usec(NowTS), PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), enforce_max_sessions(LUser, LServer, MaxSessions), case ?SQL_UPSERT(LServer, "push_session", ["!username=%(LUser)s", "!server_host=%(LServer)s", "!timestamp=%(TS)d", "!service=%(Service)s", "!node=%(Node)s", "xml=%(XML)s"]) of ok -> {ok, {NowTS, PushLJID, Node, XData}}; _Err -> {error, db_failure} end. lookup_session(LUser, LServer, PushJID, Node) -> PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), case ejabberd_sql:sql_query( LServer, ?SQL("select @(timestamp)d, @(xml)s from push_session " "where username=%(LUser)s and %(LServer)H " "and service=%(Service)s " "and node=%(Node)s")) of {selected, [{TS, XML}]} -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), {ok, {NowTS, PushLJID, Node, XData}}; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. lookup_session(LUser, LServer, NowTS) -> TS = misc:now_to_usec(NowTS), case ejabberd_sql:sql_query( LServer, ?SQL("select @(service)s, @(node)s, @(xml)s " "from push_session where username=%(LUser)s and %(LServer)H " "and timestamp=%(TS)d")) of {selected, [{Service, Node, XML}]} -> PushLJID = jid:tolower(jid:decode(Service)), XData = decode_xdata(XML, LUser, LServer), {ok, {NowTS, PushLJID, Node, XData}}; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. lookup_sessions(LUser, LServer, PushJID) -> PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), case ejabberd_sql:sql_query( LServer, ?SQL("select @(timestamp)d, @(xml)s, @(node)s from push_session " "where username=%(LUser)s and %(LServer)H " "and service=%(Service)s")) of {selected, Rows} -> {ok, lists:map( fun({TS, XML, Node}) -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), {NowTS, PushLJID, Node, XData} end, Rows)}; _Err -> {error, db_failure} end. lookup_sessions(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(timestamp)d, @(xml)s, @(node)s, @(service)s " "from push_session " "where username=%(LUser)s and %(LServer)H")) of {selected, Rows} -> {ok, lists:map( fun({TS, XML, Node, Service}) -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), PushLJID = jid:tolower(jid:decode(Service)), {NowTS, PushLJID,Node, XData} end, Rows)}; _Err -> {error, db_failure} end. lookup_sessions(LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s, @(timestamp)d, @(xml)s, " "@(node)s, @(service)s from push_session " "where %(LServer)H")) of {selected, Rows} -> {ok, lists:map( fun({LUser, TS, XML, Node, Service}) -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), PushLJID = jid:tolower(jid:decode(Service)), {NowTS, PushLJID, Node, XData} end, Rows)}; _Err -> {error, db_failure} end. delete_session(LUser, LServer, NowTS) -> TS = misc:now_to_usec(NowTS), case ejabberd_sql:sql_query( LServer, ?SQL("delete from push_session where " "username=%(LUser)s and %(LServer)H and timestamp=%(TS)d")) of {updated, _} -> ok; _Err -> {error, db_failure} end. delete_old_sessions(LServer, Time) -> TS = misc:now_to_usec(Time), case ejabberd_sql:sql_query( LServer, ?SQL("delete from push_session where timestamp<%(TS)d " "and %(LServer)H")) of {updated, _} -> ok; _Err -> {error, db_failure} end. export(_Server) -> [{push_session, fun(Host, #push_session{us = {LUser, LServer}, timestamp = NowTS, service = PushLJID, node = Node, xml = XData}) when LServer == Host -> TS = misc:now_to_usec(NowTS), Service = jid:encode(PushLJID), XML = encode_xdata(XData), [?SQL("delete from push_session where " "username=%(LUser)s and %(LServer)H and " "timestamp=%(TS)d and " "service=%(Service)s and node=%(Node)s and " "xml=%(XML)s;"), ?SQL_INSERT( "push_session", ["username=%(LUser)s", "server_host=%(LServer)s", "timestamp=%(TS)d", "service=%(Service)s", "node=%(Node)s", "xml=%(XML)s"])]; (_Host, _R) -> [] end}]. %%%=================================================================== %%% Internal functions %%%=================================================================== enforce_max_sessions(_LUser, _LServer, infinity) -> ok; enforce_max_sessions(LUser, LServer, MaxSessions) -> case lookup_sessions(LUser, LServer) of {ok, Sessions} when length(Sessions) >= MaxSessions -> ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", [LUser, LServer]), Sessions1 = lists:sort(fun({TS1, _, _, _}, {TS2, _, _, _}) -> TS1 >= TS2 end, Sessions), OldSessions = lists:nthtail(MaxSessions - 1, Sessions1), lists:foreach(fun({TS, _, _, _}) -> delete_session(LUser, LServer, TS) end, OldSessions); _ -> ok end. decode_xdata(<<>>, _LUser, _LServer) -> undefined; decode_xdata(XML, LUser, LServer) -> case fxml_stream:parse_element(XML) of #xmlel{} = El -> try xmpp:decode(El) catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts " "from table 'push_session': ~ts", [XML, LUser, LServer, xmpp:format_error(Why)]), undefined end; Err -> ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts from " "table 'push_session': ~p", [XML, LUser, LServer, Err]), undefined end. encode_xdata(undefined) -> <<>>; encode_xdata(XData) -> fxml:element_to_binary(xmpp:encode(XData)).