mirror of
https://github.com/processone/ejabberd.git
synced 2024-06-30 23:02:00 +02:00
Revert "Merge ApplePush to 2.2.x"
This reverts commit b8b6fc0da5
.
Conflicts:
src/mod_applepush.erl
src/mod_applepush_service.erl
This commit is contained in:
parent
59135cac6f
commit
aa60140ba8
File diff suppressed because it is too large
Load Diff
|
@ -60,26 +60,4 @@
|
||||||
fsm_limit_opts,
|
fsm_limit_opts,
|
||||||
lang,
|
lang,
|
||||||
debug=false,
|
debug=false,
|
||||||
flash_connection = false,
|
flash_connection = false}).
|
||||||
reception = true,
|
|
||||||
standby = false,
|
|
||||||
queue = queue:new(),
|
|
||||||
queue_len = 0,
|
|
||||||
pres_queue = gb_trees:empty(),
|
|
||||||
keepalive_timer,
|
|
||||||
keepalive_timeout,
|
|
||||||
oor_timeout,
|
|
||||||
oor_status = "",
|
|
||||||
oor_show = "",
|
|
||||||
oor_notification,
|
|
||||||
oor_send_body = all,
|
|
||||||
oor_send_groupchat = false,
|
|
||||||
oor_send_from = jid,
|
|
||||||
oor_appid = "",
|
|
||||||
oor_unread = 0,
|
|
||||||
oor_unread_users = ?SETS:new(),
|
|
||||||
oor_offline = false,
|
|
||||||
ack_enabled = false,
|
|
||||||
ack_counter = 0,
|
|
||||||
ack_queue = queue:new(),
|
|
||||||
ack_timer}).
|
|
|
@ -1,431 +0,0 @@
|
||||||
%%%----------------------------------------------------------------------
|
|
||||||
%%% File : mod_applepush.erl
|
|
||||||
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
||||||
%%% Purpose : Push module support
|
|
||||||
%%% Created : 5 Jun 2009 by Alexey Shchepin <alexey@process-one.net>
|
|
||||||
%%%
|
|
||||||
%%% ejabberd, Copyright (C) 2002-2009 ProcessOne
|
|
||||||
%%%----------------------------------------------------------------------
|
|
||||||
|
|
||||||
-module(mod_applepush).
|
|
||||||
-author('alexey@process-one.net').
|
|
||||||
|
|
||||||
-behaviour(gen_mod).
|
|
||||||
|
|
||||||
-export([start/2,
|
|
||||||
stop/1,
|
|
||||||
push_notification/8,
|
|
||||||
enable_offline_notification/5,
|
|
||||||
disable_notification/3,
|
|
||||||
receive_offline_packet/3,
|
|
||||||
resend_badge/1,
|
|
||||||
multi_resend_badge/1,
|
|
||||||
offline_resend_badge/0]).
|
|
||||||
|
|
||||||
%% Debug commands
|
|
||||||
-export([get_token_by_jid/1]).
|
|
||||||
|
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
|
||||||
-include("jlib.hrl").
|
|
||||||
-include("mod_privacy.hrl").
|
|
||||||
|
|
||||||
-define(NS_P1_PUSH, "p1:push").
|
|
||||||
-define(NS_P1_PUSHED, "p1:pushed").
|
|
||||||
-define(NS_P1_ATTACHMENT, "http://process-one.net/attachement").
|
|
||||||
|
|
||||||
-record(applepush_cache, {us, device_id, options}).
|
|
||||||
|
|
||||||
start(Host, _Opts) ->
|
|
||||||
case init_host(Host) of
|
|
||||||
true ->
|
|
||||||
mnesia:create_table(
|
|
||||||
applepush_cache,
|
|
||||||
[{disc_copies, [node()]},
|
|
||||||
{attributes, record_info(fields, applepush_cache)}]),
|
|
||||||
mnesia:add_table_copy(muc_online_room, node(), ram_copies),
|
|
||||||
ejabberd_hooks:add(p1_push_notification, Host,
|
|
||||||
?MODULE, push_notification, 50),
|
|
||||||
ejabberd_hooks:add(p1_push_enable_offline, Host,
|
|
||||||
?MODULE, enable_offline_notification, 50),
|
|
||||||
ejabberd_hooks:add(p1_push_disable, Host,
|
|
||||||
?MODULE, disable_notification, 50),
|
|
||||||
ejabberd_hooks:add(offline_message_hook, Host,
|
|
||||||
?MODULE, receive_offline_packet, 35);
|
|
||||||
false ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
stop(Host) ->
|
|
||||||
ejabberd_hooks:delete(p1_push_notification, Host,
|
|
||||||
?MODULE, push_notification, 50),
|
|
||||||
ejabberd_hooks:delete(p1_push_disable, Host,
|
|
||||||
?MODULE, disable_notification, 50),
|
|
||||||
ejabberd_hooks:delete(offline_message_hook, Host,
|
|
||||||
?MODULE, receive_offline_packet, 35).
|
|
||||||
|
|
||||||
|
|
||||||
push_notification(Host, JID, Notification, Msg, Unread, Sound, AppID, Sender) ->
|
|
||||||
Type = xml:get_path_s(Notification, [{elem, "type"}, cdata]),
|
|
||||||
case Type of
|
|
||||||
"applepush" ->
|
|
||||||
DeviceID = xml:get_path_s(Notification, [{elem, "id"}, cdata]),
|
|
||||||
PushService = get_push_service(Host, JID, AppID),
|
|
||||||
ServiceJID = jlib:make_jid("", PushService, ""),
|
|
||||||
Badge = integer_to_list(Unread),
|
|
||||||
SSound =
|
|
||||||
if
|
|
||||||
Sound -> "true";
|
|
||||||
true -> "false"
|
|
||||||
end,
|
|
||||||
Receiver = jlib:jid_to_string(JID),
|
|
||||||
Packet =
|
|
||||||
{xmlelement, "message", [],
|
|
||||||
[{xmlelement, "push", [{"xmlns", ?NS_P1_PUSH}],
|
|
||||||
[{xmlelement, "id", [], [{xmlcdata, DeviceID}]},
|
|
||||||
{xmlelement, "msg", [], [{xmlcdata, Msg}]},
|
|
||||||
{xmlelement, "badge", [], [{xmlcdata, Badge}]},
|
|
||||||
{xmlelement, "sound", [], [{xmlcdata, SSound}]},
|
|
||||||
{xmlelement, "from", [], [{xmlcdata, Sender}]},
|
|
||||||
{xmlelement, "to", [], [{xmlcdata, Receiver}]}]}]},
|
|
||||||
ejabberd_router:route(JID, ServiceJID, Packet),
|
|
||||||
stop;
|
|
||||||
_ ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
enable_offline_notification(JID, Notification, SendBody, SendFrom, AppID1) ->
|
|
||||||
Type = xml:get_path_s(Notification, [{elem, "type"}, cdata]),
|
|
||||||
case Type of
|
|
||||||
"applepush" ->
|
|
||||||
DeviceID = xml:get_path_s(Notification, [{elem, "id"}, cdata]),
|
|
||||||
case catch erlang:list_to_integer(DeviceID, 16) of
|
|
||||||
ID1 when is_integer(ID1) ->
|
|
||||||
AppID =
|
|
||||||
case xml:get_path_s(Notification,
|
|
||||||
[{elem, "appid"}, cdata]) of
|
|
||||||
"" -> AppID1;
|
|
||||||
A -> A
|
|
||||||
end,
|
|
||||||
{MegaSecs, Secs, _MicroSecs} = now(),
|
|
||||||
TimeStamp = MegaSecs * 1000000 + Secs,
|
|
||||||
Options =
|
|
||||||
[{appid, AppID},
|
|
||||||
{send_body, SendBody},
|
|
||||||
{send_from, SendFrom},
|
|
||||||
{timestamp, TimeStamp}],
|
|
||||||
store_cache(JID, ID1, Options);
|
|
||||||
_ ->
|
|
||||||
ok
|
|
||||||
end,
|
|
||||||
stop;
|
|
||||||
_ ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
disable_notification(JID, Notification, _AppID) ->
|
|
||||||
Type = xml:get_path_s(Notification, [{elem, "type"}, cdata]),
|
|
||||||
case Type of
|
|
||||||
"applepush" ->
|
|
||||||
delete_cache(JID),
|
|
||||||
stop;
|
|
||||||
_ ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
receive_offline_packet(From, To, Packet) ->
|
|
||||||
?DEBUG("mod_applepush offline~n\tfrom ~p~n\tto ~p~n\tpacket ~P~n",
|
|
||||||
[From, To, Packet, 8]),
|
|
||||||
Host = To#jid.lserver,
|
|
||||||
case gen_mod:is_loaded(Host, mod_applepush) of
|
|
||||||
true ->
|
|
||||||
case lookup_cache(To) of
|
|
||||||
false ->
|
|
||||||
ok;
|
|
||||||
{ID, AppID, SendBody, SendFrom} ->
|
|
||||||
?DEBUG("lookup: ~p~n", [{ID, AppID, SendBody, SendFrom}]),
|
|
||||||
Body1 = xml:get_path_s(Packet, [{elem, "body"}, cdata]),
|
|
||||||
Body =
|
|
||||||
case check_x_attachment(Packet) of
|
|
||||||
true ->
|
|
||||||
case Body1 of
|
|
||||||
"" -> [238, 128, 136];
|
|
||||||
_ ->
|
|
||||||
[238, 128, 136, 32 | Body1]
|
|
||||||
end;
|
|
||||||
false ->
|
|
||||||
Body1
|
|
||||||
end,
|
|
||||||
Pushed = check_x_pushed(Packet),
|
|
||||||
PushService = get_push_service(Host, To, AppID),
|
|
||||||
ServiceJID = jlib:make_jid("", PushService, ""),
|
|
||||||
if
|
|
||||||
Body == "";
|
|
||||||
Pushed ->
|
|
||||||
if
|
|
||||||
From#jid.lserver == ServiceJID#jid.lserver ->
|
|
||||||
Disable =
|
|
||||||
xml:get_path_s(
|
|
||||||
Packet, [{elem, "disable"}]) /= "",
|
|
||||||
if
|
|
||||||
Disable ->
|
|
||||||
delete_cache(To);
|
|
||||||
true ->
|
|
||||||
ok
|
|
||||||
end;
|
|
||||||
true ->
|
|
||||||
ok
|
|
||||||
end,
|
|
||||||
ok;
|
|
||||||
true ->
|
|
||||||
BFrom = jlib:jid_remove_resource(From),
|
|
||||||
SFrom = jlib:jid_to_string(BFrom),
|
|
||||||
Offline = ejabberd_hooks:run_fold(
|
|
||||||
count_offline_messages,
|
|
||||||
Host,
|
|
||||||
0,
|
|
||||||
[To#jid.luser, Host]),
|
|
||||||
IncludeBody =
|
|
||||||
case SendBody of
|
|
||||||
all ->
|
|
||||||
true;
|
|
||||||
first_per_user ->
|
|
||||||
Offline == 0;
|
|
||||||
first ->
|
|
||||||
Offline == 0;
|
|
||||||
none ->
|
|
||||||
false
|
|
||||||
end,
|
|
||||||
Msg =
|
|
||||||
if
|
|
||||||
IncludeBody ->
|
|
||||||
CBody = utf8_cut(Body, 100),
|
|
||||||
case SendFrom of
|
|
||||||
jid -> SFrom ++ ": " ++ CBody;
|
|
||||||
username -> BFrom#jid.user ++ ": " ++ CBody;
|
|
||||||
_ -> CBody
|
|
||||||
end;
|
|
||||||
true ->
|
|
||||||
""
|
|
||||||
end,
|
|
||||||
SSound =
|
|
||||||
if
|
|
||||||
IncludeBody -> "true";
|
|
||||||
true -> "false"
|
|
||||||
end,
|
|
||||||
Badge = integer_to_list(Offline + 1),
|
|
||||||
DeviceID = erlang:integer_to_list(ID, 16),
|
|
||||||
STo = jlib:jid_to_string(To),
|
|
||||||
Packet1 =
|
|
||||||
{xmlelement, "message", [],
|
|
||||||
[{xmlelement, "push", [{"xmlns", ?NS_P1_PUSH}],
|
|
||||||
[{xmlelement, "id", [],
|
|
||||||
[{xmlcdata, DeviceID}]},
|
|
||||||
{xmlelement, "msg", [],
|
|
||||||
[{xmlcdata, Msg}]},
|
|
||||||
{xmlelement, "badge", [],
|
|
||||||
[{xmlcdata, Badge}]},
|
|
||||||
{xmlelement, "sound", [],
|
|
||||||
[{xmlcdata, SSound}]},
|
|
||||||
{xmlelement, "from", [],
|
|
||||||
[{xmlcdata, SFrom}]},
|
|
||||||
{xmlelement, "to", [],
|
|
||||||
[{xmlcdata, STo}]}]}]},
|
|
||||||
ejabberd_router:route(To, ServiceJID, Packet1)
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
false ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
resend_badge(To) ->
|
|
||||||
Host = To#jid.lserver,
|
|
||||||
case gen_mod:is_loaded(Host, mod_applepush) of
|
|
||||||
true ->
|
|
||||||
case lookup_cache(To) of
|
|
||||||
false ->
|
|
||||||
{error, "no cached data for the user"};
|
|
||||||
{ID, AppID, SendBody, SendFrom} ->
|
|
||||||
?DEBUG("lookup: ~p~n", [{ID, AppID, SendBody, SendFrom}]),
|
|
||||||
PushService = get_push_service(Host, To, AppID),
|
|
||||||
ServiceJID = jlib:make_jid("", PushService, ""),
|
|
||||||
Offline = ejabberd_hooks:run_fold(
|
|
||||||
count_offline_messages,
|
|
||||||
Host,
|
|
||||||
0,
|
|
||||||
[To#jid.luser, Host]),
|
|
||||||
if
|
|
||||||
Offline == 0 ->
|
|
||||||
ok;
|
|
||||||
true ->
|
|
||||||
Badge = integer_to_list(Offline),
|
|
||||||
DeviceID = erlang:integer_to_list(ID, 16),
|
|
||||||
Packet1 =
|
|
||||||
{xmlelement, "message", [],
|
|
||||||
[{xmlelement, "push", [{"xmlns", ?NS_P1_PUSH}],
|
|
||||||
[{xmlelement, "id", [],
|
|
||||||
[{xmlcdata, DeviceID}]},
|
|
||||||
{xmlelement, "badge", [],
|
|
||||||
[{xmlcdata, Badge}]}]}]},
|
|
||||||
ejabberd_router:route(To, ServiceJID, Packet1)
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
false ->
|
|
||||||
{error, "mod_applepush is not loaded"}
|
|
||||||
end.
|
|
||||||
|
|
||||||
multi_resend_badge(JIDs) ->
|
|
||||||
lists:foreach(fun resend_badge/1, JIDs).
|
|
||||||
|
|
||||||
offline_resend_badge() ->
|
|
||||||
USs = mnesia:dirty_all_keys(applepush_cache),
|
|
||||||
JIDs = lists:map(fun({U, S}) -> jlib:make_jid(U, S, "") end, USs),
|
|
||||||
multi_resend_badge(JIDs).
|
|
||||||
|
|
||||||
lookup_cache(JID) ->
|
|
||||||
#jid{luser = LUser, lserver = LServer} = JID,
|
|
||||||
LUS = {LUser, LServer},
|
|
||||||
case catch mnesia:dirty_read(applepush_cache, LUS) of
|
|
||||||
[#applepush_cache{device_id = DeviceID, options = Options}] ->
|
|
||||||
AppID = proplists:get_value(appid, Options, "applepush.localhost"),
|
|
||||||
SendBody = proplists:get_value(send_body, Options, none),
|
|
||||||
SendFrom = proplists:get_value(send_from, Options, true),
|
|
||||||
{DeviceID, AppID, SendBody, SendFrom};
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end.
|
|
||||||
|
|
||||||
store_cache(JID, DeviceID, Options) ->
|
|
||||||
#jid{luser = LUser, lserver = LServer} = JID,
|
|
||||||
LUS = {LUser, LServer},
|
|
||||||
R = #applepush_cache{us = LUS,
|
|
||||||
device_id = DeviceID,
|
|
||||||
options = Options},
|
|
||||||
case catch mnesia:dirty_read(applepush_cache, LUS) of
|
|
||||||
[R] ->
|
|
||||||
ok;
|
|
||||||
_ ->
|
|
||||||
catch mnesia:dirty_write(R)
|
|
||||||
end.
|
|
||||||
|
|
||||||
delete_cache(JID) ->
|
|
||||||
#jid{luser = LUser, lserver = LServer} = JID,
|
|
||||||
LUS = {LUser, LServer},
|
|
||||||
catch mnesia:dirty_delete(applepush_cache, LUS).
|
|
||||||
|
|
||||||
|
|
||||||
utf8_cut(S, Bytes) ->
|
|
||||||
utf8_cut(S, [], [], Bytes + 1).
|
|
||||||
|
|
||||||
utf8_cut(_S, _Cur, Prev, 0) ->
|
|
||||||
lists:reverse(Prev);
|
|
||||||
utf8_cut([], Cur, _Prev, _Bytes) ->
|
|
||||||
lists:reverse(Cur);
|
|
||||||
utf8_cut([C | S], Cur, Prev, Bytes) ->
|
|
||||||
if
|
|
||||||
C bsr 6 == 2 ->
|
|
||||||
utf8_cut(S, [C | Cur], Prev, Bytes - 1);
|
|
||||||
true ->
|
|
||||||
utf8_cut(S, [C | Cur], Cur, Bytes - 1)
|
|
||||||
end.
|
|
||||||
|
|
||||||
check_x_pushed({xmlelement, _Name, _Attrs, Els}) ->
|
|
||||||
check_x_pushed1(Els).
|
|
||||||
|
|
||||||
check_x_pushed1([]) ->
|
|
||||||
false;
|
|
||||||
check_x_pushed1([{xmlcdata, _} | Els]) ->
|
|
||||||
check_x_pushed1(Els);
|
|
||||||
check_x_pushed1([El | Els]) ->
|
|
||||||
case xml:get_tag_attr_s("xmlns", El) of
|
|
||||||
?NS_P1_PUSHED ->
|
|
||||||
true;
|
|
||||||
_ ->
|
|
||||||
check_x_pushed1(Els)
|
|
||||||
end.
|
|
||||||
|
|
||||||
check_x_attachment({xmlelement, _Name, _Attrs, Els}) ->
|
|
||||||
check_x_attachment1(Els).
|
|
||||||
|
|
||||||
check_x_attachment1([]) ->
|
|
||||||
false;
|
|
||||||
check_x_attachment1([{xmlcdata, _} | Els]) ->
|
|
||||||
check_x_attachment1(Els);
|
|
||||||
check_x_attachment1([El | Els]) ->
|
|
||||||
case xml:get_tag_attr_s("xmlns", El) of
|
|
||||||
?NS_P1_ATTACHMENT ->
|
|
||||||
true;
|
|
||||||
_ ->
|
|
||||||
check_x_attachment1(Els)
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
get_push_service(Host, JID, AppID) ->
|
|
||||||
PushServices =
|
|
||||||
gen_mod:get_module_opt(
|
|
||||||
Host, ?MODULE,
|
|
||||||
push_services, []),
|
|
||||||
PushService =
|
|
||||||
case lists:keysearch(AppID, 1, PushServices) of
|
|
||||||
false ->
|
|
||||||
DefaultServices =
|
|
||||||
gen_mod:get_module_opt(
|
|
||||||
Host, ?MODULE,
|
|
||||||
default_services, []),
|
|
||||||
case lists:keysearch(JID#jid.lserver, 1, DefaultServices) of
|
|
||||||
false ->
|
|
||||||
gen_mod:get_module_opt(
|
|
||||||
Host, ?MODULE,
|
|
||||||
default_service, "applepush.localhost");
|
|
||||||
{value, {_, PS}} ->
|
|
||||||
PS
|
|
||||||
end;
|
|
||||||
{value, {AppID, PS}} ->
|
|
||||||
PS
|
|
||||||
end,
|
|
||||||
PushService.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
||||||
%% Internal module protection
|
|
||||||
|
|
||||||
-define(VALID_HOSTS, []). % default is unlimited use
|
|
||||||
-define(MAX_USERS, 0). % default is unlimited use
|
|
||||||
|
|
||||||
init_host(VHost) ->
|
|
||||||
case ?VALID_HOSTS of
|
|
||||||
[] -> % unlimited use
|
|
||||||
true;
|
|
||||||
ValidList -> % limited use
|
|
||||||
init_host(VHost, ValidList)
|
|
||||||
end.
|
|
||||||
init_host([], _) ->
|
|
||||||
false;
|
|
||||||
init_host(VHost, ValidEncryptedList) ->
|
|
||||||
EncryptedHost = erlang:md5(lists:reverse(VHost)),
|
|
||||||
case lists:member(EncryptedHost, ValidEncryptedList) of
|
|
||||||
true ->
|
|
||||||
case ?MAX_USERS of
|
|
||||||
0 -> true;
|
|
||||||
N -> ejabberd_auth:get_vh_registered_users_number(VHost) =< N
|
|
||||||
end;
|
|
||||||
false ->
|
|
||||||
case string:chr(VHost, $.) of
|
|
||||||
0 -> false;
|
|
||||||
Pos -> init_host(string:substr(VHost, Pos+1), ValidEncryptedList)
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Debug commands
|
|
||||||
%% JID is of form
|
|
||||||
get_token_by_jid(JIDString) ->
|
|
||||||
#jid{luser = LUser, lserver = LServer} = jlib:string_to_jid(JIDString),
|
|
||||||
LUS = {LUser, LServer},
|
|
||||||
case mnesia:dirty_read(applepush_cache, LUS) of
|
|
||||||
[{applepush_cache,_,I,_}] ->
|
|
||||||
erlang:integer_to_list(I, 16);
|
|
||||||
_ ->
|
|
||||||
undefined
|
|
||||||
end.
|
|
||||||
|
|
|
@ -1,598 +0,0 @@
|
||||||
%%%----------------------------------------------------------------------
|
|
||||||
%%% File : mod_applepush_service.erl
|
|
||||||
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
||||||
%%% Purpose : Central push infrastructure
|
|
||||||
%%% Created : 5 Jun 2009 by Alexey Shchepin <alexey@process-one.net>
|
|
||||||
%%%
|
|
||||||
%%% ejabberd, Copyright (C) 2002-2009 ProcessOne
|
|
||||||
%%%----------------------------------------------------------------------
|
|
||||||
|
|
||||||
-module(mod_applepush_service).
|
|
||||||
-author('alexey@process-one.net').
|
|
||||||
|
|
||||||
-behaviour(gen_server).
|
|
||||||
-behaviour(gen_mod).
|
|
||||||
|
|
||||||
%% API
|
|
||||||
-export([start_link/2, start/2, stop/1]).
|
|
||||||
|
|
||||||
%% gen_server callbacks
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
||||||
terminate/2, code_change/3]).
|
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
|
||||||
-include("jlib.hrl").
|
|
||||||
|
|
||||||
-record(state, {host,
|
|
||||||
socket,
|
|
||||||
gateway,
|
|
||||||
port,
|
|
||||||
feedback_socket,
|
|
||||||
feedback,
|
|
||||||
feedback_port,
|
|
||||||
feedback_buf = <<>>,
|
|
||||||
certfile,
|
|
||||||
queue,
|
|
||||||
soundfile,
|
|
||||||
cmd_id = 0,
|
|
||||||
cmd_cache = dict:new(),
|
|
||||||
device_cache = dict:new()}).
|
|
||||||
|
|
||||||
-define(PROCNAME, ejabberd_mod_applepush_service).
|
|
||||||
-define(RECONNECT_TIMEOUT, 5000).
|
|
||||||
-define(FEEDBACK_RECONNECT_TIMEOUT, 30000).
|
|
||||||
-define(MAX_QUEUE_SIZE, 1000).
|
|
||||||
-define(CACHE_SIZE, 4096).
|
|
||||||
-define(MAX_PAYLOAD_SIZE, 255).
|
|
||||||
|
|
||||||
-define(NS_P1_PUSH, "p1:push").
|
|
||||||
|
|
||||||
%%====================================================================
|
|
||||||
%% API
|
|
||||||
%%====================================================================
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
|
|
||||||
%% Description: Starts the server
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
start_link(Host, Opts) ->
|
|
||||||
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
||||||
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
|
|
||||||
|
|
||||||
start(Host, Opts) ->
|
|
||||||
ssl:start(),
|
|
||||||
MyHosts =
|
|
||||||
case catch gen_mod:get_opt(hosts, Opts) of
|
|
||||||
{'EXIT', _} ->
|
|
||||||
[{gen_mod:get_opt_host(Host, Opts, "applepush.@HOST@"), Opts}];
|
|
||||||
Hs ->
|
|
||||||
Hs
|
|
||||||
end,
|
|
||||||
lists:foreach(
|
|
||||||
fun({MyHost, MyOpts}) ->
|
|
||||||
Proc = gen_mod:get_module_proc(MyHost, ?PROCNAME),
|
|
||||||
ChildSpec =
|
|
||||||
{Proc,
|
|
||||||
{?MODULE, start_link, [MyHost, MyOpts]},
|
|
||||||
transient,
|
|
||||||
1000,
|
|
||||||
worker,
|
|
||||||
[?MODULE]},
|
|
||||||
supervisor:start_child(ejabberd_sup, ChildSpec)
|
|
||||||
end, MyHosts).
|
|
||||||
|
|
||||||
stop(Host) ->
|
|
||||||
MyHosts =
|
|
||||||
case catch gen_mod:get_module_opt(Host, ?MODULE, hosts, []) of
|
|
||||||
[] ->
|
|
||||||
[gen_mod:get_module_opt_host(
|
|
||||||
Host, ?MODULE, "applepush.@HOST@")];
|
|
||||||
Hs ->
|
|
||||||
[H || {H, _} <- Hs]
|
|
||||||
end,
|
|
||||||
lists:foreach(
|
|
||||||
fun(MyHost) ->
|
|
||||||
Proc = gen_mod:get_module_proc(MyHost, ?PROCNAME),
|
|
||||||
gen_server:call(Proc, stop),
|
|
||||||
supervisor:terminate_child(ejabberd_sup, Proc),
|
|
||||||
supervisor:delete_child(ejabberd_sup, Proc)
|
|
||||||
end, MyHosts).
|
|
||||||
|
|
||||||
|
|
||||||
%%====================================================================
|
|
||||||
%% gen_server callbacks
|
|
||||||
%%====================================================================
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Function: init(Args) -> {ok, State} |
|
|
||||||
%% {ok, State, Timeout} |
|
|
||||||
%% ignore |
|
|
||||||
%% {stop, Reason}
|
|
||||||
%% Description: Initiates the server
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
init([MyHost, Opts]) ->
|
|
||||||
CertFile = gen_mod:get_opt(certfile, Opts, ""),
|
|
||||||
SoundFile = gen_mod:get_opt(sound_file, Opts, "pushalert.wav"),
|
|
||||||
Gateway = gen_mod:get_opt(gateway, Opts, "gateway.push.apple.com"),
|
|
||||||
Feedback = gen_mod:get_opt(feedback, Opts, undefined),
|
|
||||||
Port = gen_mod:get_opt(port, Opts, 2195),
|
|
||||||
FeedbackPort = gen_mod:get_opt(feedback_port, Opts, 2196),
|
|
||||||
%MyHost = gen_mod:get_opt_host(Host, Opts, "applepush.@HOST@"),
|
|
||||||
self() ! connect,
|
|
||||||
case Feedback of
|
|
||||||
undefined ->
|
|
||||||
ok;
|
|
||||||
_ ->
|
|
||||||
self() ! connect_feedback
|
|
||||||
end,
|
|
||||||
ejabberd_router:register_route(MyHost),
|
|
||||||
{ok, #state{host = MyHost,
|
|
||||||
gateway = Gateway,
|
|
||||||
port = Port,
|
|
||||||
feedback = Feedback,
|
|
||||||
feedback_port = FeedbackPort,
|
|
||||||
certfile = CertFile,
|
|
||||||
queue = {0, queue:new()},
|
|
||||||
soundfile = SoundFile}}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
|
|
||||||
%% {reply, Reply, State, Timeout} |
|
|
||||||
%% {noreply, State} |
|
|
||||||
%% {noreply, State, Timeout} |
|
|
||||||
%% {stop, Reason, Reply, State} |
|
|
||||||
%% {stop, Reason, State}
|
|
||||||
%% Description: Handling call messages
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
handle_call(stop, _From, State) ->
|
|
||||||
{stop, normal, ok, State}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Function: handle_cast(Msg, State) -> {noreply, State} |
|
|
||||||
%% {noreply, State, Timeout} |
|
|
||||||
%% {stop, Reason, State}
|
|
||||||
%% Description: Handling cast messages
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
handle_cast(_Msg, State) ->
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Function: handle_info(Info, State) -> {noreply, State} |
|
|
||||||
%% {noreply, State, Timeout} |
|
|
||||||
%% {stop, Reason, State}
|
|
||||||
%% Description: Handling all non call/cast messages
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
handle_info({route, From, To, Packet}, State) ->
|
|
||||||
case catch do_route(From, To, Packet, State) of
|
|
||||||
{'EXIT', Reason} ->
|
|
||||||
?ERROR_MSG("~p", [Reason]),
|
|
||||||
{noreply, State};
|
|
||||||
Res ->
|
|
||||||
Res
|
|
||||||
end;
|
|
||||||
handle_info(connect, State) ->
|
|
||||||
connect(State);
|
|
||||||
handle_info(connect_feedback, State)
|
|
||||||
when State#state.feedback /= undefined,
|
|
||||||
State#state.feedback_socket == undefined ->
|
|
||||||
Feedback = State#state.feedback,
|
|
||||||
FeedbackPort = State#state.feedback_port,
|
|
||||||
CertFile = State#state.certfile,
|
|
||||||
case ssl:connect(Feedback, FeedbackPort,
|
|
||||||
[{certfile, CertFile},
|
|
||||||
{active, true},
|
|
||||||
binary]) of
|
|
||||||
{ok, Socket} ->
|
|
||||||
{noreply, State#state{feedback_socket = Socket}};
|
|
||||||
{error, Reason} ->
|
|
||||||
?ERROR_MSG("(~p) Connection to ~p:~p failed: ~p, "
|
|
||||||
"retrying after ~p seconds",
|
|
||||||
[State#state.host, Feedback, FeedbackPort,
|
|
||||||
Reason, ?FEEDBACK_RECONNECT_TIMEOUT div 1000]),
|
|
||||||
erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
|
|
||||||
connect_feedback),
|
|
||||||
{noreply, State}
|
|
||||||
end;
|
|
||||||
handle_info({ssl, Socket, Packet}, State)
|
|
||||||
when Socket == State#state.socket ->
|
|
||||||
case Packet of
|
|
||||||
<<8, Status, CmdID:32>> when Status /= 0 ->
|
|
||||||
case dict:find(CmdID, State#state.cmd_cache) of
|
|
||||||
{ok, {JID, _DeviceID}} ->
|
|
||||||
?ERROR_MSG("PUSH ERROR for ~p: ~p", [JID, Status]),
|
|
||||||
if
|
|
||||||
Status == 8 ->
|
|
||||||
From = jlib:make_jid("", State#state.host, ""),
|
|
||||||
ejabberd_router:route(
|
|
||||||
From, JID,
|
|
||||||
{xmlelement, "message", [],
|
|
||||||
[{xmlelement, "disable",
|
|
||||||
[{"xmlns", ?NS_P1_PUSH},
|
|
||||||
{"status", integer_to_list(Status)}],
|
|
||||||
[]}]});
|
|
||||||
true ->
|
|
||||||
ok
|
|
||||||
end,
|
|
||||||
ok;
|
|
||||||
error ->
|
|
||||||
?ERROR_MSG("Unknown cmd ID ~p~n", [CmdID]),
|
|
||||||
ok
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
?ERROR_MSG("Received unknown packet ~p~n", [Packet])
|
|
||||||
end,
|
|
||||||
{noreply, State};
|
|
||||||
handle_info({ssl, Socket, Packet}, State)
|
|
||||||
when Socket == State#state.feedback_socket ->
|
|
||||||
Buf = <<(State#state.feedback_buf)/binary, Packet/binary>>,
|
|
||||||
Buf2 = parse_feedback_buf(Buf, State),
|
|
||||||
{noreply, State#state{feedback_buf = Buf2}};
|
|
||||||
handle_info({ssl_closed, Socket}, State)
|
|
||||||
when Socket == State#state.feedback_socket ->
|
|
||||||
ssl:close(Socket),
|
|
||||||
erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
|
|
||||||
connect_feedback),
|
|
||||||
{noreply, State#state{feedback_socket = undefined,
|
|
||||||
feedback_buf = <<>>}};
|
|
||||||
handle_info(_Info, State) ->
|
|
||||||
%io:format("got info: ~p~n", [_Info]),
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Function: terminate(Reason, State) -> void()
|
|
||||||
%% Description: This function is called by a gen_server when it is about to
|
|
||||||
%% terminate. It should be the opposite of Module:init/1 and do any necessary
|
|
||||||
%% cleaning up. When it returns, the gen_server terminates with Reason.
|
|
||||||
%% The return value is ignored.
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
terminate(_Reason, State) ->
|
|
||||||
ejabberd_router:unregister_route(State#state.host),
|
|
||||||
ok.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
|
|
||||||
%% Description: Convert process state when code is changed
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
|
||||||
{ok, State}.
|
|
||||||
|
|
||||||
%%%----------------------------------------------------------------------
|
|
||||||
%%% Internal functions
|
|
||||||
%%%----------------------------------------------------------------------
|
|
||||||
|
|
||||||
do_route(From, To, Packet, State) ->
|
|
||||||
#jid{user = User, resource = Resource} = To,
|
|
||||||
if
|
|
||||||
(User /= "") or (Resource /= "") ->
|
|
||||||
Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE),
|
|
||||||
ejabberd_router:route(To, From, Err),
|
|
||||||
{noreply, State};
|
|
||||||
true ->
|
|
||||||
case Packet of
|
|
||||||
{xmlelement, "iq", _, _} ->
|
|
||||||
IQ = jlib:iq_query_info(Packet),
|
|
||||||
case IQ of
|
|
||||||
#iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS,
|
|
||||||
sub_el = _SubEl, lang = Lang} = IQ ->
|
|
||||||
Res = IQ#iq{type = result,
|
|
||||||
sub_el = [{xmlelement, "query",
|
|
||||||
[{"xmlns", XMLNS}],
|
|
||||||
iq_disco(Lang)}]},
|
|
||||||
ejabberd_router:route(To,
|
|
||||||
From,
|
|
||||||
jlib:iq_to_xml(Res)),
|
|
||||||
{noreply, State};
|
|
||||||
#iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS} = IQ ->
|
|
||||||
Res = IQ#iq{type = result,
|
|
||||||
sub_el = [{xmlelement, "query",
|
|
||||||
[{"xmlns", XMLNS}],
|
|
||||||
[]}]},
|
|
||||||
ejabberd_router:route(To,
|
|
||||||
From,
|
|
||||||
jlib:iq_to_xml(Res)),
|
|
||||||
{noreply, State};
|
|
||||||
%%#iq{type = get, xmlns = ?NS_VCARD, lang = Lang} ->
|
|
||||||
%% ResIQ =
|
|
||||||
%% IQ#iq{type = result,
|
|
||||||
%% sub_el = [{xmlelement,
|
|
||||||
%% "vCard",
|
|
||||||
%% [{"xmlns", ?NS_VCARD}],
|
|
||||||
%% iq_get_vcard(Lang)}]},
|
|
||||||
%% ejabberd_router:route(To,
|
|
||||||
%% From,
|
|
||||||
%% jlib:iq_to_xml(ResIQ));
|
|
||||||
_ ->
|
|
||||||
Err = jlib:make_error_reply(Packet,
|
|
||||||
?ERR_SERVICE_UNAVAILABLE),
|
|
||||||
ejabberd_router:route(To, From, Err),
|
|
||||||
{noreply, State}
|
|
||||||
end;
|
|
||||||
{xmlelement, "message", _, Els} ->
|
|
||||||
case xml:remove_cdata(Els) of
|
|
||||||
[{xmlelement, "push", _, _}] ->
|
|
||||||
NewState = handle_message(From, To, Packet, State),
|
|
||||||
{noreply, NewState};
|
|
||||||
[{xmlelement, "disable", _, _}] ->
|
|
||||||
{noreply, State};
|
|
||||||
_ ->
|
|
||||||
{noreply, State}
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
{noreply, State}
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
handle_message(From, To, Packet, #state{socket = undefined} = State) ->
|
|
||||||
queue_message(From, To, Packet, State);
|
|
||||||
handle_message(From, To, Packet, State) ->
|
|
||||||
DeviceID =
|
|
||||||
xml:get_path_s(Packet,
|
|
||||||
[{elem, "push"}, {elem, "id"}, cdata]),
|
|
||||||
Msg =
|
|
||||||
xml:get_path_s(Packet,
|
|
||||||
[{elem, "push"}, {elem, "msg"}, cdata]),
|
|
||||||
Badge =
|
|
||||||
xml:get_path_s(Packet,
|
|
||||||
[{elem, "push"}, {elem, "badge"}, cdata]),
|
|
||||||
Sound =
|
|
||||||
xml:get_path_s(Packet,
|
|
||||||
[{elem, "push"}, {elem, "sound"}, cdata]),
|
|
||||||
Sender =
|
|
||||||
xml:get_path_s(Packet,
|
|
||||||
[{elem, "push"}, {elem, "from"}, cdata]),
|
|
||||||
Receiver =
|
|
||||||
xml:get_path_s(Packet,
|
|
||||||
[{elem, "push"}, {elem, "to"}, cdata]),
|
|
||||||
Payload = make_payload(State, Msg, Badge, Sound, Sender),
|
|
||||||
ID =
|
|
||||||
case catch erlang:list_to_integer(DeviceID, 16) of
|
|
||||||
ID1 when is_integer(ID1) ->
|
|
||||||
ID1;
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end,
|
|
||||||
if
|
|
||||||
is_integer(ID) ->
|
|
||||||
Command = 1,
|
|
||||||
CmdID = State#state.cmd_id,
|
|
||||||
{MegaSecs, Secs, _MicroSecs} = now(),
|
|
||||||
Expiry = MegaSecs * 1000000 + Secs + 24 * 60 * 60,
|
|
||||||
BDeviceID = <<ID:256>>,
|
|
||||||
BPayload = list_to_binary(Payload),
|
|
||||||
IDLen = size(BDeviceID),
|
|
||||||
PayloadLen = size(BPayload),
|
|
||||||
Notification =
|
|
||||||
<<Command:8,
|
|
||||||
CmdID:32,
|
|
||||||
Expiry:32,
|
|
||||||
IDLen:16,
|
|
||||||
BDeviceID/binary,
|
|
||||||
PayloadLen:16,
|
|
||||||
BPayload/binary>>,
|
|
||||||
?INFO_MSG("(~p) sending notification for ~s~n~p~npayload:~n~s~n"
|
|
||||||
"Sender: ~s~n"
|
|
||||||
"Receiver: ~s~n"
|
|
||||||
"Device ID: ~s~n",
|
|
||||||
[State#state.host, erlang:integer_to_list(ID, 16),
|
|
||||||
Notification, Payload,
|
|
||||||
Sender,
|
|
||||||
Receiver, DeviceID]),
|
|
||||||
case ssl:send(State#state.socket, Notification) of
|
|
||||||
ok ->
|
|
||||||
cache(From, ID, State);
|
|
||||||
{error, Reason} ->
|
|
||||||
?INFO_MSG("(~p) Connection closed: ~p, reconnecting",
|
|
||||||
[State#state.host, Reason]),
|
|
||||||
ssl:close(State#state.socket),
|
|
||||||
self() ! connect,
|
|
||||||
queue_message(From, To, Packet,
|
|
||||||
State#state{socket = undefined})
|
|
||||||
end;
|
|
||||||
true ->
|
|
||||||
State
|
|
||||||
end.
|
|
||||||
|
|
||||||
make_payload(State, Msg, Badge, Sound, Sender) ->
|
|
||||||
Msg2 = json_escape(Msg),
|
|
||||||
AlertPayload =
|
|
||||||
case Msg2 of
|
|
||||||
"" -> "";
|
|
||||||
_ -> "\"alert\":\"" ++ Msg2 ++ "\""
|
|
||||||
end,
|
|
||||||
BadgePayload =
|
|
||||||
case catch list_to_integer(Badge) of
|
|
||||||
B when is_integer(B) ->
|
|
||||||
"\"badge\":" ++ Badge;
|
|
||||||
_ -> ""
|
|
||||||
end,
|
|
||||||
SoundPayload =
|
|
||||||
case Sound of
|
|
||||||
"true" ->
|
|
||||||
SoundFile = State#state.soundfile,
|
|
||||||
"\"sound\":\"" ++ json_escape(SoundFile) ++ "\"";
|
|
||||||
_ -> ""
|
|
||||||
end,
|
|
||||||
Payloads = lists:filter(fun(S) -> S /= "" end,
|
|
||||||
[AlertPayload, BadgePayload, SoundPayload]),
|
|
||||||
Payload =
|
|
||||||
case Sender of
|
|
||||||
"" ->
|
|
||||||
"{\"aps\":{" ++ join(Payloads, ",") ++ "}}";
|
|
||||||
_ ->
|
|
||||||
"{\"aps\":{" ++ join(Payloads, ",") ++ "},"
|
|
||||||
"\"from\":\"" ++ json_escape(Sender) ++ "\"}"
|
|
||||||
end,
|
|
||||||
PayloadLen = length(Payload),
|
|
||||||
if
|
|
||||||
PayloadLen > ?MAX_PAYLOAD_SIZE ->
|
|
||||||
Delta = PayloadLen - ?MAX_PAYLOAD_SIZE,
|
|
||||||
MsgLen = length(Msg),
|
|
||||||
if
|
|
||||||
MsgLen /= 0 ->
|
|
||||||
CutMsg =
|
|
||||||
if
|
|
||||||
MsgLen > Delta ->
|
|
||||||
lists:sublist(Msg, MsgLen - Delta);
|
|
||||||
true ->
|
|
||||||
""
|
|
||||||
end,
|
|
||||||
make_payload(State, CutMsg, Badge, Sound, Sender);
|
|
||||||
true ->
|
|
||||||
Payload2 =
|
|
||||||
"{\"aps\":{" ++ join(Payloads, ",") ++ "}}",
|
|
||||||
%PayloadLen2 = length(Payload2),
|
|
||||||
Payload2
|
|
||||||
end;
|
|
||||||
true ->
|
|
||||||
Payload
|
|
||||||
end.
|
|
||||||
|
|
||||||
connect(#state{socket = undefined} = State) ->
|
|
||||||
Gateway = State#state.gateway,
|
|
||||||
Port = State#state.port,
|
|
||||||
CertFile = State#state.certfile,
|
|
||||||
case ssl:connect(Gateway, Port, [{certfile, CertFile},
|
|
||||||
{active, true},
|
|
||||||
binary]) of
|
|
||||||
{ok, Socket} ->
|
|
||||||
{noreply, resend_messages(State#state{socket = Socket})};
|
|
||||||
{error, Reason} ->
|
|
||||||
?ERROR_MSG("(~p) Connection to ~p:~p failed: ~p, "
|
|
||||||
"retrying after ~p seconds",
|
|
||||||
[State#state.host, Gateway, Port,
|
|
||||||
Reason, ?RECONNECT_TIMEOUT div 1000]),
|
|
||||||
erlang:send_after(?RECONNECT_TIMEOUT, self(), connect),
|
|
||||||
{noreply, State}
|
|
||||||
end;
|
|
||||||
connect(State) ->
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
bounce_message(From, To, Packet, Reason) ->
|
|
||||||
{xmlelement, _, Attrs, _} = Packet,
|
|
||||||
Type = xml:get_attr_s("type", Attrs),
|
|
||||||
if Type /= "error"; Type /= "result" ->
|
|
||||||
ejabberd_router:route(
|
|
||||||
To, From,
|
|
||||||
jlib:make_error_reply(
|
|
||||||
Packet,
|
|
||||||
?ERRT_INTERNAL_SERVER_ERROR(
|
|
||||||
xml:get_attr_s("xml:lang", Attrs),
|
|
||||||
Reason)));
|
|
||||||
true ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
queue_message(From, To, Packet, State) ->
|
|
||||||
case State#state.queue of
|
|
||||||
{?MAX_QUEUE_SIZE, Queue} ->
|
|
||||||
{{value, {From1, To1, Packet1}}, Queue1} = queue:out(Queue),
|
|
||||||
bounce_message(From1, To1, Packet1,
|
|
||||||
"Unable to connect to push service"),
|
|
||||||
Queue2 = queue:in({From, To, Packet}, Queue1),
|
|
||||||
State#state{queue = {?MAX_QUEUE_SIZE, Queue2}};
|
|
||||||
{Size, Queue} ->
|
|
||||||
Queue1 = queue:in({From, To, Packet}, Queue),
|
|
||||||
State#state{queue = {Size+1, Queue1}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
resend_messages(#state{queue = {_, Queue}} = State) ->
|
|
||||||
lists:foldl(
|
|
||||||
fun({From, To, Packet}, AccState) ->
|
|
||||||
case catch handle_message(From, To, Packet, AccState) of
|
|
||||||
{'EXIT', _} = Err ->
|
|
||||||
?ERROR_MSG("error while processing message:~n"
|
|
||||||
"** From: ~p~n"
|
|
||||||
"** To: ~p~n"
|
|
||||||
"** Packet: ~p~n"
|
|
||||||
"** Reason: ~p",
|
|
||||||
[From, To, Packet, Err]),
|
|
||||||
AccState;
|
|
||||||
NewAccState ->
|
|
||||||
NewAccState
|
|
||||||
end
|
|
||||||
end, State#state{queue = {0, queue:new()}}, queue:to_list(Queue)).
|
|
||||||
|
|
||||||
cache(JID, DeviceID, State) ->
|
|
||||||
CmdID = State#state.cmd_id,
|
|
||||||
Key = CmdID rem ?CACHE_SIZE,
|
|
||||||
C1 = State#state.cmd_cache,
|
|
||||||
D1 = State#state.device_cache,
|
|
||||||
D2 = case dict:find(Key, C1) of
|
|
||||||
{ok, {_, OldDeviceID}} ->
|
|
||||||
del_device_cache(D1, OldDeviceID);
|
|
||||||
error ->
|
|
||||||
D1
|
|
||||||
end,
|
|
||||||
D3 = add_device_cache(D2, DeviceID, JID),
|
|
||||||
C2 = dict:store(Key, {JID, DeviceID}, C1),
|
|
||||||
State#state{cmd_id = CmdID + 1,
|
|
||||||
cmd_cache = C2,
|
|
||||||
device_cache = D3}.
|
|
||||||
|
|
||||||
add_device_cache(DeviceCache, DeviceID, JID) ->
|
|
||||||
dict:update(
|
|
||||||
DeviceID,
|
|
||||||
fun({Counter, _}) -> {Counter + 1, JID} end,
|
|
||||||
{1, JID},
|
|
||||||
DeviceCache).
|
|
||||||
|
|
||||||
del_device_cache(DeviceCache, DeviceID) ->
|
|
||||||
case dict:find(DeviceID, DeviceCache) of
|
|
||||||
{ok, {Counter, JID}} ->
|
|
||||||
case Counter of
|
|
||||||
1 ->
|
|
||||||
dict:erase(DeviceID, DeviceCache);
|
|
||||||
_ ->
|
|
||||||
dict:store(DeviceID, {Counter - 1, JID}, DeviceCache)
|
|
||||||
end;
|
|
||||||
error ->
|
|
||||||
DeviceCache
|
|
||||||
end.
|
|
||||||
|
|
||||||
json_escape(S) ->
|
|
||||||
[case C of
|
|
||||||
$" -> "\\\"";
|
|
||||||
$\\ -> "\\\\";
|
|
||||||
_ when C < 16 -> ["\\u000", erlang:integer_to_list(C, 16)];
|
|
||||||
_ when C < 32 -> ["\\u00", erlang:integer_to_list(C, 16)];
|
|
||||||
_ -> C
|
|
||||||
end || C <- S].
|
|
||||||
|
|
||||||
join(List, Sep) ->
|
|
||||||
lists:foldr(fun(A, "") -> A;
|
|
||||||
(A, Acc) -> A ++ Sep ++ Acc
|
|
||||||
end, "", List).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
iq_disco(Lang) ->
|
|
||||||
[{xmlelement, "identity",
|
|
||||||
[{"category", "gateway"},
|
|
||||||
{"type", "apple"},
|
|
||||||
{"name", translate:translate(Lang, "Apple Push Service")}], []},
|
|
||||||
{xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []}].
|
|
||||||
|
|
||||||
|
|
||||||
parse_feedback_buf(Buf, State) ->
|
|
||||||
case Buf of
|
|
||||||
<<TimeStamp:32, IDLen:16, BDeviceID:IDLen/binary, Rest/binary>> ->
|
|
||||||
IDLen8 = IDLen * 8,
|
|
||||||
<<DeviceID:IDLen8>> = BDeviceID,
|
|
||||||
case dict:find(DeviceID, State#state.device_cache) of
|
|
||||||
{ok, {_Counter, JID}} ->
|
|
||||||
From = jlib:make_jid("", State#state.host, ""),
|
|
||||||
ejabberd_router:route(
|
|
||||||
From, JID,
|
|
||||||
{xmlelement, "message", [],
|
|
||||||
[{xmlelement, "disable",
|
|
||||||
[{"xmlns", ?NS_P1_PUSH},
|
|
||||||
{"status", "feedback"},
|
|
||||||
{"ts", integer_to_list(TimeStamp)}],
|
|
||||||
[]}]});
|
|
||||||
error ->
|
|
||||||
ok
|
|
||||||
end,
|
|
||||||
parse_feedback_buf(Rest, State);
|
|
||||||
_ ->
|
|
||||||
Buf
|
|
||||||
end.
|
|
|
@ -30,18 +30,19 @@
|
||||||
-behaviour(gen_mod).
|
-behaviour(gen_mod).
|
||||||
|
|
||||||
-export([start/2,
|
-export([start/2,
|
||||||
init/1,
|
loop/1,
|
||||||
stop/1,
|
stop/1,
|
||||||
store_packet/3,
|
store_packet/3,
|
||||||
resend_offline_messages/2,
|
resend_offline_messages/2,
|
||||||
pop_offline_messages/3,
|
pop_offline_messages/3,
|
||||||
|
get_sm_features/5,
|
||||||
remove_expired_messages/0,
|
remove_expired_messages/0,
|
||||||
remove_old_messages/1,
|
remove_old_messages/1,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
|
get_queue_length/2,
|
||||||
webadmin_page/3,
|
webadmin_page/3,
|
||||||
webadmin_user/4,
|
webadmin_user/4,
|
||||||
webadmin_user_parse_query/5,
|
webadmin_user_parse_query/5]).
|
||||||
count_offline_messages/3]).
|
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
-include("jlib.hrl").
|
-include("jlib.hrl").
|
||||||
|
@ -53,6 +54,9 @@
|
||||||
-define(PROCNAME, ejabberd_offline).
|
-define(PROCNAME, ejabberd_offline).
|
||||||
-define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000).
|
-define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000).
|
||||||
|
|
||||||
|
%% default value for the maximum number of user messages
|
||||||
|
-define(MAX_USER_MESSAGES, infinity).
|
||||||
|
|
||||||
start(Host, Opts) ->
|
start(Host, Opts) ->
|
||||||
mnesia:create_table(offline_msg,
|
mnesia:create_table(offline_msg,
|
||||||
[{disc_only_copies, [node()]},
|
[{disc_only_copies, [node()]},
|
||||||
|
@ -67,30 +71,28 @@ start(Host, Opts) ->
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
ejabberd_hooks:add(anonymous_purge_hook, Host,
|
ejabberd_hooks:add(anonymous_purge_hook, Host,
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
|
ejabberd_hooks:add(disco_sm_features, Host,
|
||||||
|
?MODULE, get_sm_features, 50),
|
||||||
|
ejabberd_hooks:add(disco_local_features, Host,
|
||||||
|
?MODULE, get_sm_features, 50),
|
||||||
ejabberd_hooks:add(webadmin_page_host, Host,
|
ejabberd_hooks:add(webadmin_page_host, Host,
|
||||||
?MODULE, webadmin_page, 50),
|
?MODULE, webadmin_page, 50),
|
||||||
ejabberd_hooks:add(webadmin_user, Host,
|
ejabberd_hooks:add(webadmin_user, Host,
|
||||||
?MODULE, webadmin_user, 50),
|
?MODULE, webadmin_user, 50),
|
||||||
ejabberd_hooks:add(webadmin_user_parse_query, Host,
|
ejabberd_hooks:add(webadmin_user_parse_query, Host,
|
||||||
?MODULE, webadmin_user_parse_query, 50),
|
?MODULE, webadmin_user_parse_query, 50),
|
||||||
ejabberd_hooks:add(count_offline_messages, Host,
|
AccessMaxOfflineMsgs = gen_mod:get_opt(access_max_user_messages, Opts, max_user_offline_messages),
|
||||||
?MODULE, count_offline_messages, 50),
|
|
||||||
MaxOfflineMsgs = gen_mod:get_opt(user_max_messages, Opts, infinity),
|
|
||||||
register(gen_mod:get_module_proc(Host, ?PROCNAME),
|
register(gen_mod:get_module_proc(Host, ?PROCNAME),
|
||||||
spawn(?MODULE, init, [MaxOfflineMsgs])).
|
spawn(?MODULE, loop, [AccessMaxOfflineMsgs])).
|
||||||
|
|
||||||
%% MaxOfflineMsgs is either infinity of integer > 0
|
loop(AccessMaxOfflineMsgs) ->
|
||||||
init(infinity) ->
|
|
||||||
loop(infinity);
|
|
||||||
init(MaxOfflineMsgs)
|
|
||||||
when is_integer(MaxOfflineMsgs), MaxOfflineMsgs > 0 ->
|
|
||||||
loop(MaxOfflineMsgs).
|
|
||||||
|
|
||||||
loop(MaxOfflineMsgs) ->
|
|
||||||
receive
|
receive
|
||||||
#offline_msg{us=US} = Msg ->
|
#offline_msg{us=US} = Msg ->
|
||||||
Msgs = receive_all(US, [Msg]),
|
Msgs = receive_all(US, [Msg]),
|
||||||
Len = length(Msgs),
|
Len = length(Msgs),
|
||||||
|
{User, Host} = US,
|
||||||
|
MaxOfflineMsgs = get_max_user_messages(AccessMaxOfflineMsgs,
|
||||||
|
User, Host),
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
%% Only count messages if needed:
|
%% Only count messages if needed:
|
||||||
Count = if MaxOfflineMsgs =/= infinity ->
|
Count = if MaxOfflineMsgs =/= infinity ->
|
||||||
|
@ -116,9 +118,18 @@ loop(MaxOfflineMsgs) ->
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
mnesia:transaction(F),
|
mnesia:transaction(F),
|
||||||
loop(MaxOfflineMsgs);
|
loop(AccessMaxOfflineMsgs);
|
||||||
_ ->
|
_ ->
|
||||||
loop(MaxOfflineMsgs)
|
loop(AccessMaxOfflineMsgs)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Function copied from ejabberd_sm.erl:
|
||||||
|
get_max_user_messages(AccessRule, LUser, Host) ->
|
||||||
|
case acl:match_rule(
|
||||||
|
Host, AccessRule, jlib:make_jid(LUser, Host, "")) of
|
||||||
|
Max when is_integer(Max) -> Max;
|
||||||
|
infinity -> infinity;
|
||||||
|
_ -> ?MAX_USER_MESSAGES
|
||||||
end.
|
end.
|
||||||
|
|
||||||
receive_all(US, Msgs) ->
|
receive_all(US, Msgs) ->
|
||||||
|
@ -139,6 +150,8 @@ stop(Host) ->
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
ejabberd_hooks:delete(anonymous_purge_hook, Host,
|
ejabberd_hooks:delete(anonymous_purge_hook, Host,
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
|
ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
|
||||||
|
ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_sm_features, 50),
|
||||||
ejabberd_hooks:delete(webadmin_page_host, Host,
|
ejabberd_hooks:delete(webadmin_page_host, Host,
|
||||||
?MODULE, webadmin_page, 50),
|
?MODULE, webadmin_page, 50),
|
||||||
ejabberd_hooks:delete(webadmin_user, Host,
|
ejabberd_hooks:delete(webadmin_user, Host,
|
||||||
|
@ -149,12 +162,27 @@ stop(Host) ->
|
||||||
exit(whereis(Proc), stop),
|
exit(whereis(Proc), stop),
|
||||||
{wait, Proc}.
|
{wait, Proc}.
|
||||||
|
|
||||||
|
get_sm_features(Acc, _From, _To, "", _Lang) ->
|
||||||
|
Feats = case Acc of
|
||||||
|
{result, I} -> I;
|
||||||
|
_ -> []
|
||||||
|
end,
|
||||||
|
{result, Feats ++ [?NS_FEATURE_MSGOFFLINE]};
|
||||||
|
|
||||||
|
get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) ->
|
||||||
|
%% override all lesser features...
|
||||||
|
{result, []};
|
||||||
|
|
||||||
|
get_sm_features(Acc, _From, _To, _Node, _Lang) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
|
|
||||||
store_packet(From, To, Packet) ->
|
store_packet(From, To, Packet) ->
|
||||||
Type = xml:get_tag_attr_s("type", Packet),
|
Type = xml:get_tag_attr_s("type", Packet),
|
||||||
if
|
if
|
||||||
(Type /= "error") and (Type /= "groupchat") and
|
(Type /= "error") and (Type /= "groupchat") and
|
||||||
(Type /= "headline") ->
|
(Type /= "headline") ->
|
||||||
case check_event(From, To, Packet) of
|
case check_event_chatstates(From, To, Packet) of
|
||||||
true ->
|
true ->
|
||||||
#jid{luser = LUser, lserver = LServer} = To,
|
#jid{luser = LUser, lserver = LServer} = To,
|
||||||
TimeStamp = now(),
|
TimeStamp = now(),
|
||||||
|
@ -175,12 +203,22 @@ store_packet(From, To, Packet) ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_event(From, To, Packet) ->
|
%% Check if the packet has any content about XEP-0022 or XEP-0085
|
||||||
|
check_event_chatstates(From, To, Packet) ->
|
||||||
{xmlelement, Name, Attrs, Els} = Packet,
|
{xmlelement, Name, Attrs, Els} = Packet,
|
||||||
case find_x_event(Els) of
|
case find_x_event_chatstates(Els, {false, false, false}) of
|
||||||
false ->
|
%% There wasn't any x:event or chatstates subelements
|
||||||
|
{false, false, _} ->
|
||||||
true;
|
true;
|
||||||
El ->
|
%% There a chatstates subelement and other stuff, but no x:event
|
||||||
|
{false, CEl, true} when CEl /= false ->
|
||||||
|
true;
|
||||||
|
%% There was only a subelement: a chatstates
|
||||||
|
{false, CEl, false} when CEl /= false ->
|
||||||
|
%% Don't allow offline storage
|
||||||
|
false;
|
||||||
|
%% There was an x:event element, and maybe also other stuff
|
||||||
|
{El, _, _} when El /= false ->
|
||||||
case xml:get_subtag(El, "id") of
|
case xml:get_subtag(El, "id") of
|
||||||
false ->
|
false ->
|
||||||
case xml:get_subtag(El, "offline") of
|
case xml:get_subtag(El, "offline") of
|
||||||
|
@ -208,16 +246,19 @@ check_event(From, To, Packet) ->
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_x_event([]) ->
|
%% Check if the packet has subelements about XEP-0022, XEP-0085 or other
|
||||||
false;
|
find_x_event_chatstates([], Res) ->
|
||||||
find_x_event([{xmlcdata, _} | Els]) ->
|
Res;
|
||||||
find_x_event(Els);
|
find_x_event_chatstates([{xmlcdata, _} | Els], Res) ->
|
||||||
find_x_event([El | Els]) ->
|
find_x_event_chatstates(Els, Res);
|
||||||
|
find_x_event_chatstates([El | Els], {A, B, C}) ->
|
||||||
case xml:get_tag_attr_s("xmlns", El) of
|
case xml:get_tag_attr_s("xmlns", El) of
|
||||||
?NS_EVENT ->
|
?NS_EVENT ->
|
||||||
El;
|
find_x_event_chatstates(Els, {El, B, C});
|
||||||
|
?NS_CHATSTATES ->
|
||||||
|
find_x_event_chatstates(Els, {A, El, C});
|
||||||
_ ->
|
_ ->
|
||||||
find_x_event(Els)
|
find_x_event_chatstates(Els, {A, B, true})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_x_expire(_, []) ->
|
find_x_expire(_, []) ->
|
||||||
|
@ -266,6 +307,13 @@ resend_offline_messages(User, Server) ->
|
||||||
{xmlelement, Name, Attrs,
|
{xmlelement, Name, Attrs,
|
||||||
Els ++
|
Els ++
|
||||||
[jlib:timestamp_to_xml(
|
[jlib:timestamp_to_xml(
|
||||||
|
calendar:now_to_universal_time(
|
||||||
|
R#offline_msg.timestamp),
|
||||||
|
utc,
|
||||||
|
jlib:make_jid("", Server, ""),
|
||||||
|
"Offline Storage"),
|
||||||
|
%% TODO: Delete the next three lines once XEP-0091 is Obsolete
|
||||||
|
jlib:timestamp_to_xml(
|
||||||
calendar:now_to_universal_time(
|
calendar:now_to_universal_time(
|
||||||
R#offline_msg.timestamp))]}}
|
R#offline_msg.timestamp))]}}
|
||||||
end,
|
end,
|
||||||
|
@ -295,7 +343,14 @@ pop_offline_messages(Ls, User, Server) ->
|
||||||
{xmlelement, Name, Attrs,
|
{xmlelement, Name, Attrs,
|
||||||
Els ++
|
Els ++
|
||||||
[jlib:timestamp_to_xml(
|
[jlib:timestamp_to_xml(
|
||||||
calendar:now_to_universal_time(
|
calendar:now_to_universal_time(
|
||||||
|
R#offline_msg.timestamp),
|
||||||
|
utc,
|
||||||
|
jlib:make_jid("", Server, ""),
|
||||||
|
"Offline Storage"),
|
||||||
|
%% TODO: Delete the next three lines once XEP-0091 is Obsolete
|
||||||
|
jlib:timestamp_to_xml(
|
||||||
|
calendar:now_to_universal_time(
|
||||||
R#offline_msg.timestamp))]}}
|
R#offline_msg.timestamp))]}}
|
||||||
end,
|
end,
|
||||||
lists:filter(
|
lists:filter(
|
||||||
|
@ -312,6 +367,7 @@ pop_offline_messages(Ls, User, Server) ->
|
||||||
Ls
|
Ls
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
remove_expired_messages() ->
|
remove_expired_messages() ->
|
||||||
TimeStamp = now(),
|
TimeStamp = now(),
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
|
@ -474,8 +530,9 @@ webadmin_page(Acc, _, _) -> Acc.
|
||||||
user_queue(User, Server, Query, Lang) ->
|
user_queue(User, Server, Query, Lang) ->
|
||||||
US = {jlib:nodeprep(User), jlib:nameprep(Server)},
|
US = {jlib:nodeprep(User), jlib:nameprep(Server)},
|
||||||
Res = user_queue_parse_query(US, Query),
|
Res = user_queue_parse_query(US, Query),
|
||||||
Msgs = lists:keysort(#offline_msg.timestamp,
|
MsgsAll = lists:keysort(#offline_msg.timestamp,
|
||||||
mnesia:dirty_read({offline_msg, US})),
|
mnesia:dirty_read({offline_msg, US})),
|
||||||
|
Msgs = get_messages_subset(User, Server, MsgsAll),
|
||||||
FMsgs =
|
FMsgs =
|
||||||
lists:map(
|
lists:map(
|
||||||
fun(#offline_msg{timestamp = TimeStamp, from = From, to = To,
|
fun(#offline_msg{timestamp = TimeStamp, from = From, to = To,
|
||||||
|
@ -557,9 +614,32 @@ user_queue_parse_query(US, Query) ->
|
||||||
us_to_list({User, Server}) ->
|
us_to_list({User, Server}) ->
|
||||||
jlib:jid_to_string({User, Server, ""}).
|
jlib:jid_to_string({User, Server, ""}).
|
||||||
|
|
||||||
|
get_queue_length(User, Server) ->
|
||||||
|
length(mnesia:dirty_read({offline_msg, {User, Server}})).
|
||||||
|
|
||||||
|
get_messages_subset(User, Host, MsgsAll) ->
|
||||||
|
Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages,
|
||||||
|
max_user_offline_messages),
|
||||||
|
MaxOfflineMsgs = case get_max_user_messages(Access, User, Host) of
|
||||||
|
Number when is_integer(Number) -> Number;
|
||||||
|
_ -> 100
|
||||||
|
end,
|
||||||
|
Length = length(MsgsAll),
|
||||||
|
get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll).
|
||||||
|
|
||||||
|
get_messages_subset2(Max, Length, MsgsAll) when Length =< Max*2 ->
|
||||||
|
MsgsAll;
|
||||||
|
get_messages_subset2(Max, Length, MsgsAll) ->
|
||||||
|
FirstN = Max,
|
||||||
|
{MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll),
|
||||||
|
MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2),
|
||||||
|
NoJID = jlib:make_jid("...", "...", ""),
|
||||||
|
IntermediateMsg = #offline_msg{timestamp = now(), from = NoJID, to = NoJID,
|
||||||
|
packet = {xmlelement, "...", [], []}},
|
||||||
|
MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN.
|
||||||
|
|
||||||
webadmin_user(Acc, User, Server, Lang) ->
|
webadmin_user(Acc, User, Server, Lang) ->
|
||||||
US = {jlib:nodeprep(User), jlib:nameprep(Server)},
|
QueueLen = get_queue_length(jlib:nodeprep(User), jlib:nameprep(Server)),
|
||||||
QueueLen = length(mnesia:dirty_read({offline_msg, US})),
|
|
||||||
FQueueLen = [?AC("queue/",
|
FQueueLen = [?AC("queue/",
|
||||||
integer_to_list(QueueLen))],
|
integer_to_list(QueueLen))],
|
||||||
Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")].
|
Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")].
|
||||||
|
@ -583,23 +663,3 @@ webadmin_user_parse_query(_, "removealloffline", User, Server, _Query) ->
|
||||||
end;
|
end;
|
||||||
webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) ->
|
webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) ->
|
||||||
Acc.
|
Acc.
|
||||||
|
|
||||||
|
|
||||||
%% ------------------------------------------------
|
|
||||||
%% mod_offline: number of messages quota management
|
|
||||||
|
|
||||||
count_offline_messages(_Acc, User, Server) ->
|
|
||||||
LUser = jlib:nodeprep(User),
|
|
||||||
LServer = jlib:nameprep(Server),
|
|
||||||
US = {LUser, LServer},
|
|
||||||
F = fun () ->
|
|
||||||
p1_mnesia:count_records(
|
|
||||||
offline_msg,
|
|
||||||
#offline_msg{us=US, _='_'})
|
|
||||||
end,
|
|
||||||
N = case catch mnesia:async_dirty(F) of
|
|
||||||
I when is_integer(I) -> I;
|
|
||||||
_ -> 0
|
|
||||||
end,
|
|
||||||
{stop, N}.
|
|
||||||
|
|
||||||
|
|
|
@ -32,15 +32,16 @@
|
||||||
-export([count_offline_messages/2]).
|
-export([count_offline_messages/2]).
|
||||||
|
|
||||||
-export([start/2,
|
-export([start/2,
|
||||||
init/2,
|
loop/2,
|
||||||
stop/1,
|
stop/1,
|
||||||
store_packet/3,
|
store_packet/3,
|
||||||
pop_offline_messages/3,
|
pop_offline_messages/3,
|
||||||
|
get_sm_features/5,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
|
get_queue_length/2,
|
||||||
webadmin_page/3,
|
webadmin_page/3,
|
||||||
webadmin_user/4,
|
webadmin_user/4,
|
||||||
webadmin_user_parse_query/5,
|
webadmin_user_parse_query/5]).
|
||||||
count_offline_messages/3]).
|
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
-include("jlib.hrl").
|
-include("jlib.hrl").
|
||||||
|
@ -52,6 +53,9 @@
|
||||||
-define(PROCNAME, ejabberd_offline).
|
-define(PROCNAME, ejabberd_offline).
|
||||||
-define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000).
|
-define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000).
|
||||||
|
|
||||||
|
%% default value for the maximum number of user messages
|
||||||
|
-define(MAX_USER_MESSAGES, infinity).
|
||||||
|
|
||||||
start(Host, Opts) ->
|
start(Host, Opts) ->
|
||||||
ejabberd_hooks:add(offline_message_hook, Host,
|
ejabberd_hooks:add(offline_message_hook, Host,
|
||||||
?MODULE, store_packet, 50),
|
?MODULE, store_packet, 50),
|
||||||
|
@ -61,30 +65,27 @@ start(Host, Opts) ->
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
ejabberd_hooks:add(anonymous_purge_hook, Host,
|
ejabberd_hooks:add(anonymous_purge_hook, Host,
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
|
ejabberd_hooks:add(disco_sm_features, Host,
|
||||||
|
?MODULE, get_sm_features, 50),
|
||||||
|
ejabberd_hooks:add(disco_local_features, Host,
|
||||||
|
?MODULE, get_sm_features, 50),
|
||||||
ejabberd_hooks:add(webadmin_page_host, Host,
|
ejabberd_hooks:add(webadmin_page_host, Host,
|
||||||
?MODULE, webadmin_page, 50),
|
?MODULE, webadmin_page, 50),
|
||||||
ejabberd_hooks:add(webadmin_user, Host,
|
ejabberd_hooks:add(webadmin_user, Host,
|
||||||
?MODULE, webadmin_user, 50),
|
?MODULE, webadmin_user, 50),
|
||||||
ejabberd_hooks:add(webadmin_user_parse_query, Host,
|
ejabberd_hooks:add(webadmin_user_parse_query, Host,
|
||||||
?MODULE, webadmin_user_parse_query, 50),
|
?MODULE, webadmin_user_parse_query, 50),
|
||||||
ejabberd_hooks:add(count_offline_messages, Host,
|
AccessMaxOfflineMsgs = gen_mod:get_opt(access_max_user_messages, Opts, max_user_offline_messages),
|
||||||
?MODULE, count_offline_messages, 50),
|
|
||||||
MaxOfflineMsgs = gen_mod:get_opt(user_max_messages, Opts, infinity),
|
|
||||||
register(gen_mod:get_module_proc(Host, ?PROCNAME),
|
register(gen_mod:get_module_proc(Host, ?PROCNAME),
|
||||||
spawn(?MODULE, init, [Host, MaxOfflineMsgs])).
|
spawn(?MODULE, loop, [Host, AccessMaxOfflineMsgs])).
|
||||||
|
|
||||||
%% MaxOfflineMsgs is either infinity of integer > 0
|
loop(Host, AccessMaxOfflineMsgs) ->
|
||||||
init(Host, infinity) ->
|
|
||||||
loop(Host, infinity);
|
|
||||||
init(Host, MaxOfflineMsgs)
|
|
||||||
when is_integer(MaxOfflineMsgs), MaxOfflineMsgs > 0 ->
|
|
||||||
loop(Host, MaxOfflineMsgs).
|
|
||||||
|
|
||||||
loop(Host, MaxOfflineMsgs) ->
|
|
||||||
receive
|
receive
|
||||||
#offline_msg{user = User} = Msg ->
|
#offline_msg{user = User} = Msg ->
|
||||||
Msgs = receive_all(User, [Msg]),
|
Msgs = receive_all(User, [Msg]),
|
||||||
Len = length(Msgs),
|
Len = length(Msgs),
|
||||||
|
MaxOfflineMsgs = get_max_user_messages(AccessMaxOfflineMsgs,
|
||||||
|
User, Host),
|
||||||
|
|
||||||
%% Only count existing messages if needed:
|
%% Only count existing messages if needed:
|
||||||
Count = if MaxOfflineMsgs =/= infinity ->
|
Count = if MaxOfflineMsgs =/= infinity ->
|
||||||
|
@ -112,11 +113,17 @@ loop(Host, MaxOfflineMsgs) ->
|
||||||
Els ++
|
Els ++
|
||||||
[jlib:timestamp_to_xml(
|
[jlib:timestamp_to_xml(
|
||||||
calendar:now_to_universal_time(
|
calendar:now_to_universal_time(
|
||||||
|
M#offline_msg.timestamp),
|
||||||
|
utc,
|
||||||
|
jlib:make_jid("", Host, ""),
|
||||||
|
"Offline Storage"),
|
||||||
|
%% TODO: Delete the next three lines once XEP-0091 is Obsolete
|
||||||
|
jlib:timestamp_to_xml(
|
||||||
|
calendar:now_to_universal_time(
|
||||||
M#offline_msg.timestamp))]},
|
M#offline_msg.timestamp))]},
|
||||||
XML =
|
XML =
|
||||||
ejabberd_odbc:escape(
|
ejabberd_odbc:escape(
|
||||||
lists:flatten(
|
xml:element_to_binary(Packet)),
|
||||||
xml:element_to_string(Packet))),
|
|
||||||
odbc_queries:add_spool_sql(Username, XML)
|
odbc_queries:add_spool_sql(Username, XML)
|
||||||
end, Msgs),
|
end, Msgs),
|
||||||
case catch odbc_queries:add_spool(Host, Query) of
|
case catch odbc_queries:add_spool(Host, Query) of
|
||||||
|
@ -128,9 +135,18 @@ loop(Host, MaxOfflineMsgs) ->
|
||||||
ok
|
ok
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
loop(Host, MaxOfflineMsgs);
|
loop(Host, AccessMaxOfflineMsgs);
|
||||||
_ ->
|
_ ->
|
||||||
loop(Host, MaxOfflineMsgs)
|
loop(Host, AccessMaxOfflineMsgs)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Function copied from ejabberd_sm.erl:
|
||||||
|
get_max_user_messages(AccessRule, LUser, Host) ->
|
||||||
|
case acl:match_rule(
|
||||||
|
Host, AccessRule, jlib:make_jid(LUser, Host, "")) of
|
||||||
|
Max when is_integer(Max) -> Max;
|
||||||
|
infinity -> infinity;
|
||||||
|
_ -> ?MAX_USER_MESSAGES
|
||||||
end.
|
end.
|
||||||
|
|
||||||
receive_all(Username, Msgs) ->
|
receive_all(Username, Msgs) ->
|
||||||
|
@ -151,6 +167,8 @@ stop(Host) ->
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
ejabberd_hooks:delete(anonymous_purge_hook, Host,
|
ejabberd_hooks:delete(anonymous_purge_hook, Host,
|
||||||
?MODULE, remove_user, 50),
|
?MODULE, remove_user, 50),
|
||||||
|
ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
|
||||||
|
ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_sm_features, 50),
|
||||||
ejabberd_hooks:delete(webadmin_page_host, Host,
|
ejabberd_hooks:delete(webadmin_page_host, Host,
|
||||||
?MODULE, webadmin_page, 50),
|
?MODULE, webadmin_page, 50),
|
||||||
ejabberd_hooks:delete(webadmin_user, Host,
|
ejabberd_hooks:delete(webadmin_user, Host,
|
||||||
|
@ -161,12 +179,27 @@ stop(Host) ->
|
||||||
exit(whereis(Proc), stop),
|
exit(whereis(Proc), stop),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
get_sm_features(Acc, _From, _To, "", _Lang) ->
|
||||||
|
Feats = case Acc of
|
||||||
|
{result, I} -> I;
|
||||||
|
_ -> []
|
||||||
|
end,
|
||||||
|
{result, Feats ++ [?NS_FEATURE_MSGOFFLINE]};
|
||||||
|
|
||||||
|
get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) ->
|
||||||
|
%% override all lesser features...
|
||||||
|
{result, []};
|
||||||
|
|
||||||
|
get_sm_features(Acc, _From, _To, _Node, _Lang) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
|
|
||||||
store_packet(From, To, Packet) ->
|
store_packet(From, To, Packet) ->
|
||||||
Type = xml:get_tag_attr_s("type", Packet),
|
Type = xml:get_tag_attr_s("type", Packet),
|
||||||
if
|
if
|
||||||
(Type /= "error") and (Type /= "groupchat") and
|
(Type /= "error") and (Type /= "groupchat") and
|
||||||
(Type /= "headline") ->
|
(Type /= "headline") ->
|
||||||
case check_event(From, To, Packet) of
|
case check_event_chatstates(From, To, Packet) of
|
||||||
true ->
|
true ->
|
||||||
#jid{luser = LUser} = To,
|
#jid{luser = LUser} = To,
|
||||||
TimeStamp = now(),
|
TimeStamp = now(),
|
||||||
|
@ -187,12 +220,22 @@ store_packet(From, To, Packet) ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_event(From, To, Packet) ->
|
%% Check if the packet has any content about XEP-0022 or XEP-0085
|
||||||
|
check_event_chatstates(From, To, Packet) ->
|
||||||
{xmlelement, Name, Attrs, Els} = Packet,
|
{xmlelement, Name, Attrs, Els} = Packet,
|
||||||
case find_x_event(Els) of
|
case find_x_event_chatstates(Els, {false, false, false}) of
|
||||||
false ->
|
%% There wasn't any x:event or chatstates subelements
|
||||||
|
{false, false, _} ->
|
||||||
true;
|
true;
|
||||||
El ->
|
%% There a chatstates subelement and other stuff, but no x:event
|
||||||
|
{false, CEl, true} when CEl /= false ->
|
||||||
|
true;
|
||||||
|
%% There was only a subelement: a chatstates
|
||||||
|
{false, CEl, false} when CEl /= false ->
|
||||||
|
%% Don't allow offline storage
|
||||||
|
false;
|
||||||
|
%% There was an x:event element, and maybe also other stuff
|
||||||
|
{El, _, _} when El /= false ->
|
||||||
case xml:get_subtag(El, "id") of
|
case xml:get_subtag(El, "id") of
|
||||||
false ->
|
false ->
|
||||||
case xml:get_subtag(El, "offline") of
|
case xml:get_subtag(El, "offline") of
|
||||||
|
@ -214,22 +257,25 @@ check_event(From, To, Packet) ->
|
||||||
{xmlelement, "offline", [], []}]}]
|
{xmlelement, "offline", [], []}]}]
|
||||||
}),
|
}),
|
||||||
true
|
true
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_x_event([]) ->
|
%% Check if the packet has subelements about XEP-0022, XEP-0085 or other
|
||||||
false;
|
find_x_event_chatstates([], Res) ->
|
||||||
find_x_event([{xmlcdata, _} | Els]) ->
|
Res;
|
||||||
find_x_event(Els);
|
find_x_event_chatstates([{xmlcdata, _} | Els], Res) ->
|
||||||
find_x_event([El | Els]) ->
|
find_x_event_chatstates(Els, Res);
|
||||||
|
find_x_event_chatstates([El | Els], {A, B, C}) ->
|
||||||
case xml:get_tag_attr_s("xmlns", El) of
|
case xml:get_tag_attr_s("xmlns", El) of
|
||||||
?NS_EVENT ->
|
?NS_EVENT ->
|
||||||
El;
|
find_x_event_chatstates(Els, {El, B, C});
|
||||||
|
?NS_CHATSTATES ->
|
||||||
|
find_x_event_chatstates(Els, {A, El, C});
|
||||||
_ ->
|
_ ->
|
||||||
find_x_event(Els)
|
find_x_event_chatstates(Els, {A, B, true})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_x_expire(_, []) ->
|
find_x_expire(_, []) ->
|
||||||
|
@ -329,7 +375,7 @@ user_queue(User, Server, Query, Lang) ->
|
||||||
Username = ejabberd_odbc:escape(LUser),
|
Username = ejabberd_odbc:escape(LUser),
|
||||||
US = {LUser, LServer},
|
US = {LUser, LServer},
|
||||||
Res = user_queue_parse_query(Username, LServer, Query),
|
Res = user_queue_parse_query(Username, LServer, Query),
|
||||||
Msgs = case catch ejabberd_odbc:sql_query(
|
MsgsAll = case catch ejabberd_odbc:sql_query(
|
||||||
LServer,
|
LServer,
|
||||||
["select username, xml from spool"
|
["select username, xml from spool"
|
||||||
" where username='", Username, "'"
|
" where username='", Username, "'"
|
||||||
|
@ -347,6 +393,7 @@ user_queue(User, Server, Query, Lang) ->
|
||||||
_ ->
|
_ ->
|
||||||
[]
|
[]
|
||||||
end,
|
end,
|
||||||
|
Msgs = get_messages_subset(User, Server, MsgsAll),
|
||||||
FMsgs =
|
FMsgs =
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({xmlelement, _Name, _Attrs, _Els} = Msg) ->
|
fun({xmlelement, _Name, _Attrs, _Els} = Msg) ->
|
||||||
|
@ -433,11 +480,8 @@ user_queue_parse_query(Username, LServer, Query) ->
|
||||||
us_to_list({User, Server}) ->
|
us_to_list({User, Server}) ->
|
||||||
jlib:jid_to_string({User, Server, ""}).
|
jlib:jid_to_string({User, Server, ""}).
|
||||||
|
|
||||||
webadmin_user(Acc, User, Server, Lang) ->
|
get_queue_length(Username, LServer) ->
|
||||||
LUser = jlib:nodeprep(User),
|
case catch ejabberd_odbc:sql_query(
|
||||||
LServer = jlib:nameprep(Server),
|
|
||||||
Username = ejabberd_odbc:escape(LUser),
|
|
||||||
QueueLen = case catch ejabberd_odbc:sql_query(
|
|
||||||
LServer,
|
LServer,
|
||||||
["select count(*) from spool"
|
["select count(*) from spool"
|
||||||
" where username='", Username, "';"]) of
|
" where username='", Username, "';"]) of
|
||||||
|
@ -445,7 +489,32 @@ webadmin_user(Acc, User, Server, Lang) ->
|
||||||
SCount;
|
SCount;
|
||||||
_ ->
|
_ ->
|
||||||
0
|
0
|
||||||
end,
|
end.
|
||||||
|
|
||||||
|
get_messages_subset(User, Host, MsgsAll) ->
|
||||||
|
Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages,
|
||||||
|
max_user_offline_messages),
|
||||||
|
MaxOfflineMsgs = case get_max_user_messages(Access, User, Host) of
|
||||||
|
Number when is_integer(Number) -> Number;
|
||||||
|
_ -> 100
|
||||||
|
end,
|
||||||
|
Length = length(MsgsAll),
|
||||||
|
get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll).
|
||||||
|
|
||||||
|
get_messages_subset2(Max, Length, MsgsAll) when Length =< Max*2 ->
|
||||||
|
MsgsAll;
|
||||||
|
get_messages_subset2(Max, Length, MsgsAll) ->
|
||||||
|
FirstN = Max,
|
||||||
|
{MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll),
|
||||||
|
MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2),
|
||||||
|
IntermediateMsg = {xmlelement, "...", [], []},
|
||||||
|
MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN.
|
||||||
|
|
||||||
|
webadmin_user(Acc, User, Server, Lang) ->
|
||||||
|
LUser = jlib:nodeprep(User),
|
||||||
|
LServer = jlib:nameprep(Server),
|
||||||
|
Username = ejabberd_odbc:escape(LUser),
|
||||||
|
QueueLen = get_queue_length(Username, LServer),
|
||||||
FQueueLen = [?AC("queue/", QueueLen)],
|
FQueueLen = [?AC("queue/", QueueLen)],
|
||||||
Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")].
|
Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")].
|
||||||
|
|
||||||
|
@ -477,30 +546,3 @@ count_offline_messages(LUser, LServer) ->
|
||||||
_ ->
|
_ ->
|
||||||
0
|
0
|
||||||
end.
|
end.
|
||||||
|
|
||||||
count_offline_messages(_Acc, User, Server) ->
|
|
||||||
LUser = jlib:nodeprep(User),
|
|
||||||
LServer = jlib:nameprep(Server),
|
|
||||||
Num = case catch ejabberd_odbc:sql_query(
|
|
||||||
LServer,
|
|
||||||
["select xml from spool"
|
|
||||||
" where username='", LUser, "';"]) of
|
|
||||||
{selected, ["xml"], Rs} ->
|
|
||||||
lists:foldl(
|
|
||||||
fun({XML}, Acc) ->
|
|
||||||
case xml_stream:parse_element(XML) of
|
|
||||||
{error, _Reason} ->
|
|
||||||
Acc;
|
|
||||||
El ->
|
|
||||||
case xml:get_subtag(El, "body") of
|
|
||||||
false ->
|
|
||||||
Acc;
|
|
||||||
_ ->
|
|
||||||
Acc + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end, 0, Rs);
|
|
||||||
_ ->
|
|
||||||
0
|
|
||||||
end,
|
|
||||||
{stop, Num}.
|
|
||||||
|
|
26
src/xml.erl
26
src/xml.erl
|
@ -31,7 +31,6 @@
|
||||||
element_to_binary/1,
|
element_to_binary/1,
|
||||||
crypt/1, make_text_node/1,
|
crypt/1, make_text_node/1,
|
||||||
remove_cdata/1,
|
remove_cdata/1,
|
||||||
remove_subtags/3,
|
|
||||||
get_cdata/1, get_tag_cdata/1,
|
get_cdata/1, get_tag_cdata/1,
|
||||||
get_attr/2, get_attr_s/2,
|
get_attr/2, get_attr_s/2,
|
||||||
get_tag_attr/2, get_tag_attr_s/2,
|
get_tag_attr/2, get_tag_attr_s/2,
|
||||||
|
@ -187,31 +186,6 @@ remove_cdata_p(_) -> false.
|
||||||
|
|
||||||
remove_cdata(L) -> [E || E <- L, remove_cdata_p(E)].
|
remove_cdata(L) -> [E || E <- L, remove_cdata_p(E)].
|
||||||
|
|
||||||
%% TODO: Make more generic.
|
|
||||||
%% For now only support all parameters:
|
|
||||||
%% xml:remove_subtags({xmlelement,"message", [{"id","81be72"}],[{xmlelement,"on-sender-server",[{"xmlns","urn:xmpp:receipts"},{"server","text-one.com"}], []}]}, "on-sender-server", {"xmlns","urn:xmpp:receipts"}).
|
|
||||||
remove_subtags({xmlelement, TagName, TagAttrs, Els}, Name, Attr) ->
|
|
||||||
{xmlelement, TagName, TagAttrs, remove_subtags1(Els, [], Name, Attr)}.
|
|
||||||
|
|
||||||
remove_subtags1([], NewEls, _Name, _Attr) ->
|
|
||||||
lists:reverse(NewEls);
|
|
||||||
remove_subtags1([El | Els], NewEls, Name, {AttrName, AttrValue} = Attr) ->
|
|
||||||
case El of
|
|
||||||
{xmlelement, Name, Attrs, _} ->
|
|
||||||
case get_attr(AttrName, Attrs) of
|
|
||||||
false ->
|
|
||||||
remove_subtags1(Els, [El|NewEls], Name, Attr);
|
|
||||||
{value, AttrValue} ->
|
|
||||||
remove_subtags1(Els, NewEls, Name, Attr);
|
|
||||||
_ ->
|
|
||||||
remove_subtags1(Els, [El|NewEls], Name, Attr)
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
remove_subtags1(Els, [El|NewEls], Name, Attr)
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
get_cdata(L) ->
|
get_cdata(L) ->
|
||||||
binary_to_list(list_to_binary(get_cdata(L, ""))).
|
binary_to_list(list_to_binary(get_cdata(L, ""))).
|
||||||
|
|
||||||
|
|
|
@ -76,9 +76,6 @@ process_data(CallbackPid, Stack, Data) ->
|
||||||
{?XML_CDATA, CData} ->
|
{?XML_CDATA, CData} ->
|
||||||
case Stack of
|
case Stack of
|
||||||
[El] ->
|
[El] ->
|
||||||
catch gen_fsm:send_all_state_event(
|
|
||||||
CallbackPid,
|
|
||||||
{xmlstreamcdata, CData}),
|
|
||||||
[El];
|
[El];
|
||||||
%% Merge CDATA nodes if they are contiguous
|
%% Merge CDATA nodes if they are contiguous
|
||||||
%% This does not change the semantic: the split in
|
%% This does not change the semantic: the split in
|
||||||
|
|
Loading…
Reference in New Issue
Block a user