%%% TODO
%%% plugin: generate Reply (do not use broadcast atom anymore)
-%%% to be implemented: deliver_notifications
-module(mod_pubsub).
-version('1.10-01').
-define(PEPNODE, "pep").
%% exports for hooks
--export([set_presence/4,
- unset_presence/4,
- incoming_presence/3,
+-export([presence_probe/3,
remove_user/2,
disco_local_identity/5,
disco_local_features/5,
Host = gen_mod:get_opt_host(ServerHost, Opts, "pubsub.@HOST@"),
ServedHosts = gen_mod:get_opt(served_hosts, Opts, []),
Access = gen_mod:get_opt(access_createnode, Opts, all),
- mnesia:create_table(pubsub_presence,
- [{disc_copies, [node()]},
- {attributes, record_info(fields, pubsub_presence)}]),
mod_disco:register_feature(ServerHost, ?NS_PUBSUB),
ejabberd_hooks:add(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75),
ejabberd_hooks:add(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75),
ejabberd_hooks:add(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75),
ejabberd_hooks:add(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75),
ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75),
- ejabberd_hooks:add(incoming_presence_hook, ServerHost, ?MODULE, incoming_presence, 50),
- %%ejabberd_hooks:add(set_presence_hook, ServerHost, ?MODULE, set_presence, 50),
- %%ejabberd_hooks:add(unset_presence_hook, ServerHost, ?MODULE, unset_presence, 50),
+ ejabberd_hooks:add(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 50),
ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, remove_user, 50),
IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
lists:foreach(
ok.
init_nodes(Host, ServerHost, ServedHosts) ->
- create_node(Host, ServerHost, ["pubsub"], ?PUBSUB_JID, ?STDNODE),
- create_node(Host, ServerHost, ["pubsub", "nodes"], ?PUBSUB_JID, ?STDNODE),
- create_node(Host, ServerHost, ["home"], ?PUBSUB_JID, ?STDNODE),
+ create_node(Host, ServerHost, ["pubsub"], service_jid(Host), ?STDNODE),
+ create_node(Host, ServerHost, ["pubsub", "nodes"], service_jid(Host), ?STDNODE),
+ create_node(Host, ServerHost, ["home"], service_jid(Host), ?STDNODE),
lists:foreach(
fun(H) ->
- create_node(Host, ServerHost, ["home", H], ?PUBSUB_JID, ?STDNODE)
+ create_node(Host, ServerHost, ["home", H], service_jid(Host), ?STDNODE)
end, [ServerHost | ServedHosts]),
ok.
%% presence hooks handling functions
%%
-set_presence(User, Server, Resource, Presence) ->
- Proc = gen_mod:get_module_proc(Server, ?PROCNAME),
- gen_server:cast(Proc, {set_presence, User, Server, Resource, Presence}).
-unset_presence(User, Server, Resource, Status) ->
- Proc = gen_mod:get_module_proc(Server, ?PROCNAME),
- gen_server:cast(Proc, {unset_presence, User, Server, Resource, Status}).
-incoming_presence(From, #jid{lserver = Host} = To, Packet) ->
+presence_probe(#jid{lserver = Host} = From, To, Packet) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
gen_server:cast(Proc, {presence, From, To, Packet}).
%%--------------------------------------------------------------------
%% @private
handle_cast({presence, From, To, Packet}, State) ->
- Plugins = State#state.plugins,
- Key = {To#jid.lserver, From#jid.luser, From#jid.lserver},
- Record = #pubsub_presence{key = Key, resource = From#jid.lresource},
- Priority = case xml:get_subtag(Packet, "priority") of
- false -> 0;
- SubEl ->
- case catch list_to_integer(xml:get_tag_cdata(SubEl)) of
- P when is_integer(P) -> P;
- _ -> 0
- end
- end,
- PresenceType = case xml:get_tag_attr_s("type", Packet) of
- "" ->
- if Priority < 0 -> unavailable;
- true -> available
- end;
- "unavailable" -> unavailable;
- "error" -> unavailable;
- _ -> none
- end,
- PreviouslyAvailable =
- lists:member(From#jid.lresource,
- lists:map(fun(#pubsub_presence{resource = R}) -> R end,
- case catch mnesia:dirty_read(pubsub_presence, Key) of
- Result when is_list(Result) -> Result;
- _ -> []
- end)),
- case PresenceType of
- available -> mnesia:dirty_write(Record);
- unavailable -> mnesia:dirty_delete_object(Record);
- _ -> ok
- end,
- if PreviouslyAvailable == false,
- PresenceType == available ->
- %% A new resource is available. Loop through all subscriptions
- %% and if the node is so configured, send the last item.
- JID = jlib:jid_tolower(From),
- Host = State#state.host,
- ServerHost = State#state.server_host,
+ %% A new resource is available. send last published items
+ JID = jlib:jid_tolower(From),
+ Host = State#state.host,
+ ServerHost = State#state.server_host,
+ if From == To ->
+ %% for each node From is subscribed to
+ %% and if the node is so configured, send the last published item to From
+ lists:foreach(fun(Type) ->
+ {result, Subscriptions} = node_action(Type, get_entity_subscriptions, [Host, From]),
lists:foreach(
- fun(Type) ->
- {result, Subscriptions} = node_action(Type, get_entity_subscriptions, [Host, From]),
- Options = node_options(Type),
- SendLast = get_option(Options, send_last_published_item),
- AccessModel = get_option(Options, access_model),
- AllowedGroups = get_option(Options, roster_groups_allowed),
- lists:foreach(
- fun({Node, Subscription}) ->
- if
- Subscription /= none, Subscription /= pending, SendLast == on_sub_and_presence ->
+ fun({Node, subscribed}) ->
+ case tree_action(Host, get_node, [Host, Node]) of
+ #pubsub_node{options = Options} ->
+ case get_option(Options, send_last_published_item) of
+ on_sub_and_presence ->
send_last_item(Host, Node, JID);
- SendLast == on_sub_and_presence ->
- {PresenceSubscription, RosterGroup} = get_roster_info(
- To#jid.luser, To#jid.lserver, JID, AllowedGroups),
- Features = case catch mod_caps:get_features(ServerHost, mod_caps:read_caps(element(4, Packet))) of
- F when is_list(F) -> F;
- _ -> []
- end,
- case lists:member(Node ++ "+notify", Features) of
- true ->
- MaySubscribe =
- case AccessModel of
- open -> true;
- presence -> PresenceSubscription;
- whitelist -> false; % subscribers are added manually
- authorize -> false; % likewise
- roster -> RosterGroup
- end,
- if MaySubscribe ->
- send_last_item(Host, Node, JID);
- true ->
- ok
- end;
- false ->
- ok
- end;
- true ->
+ _ ->
ok
end;
- (_) ->
+ _ ->
ok
- end, Subscriptions)
- end, Plugins),
- {noreply, State};
- true ->
- {noreply, State}
- end;
-
-handle_cast({set_presence, _User, _Server, _Resource, _Presence}, State) ->
- {noreply, State};
-handle_cast({unset_presence, _User, _Server, _Resource, _Status}, State) ->
+ end;
+ (_) ->
+ ok
+ end, Subscriptions)
+ end, State#state.plugins);
+ true ->
+ ok
+ end,
+ %% and send to From last PEP events published by To
+ PepKey = jlib:jid_tolower(jlib:jid_remove_resource(To)),
+ Caps = mod_caps:read_caps(element(4, Packet)),
+ lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, options = Options}) ->
+ case get_option(Options, send_last_published_item) of
+ on_sub_and_presence ->
+ case is_caps_notify(ServerHost, Node, Caps) of
+ true ->
+ AllowedGroups = get_option(Options, roster_groups_allowed),
+ {PresenceSubscription, RosterGroup} = get_roster_info(
+ To#jid.luser, To#jid.lserver, JID, AllowedGroups),
+ Subscribed = case get_option(Options, access_model) of
+ open -> true;
+ presence -> PresenceSubscription;
+ whitelist -> false; % subscribers are added manually
+ authorize -> false; % likewise
+ roster -> RosterGroup
+ end,
+ if Subscribed ->
+ send_last_item(PepKey, Node, JID);
+ true ->
+ ok
+ end;
+ false ->
+ ok
+ end;
+ _ ->
+ ok
+ end
+ end, tree_action(Host, get_nodes, [PepKey])),
{noreply, State};
handle_cast({remove, User, Server}, State) ->
Owner = jlib:make_jid(User, Server, ""),
- delete_nodes(Server, Owner, State#state.plugins),
+ delete_nodes(Server, Owner),
{noreply, State};
handle_cast(_Msg, State) ->
ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75),
ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75),
ejabberd_hooks:delete(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75),
- ejabberd_hooks:delete(incoming_presence_hook, ServerHost, ?MODULE, incoming_presence, 50),
- %%ejabberd_hooks:delete(set_presence_hook, ServerHost, ?MODULE, set_presence, 50),
- %%ejabberd_hooks:delete(unset_presence_hook, ServerHost, ?MODULE, unset_presence, 50),
+ ejabberd_hooks:delete(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 50),
ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50),
lists:foreach(fun({NS,Mod}) ->
gen_iq_handler:remove_iq_handler(Mod, ServerHost, NS)
Res = IQ#iq{type = result,
sub_el = [{xmlelement, "vCard", [{"xmlns", XMLNS}],
iq_get_vcard(Lang)}]},
- ejabberd_router:route(To,
- From,
- jlib:iq_to_xml(Res));
+ ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
#iq{} ->
Err = jlib:make_error_reply(
Packet,
ok
end;
"presence" ->
- incoming_presence(From, To, Packet),
+ presence_probe(From, To, Packet),
ok;
"message" ->
case xml:get_attr_s("type", Attrs) of
#pubsub_node{owners = Owners} ->
lists:foreach(
fun(Owner) ->
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(Owner),
- Stanza)
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza}
end, Owners),
ok;
_ ->
end,
case transaction(Host, Node, Action, sync_dirty) of
{error, Error} ->
- ejabberd_router:route(To, From,
- jlib:make_error_reply(Packet, Error));
+ ejabberd_router:route(
+ To, From,
+ jlib:make_error_reply(Packet, Error));
{result, _NewSubscription} ->
%% XXX: notify about subscription state change, section 12.11
ok;
{result, Result} ->
{result, Result}
end.
-delete_nodes(Host, Owner, Plugins) ->
+delete_nodes(Host, Owner) ->
%% This removes only PEP nodes when user is removed
OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)),
lists:foreach(fun(#pubsub_node{nodeid={NodeKey, NodeName}}) ->
delete_node(NodeKey, NodeName, Owner)
end, tree_action(Host, get_nodes, [OwnerKey])).
-% TODO: may be best to use the generic following code
-% lists:foreach(fun(Type) ->
-% {result, Affiliations} = node_action(Type, get_entity_affiliations, [Host, Owner]),
-% NodeKey = case Type of
-% ?PEPNODE -> OwnerKey;
-% _ -> Host
-% end,
-% lists:foreach(
-% fun({NodeId, owner}) -> delete_node(NodeKey, NodeId, Owner);
-% (_) -> ok
-% end
-% end, Affiliations)
-% end, Plugins),
%% @spec (Host, Node, From, JID) ->
%% {error, Reason::stanzaError()} |
{error, Error} ->
{error, Error};
{result, {Result, subscribed, send_last}} ->
- %% TODO was send_last_published_item
send_all_items(Host, Node, Subscriber),
case Result of
default -> {result, Reply(subscribed)};
node_call(Type, publish_item, [Host, Node, Publisher, Model, MaxItems, ItemId, Payload])
end
end,
- %%ejabberd_hooks:run(pubsub_publish_item, Host, [Host, Node, JID, ?PUBSUB_JID, ItemId, Payload]),
+ %%ejabberd_hooks:run(pubsub_publish_item, Host, [Host, Node, JID, service_jid(Host), ItemId, Payload]),
Reply = [],
case transaction(Host, Node, Action, sync_dirty) of
{error, ?ERR_ITEM_NOT_FOUND} ->
[{xmlelement, "x", [{"xmlns", ?NS_PUBSUB_EVENT}],
[{xmlelement, "items", [{"node", node_to_string(Node)}],
ItemsEls}]}]},
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(LJID), Stanza).
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza}.
%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response}
%% Host = host()
string_to_node(SNode) ->
string:tokens(SNode, "/").
-%% @spec (Host, JID, PresenceDelivery) -> boolean()
+%% @spec (Host) -> jid()
%% Host = host()
-%% JID = jid()
+%% @doc <p>Generate pubsub service JID.</p>
+service_jid(Host) ->
+ case Host of
+ {U,S,_} -> {jid, U, S, "", U, S, ""};
+ _ -> {jid, "", Host, "", "", Host, ""}
+ end.
+
+%% @spec (LJID, PresenceDelivery) -> boolean()
+%% LJID = jid()
+%% Subscription = atom()
%% PresenceDelivery = boolean()
%% @doc <p>Check if a notification must be delivered or not
-to_be_delivered(_Host, _JID, false) ->
- % default is true
- true;
-to_be_delivered(Host, JID, true) ->
- case mnesia:dirty_read(pubsub_presence, {Host, element(1, JID), element(2, JID)}) of
- [_] -> true;
- [] -> false
+is_to_delivered(_, none, _) -> false;
+is_to_delivered(_, pending, _) -> false;
+is_to_delivered(_, _, false) -> true;
+is_to_delivered({User, Server, _}, _, true) ->
+ case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of
+ [] -> false;
+ Ss ->
+ lists:foldl(fun({session, _, _, _, undefined, _}, Acc) -> Acc;
+ ({session, _, _, _, _Priority, _}, _Acc) -> true
+ end, false, Ss)
end.
%%%%%% broadcast functions
[{xmlelement, "items", [{"node", node_to_string(Node)}],
[{xmlelement, "item", ItemAttrs, Content}]}]}]},
lists:foreach(
- fun(#pubsub_state{stateid = {JID, _},
+ fun(#pubsub_state{stateid = {LJID, _},
subscription = Subscription}) ->
- ToBeSent = to_be_delivered(Host, JID, PresenceDelivery),
- if
- (Subscription /= none) and
- (Subscription /= pending) and
- ToBeSent ->
+ case is_to_delivered(LJID, Subscription, PresenceDelivery) of
+ true ->
DestJIDs = case BroadcastAll of
- true -> ejabberd_sm:get_user_resources(element(1, JID), element(2, JID));
- false -> [JID]
+ true -> ejabberd_sm:get_user_resources(element(1, LJID), element(2, LJID));
+ false -> [LJID]
end,
lists:foreach(
fun(DestJID) ->
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(DestJID), Stanza)
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(DestJID), Stanza}
end, DestJIDs);
- true ->
+ false ->
ok
end
end, States),
subscription = Subscription}) ->
if (Subscription /= none) and
(Subscription /= pending) ->
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza);
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza};
true ->
ok
end
subscription = Subscription}) ->
if (Subscription /= none) and
(Subscription /= pending) ->
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza);
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza};
true ->
ok
end
subscription = Subscription}) ->
if (Subscription /= none) and
(Subscription /= pending) ->
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza);
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza};
true ->
ok
end
[{xmlelement, "item", [{"id", "configuration"}],
Content}]}]}]},
lists:foreach(
- fun(#pubsub_state{stateid = {JID, _},
+ fun(#pubsub_state{stateid = {LJID, _},
subscription = Subscription}) ->
- ToBeSent = to_be_delivered(Host, JID, PresenceDelivery),
- if (Subscription /= none) and
- (Subscription /= pending) and
- ToBeSent ->
- ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza);
- true ->
+ case is_to_delivered(LJID, Subscription, PresenceDelivery) of
+ true ->
+ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza};
+ false ->
ok
end
end, States),
broadcast_by_caps({LUser, LServer, LResource}, Node, _Type, Stanza) ->
?DEBUG("looking for pid of ~p@~p/~p", [LUser, LServer, LResource]),
%% We need to know the resource, so we can ask for presence data.
- SenderResources = ejabberd_sm:get_user_resources(LUser, LServer),
SenderResource = case LResource of
"" ->
%% If we don't know the resource, just pick one.
- case SenderResources of
+ case ejabberd_sm:get_user_resources(LUser, LServer) of
[R|_] ->
R;
[] ->
case catch ejabberd_c2s:get_subscribed_and_online(C2SPid) of
ContactsWithCaps when is_list(ContactsWithCaps) ->
?DEBUG("found contacts with caps: ~p", [ContactsWithCaps]),
- LookingFor = Node ++ "+notify",
lists:foreach(
fun({JID, Caps}) ->
- case catch mod_caps:get_features(LServer, Caps) of
- Features when is_list(Features) ->
- case lists:member(LookingFor, Features) of
- true ->
- To = jlib:make_jid(JID),
- ejabberd_router:route(Sender, To, Stanza);
- _ ->
- ok
- end;
- _ ->
- %% couldn't get entity capabilities.
- %% nothing to do about that...
- ok
- end
+ case is_caps_notify(LServer, Node, Caps) of
+ true ->
+ To = jlib:make_jid(JID),
+ ejabberd_router ! {route, Sender, To, Stanza};
+ false ->
+ ok
+ end
end, ContactsWithCaps);
_ ->
ok
end,
- %% also send a notification to any
- %% of the account owner's available resources.
- %% See: XEP-0163 1.1 section 3
- %% Note: ejabberd_c2s:get_subscribed_and_online already returns owner online resources
- %% this loop should be deleted
- % lists:foreach(fun(Resource) ->
- % To = jlib:make_jid(LUser, LServer, Resource),
- % ejabberd_router:route(Sender, To, Stanza)
- % end, SenderResources),
ok;
_ ->
ok
broadcast_by_caps(_, _, _, _) ->
ok.
+is_caps_notify(Host, Node, Caps) ->
+ case catch mod_caps:get_features(Host, Caps) of
+ Features when is_list(Features) -> lists:member(Node ++ "+notify", Features);
+ _ -> false
+ end.
+
%%%%%%% Configuration handling
%%<p>There are several reasons why the default node configuration options request might fail:</p>