]> granicus.if.org Git - ejabberd/commitdiff
Experimental MIX (XEP-0369) support
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Tue, 8 Mar 2016 17:04:29 +0000 (20:04 +0300)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Tue, 8 Mar 2016 17:04:29 +0000 (20:04 +0300)
include/ns.hrl
src/ejabberd_local.erl
src/ejabberd_sm.erl
src/mod_mix.erl [new file with mode: 0644]
src/mod_pubsub.erl
src/node_mix.erl [new file with mode: 0644]

index 6934195d957c0f6ea377f5c1caba2a5fa6c3f99a..c7f55637248004bd899471182a59d0ad30457346 100644 (file)
 -define(NS_HTTP_UPLOAD_OLD, <<"eu:siacs:conversations:http:upload">>).
 -define(NS_THUMBS_1, <<"urn:xmpp:thumbs:1">>).
 -define(NS_NICK,  <<"http://jabber.org/protocol/nick">>).
+-define(NS_MIX_0, <<"urn:xmpp:mix:0">>).
+-define(NS_MIX_SERVICEINFO_0, <<"urn:xmpp:mix:0#serviceinfo">>).
+-define(NS_MIX_NODES_MESSAGES, <<"urn:xmpp:mix:nodes:messages">>).
+-define(NS_MIX_NODES_PRESENCE, <<"urn:xmpp:mix:nodes:presence">>).
+-define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>).
+-define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>).
+-define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>).
index 1b7f93c77f110dc41cdde8c9a49c68a825de0ff3..292288a374b596737823d6ca2bde272bfaa5a1fd 100644 (file)
@@ -32,7 +32,7 @@
 %% API
 -export([start_link/0]).
 
--export([route/3, route_iq/4, route_iq/5,
+-export([route/3, route_iq/4, route_iq/5, process_iq/3,
         process_iq_reply/3, register_iq_handler/4,
         register_iq_handler/5, register_iq_response_handler/4,
         register_iq_response_handler/5, unregister_iq_handler/2,
index b2e5a21f3e98ba85231a6c8e0acb06d20ddb834f..218e657f3cde6a61b62bacee989c54246fe9af6a 100644 (file)
@@ -35,6 +35,7 @@
 -export([start/0,
         start_link/0,
         route/3,
+        process_iq/3,
         open_session/5,
         open_session/6,
         close_session/4,
diff --git a/src/mod_mix.erl b/src/mod_mix.erl
new file mode 100644 (file)
index 0000000..047ac8d
--- /dev/null
@@ -0,0 +1,329 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created :  2 Mar 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(mod_mix).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2, start/2, stop/1, process_iq/3,
+        disco_items/5, disco_identity/5, disco_info/5,
+        disco_features/5]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+        terminate/2, code_change/3]).
+
+-include("logger.hrl").
+-include("jlib.hrl").
+-include("pubsub.hrl").
+
+-define(PROCNAME, ejabberd_mod_mix).
+-define(NODES, [?NS_MIX_NODES_MESSAGES,
+               ?NS_MIX_NODES_PRESENCE,
+               ?NS_MIX_NODES_PARTICIPANTS,
+               ?NS_MIX_NODES_SUBJECT,
+               ?NS_MIX_NODES_CONFIG]).
+
+-record(state, {server_host :: binary(),
+               host :: binary()}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link(Host, Opts) ->
+    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+    gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+    ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
+                temporary, 5000, worker, [?MODULE]},
+    supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop(Host) ->
+    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+    supervisor:terminate_child(ejabberd_sup, Proc),
+    supervisor:delete_child(ejabberd_sup, Proc),
+    ok.
+
+disco_features(_Acc, _From, _To, _Node, _Lang) ->
+    {result, [?NS_MIX_0]}.
+
+disco_items(_Acc, _From, To, _Node, _Lang) when To#jid.luser /= <<"">> ->
+    To_s = jid:to_string(jid:remove_resource(To)),
+    {result, [#xmlel{name = <<"item">>,
+                    attrs = [{<<"jid">>, To_s},
+                             {<<"node">>, Node}]} || Node <- ?NODES]};
+disco_items(_Acc, _From, _To, _Node, _Lang) ->
+    {result, []}.
+
+disco_identity(Acc, _From, To, _Node, _Lang) when To#jid.luser == <<"">> ->
+    Acc ++ [#xmlel{name = <<"identity">>,
+                  attrs =
+                      [{<<"category">>, <<"conference">>},
+                       {<<"name">>, <<"MIX service">>},
+                       {<<"type">>, <<"text">>}]}];
+disco_identity(Acc, _From, _To, _Node, _Lang) ->
+    Acc ++ [#xmlel{name = <<"identity">>,
+                  attrs =
+                      [{<<"category">>, <<"conference">>},
+                       {<<"type">>, <<"mix">>}]}].
+
+disco_info(_Acc, _From, To, _Node, _Lang) when is_atom(To) ->
+    [#xmlel{name = <<"x">>,
+           attrs = [{<<"xmlns">>, ?NS_XDATA},
+                    {<<"type">>, <<"result">>}],
+           children = [#xmlel{name = <<"field">>,
+                              attrs = [{<<"var">>, <<"FORM_TYPE">>},
+                                       {<<"type">>, <<"hidden">>}],
+                              children = [#xmlel{name = <<"value">>,
+                                                 children = [{xmlcdata,
+                                                              ?NS_MIX_SERVICEINFO_0}]}]}]}];
+disco_info(Acc, _From, _To, _Node, _Lang) ->
+    Acc.
+
+process_iq(From, To,
+          #iq{type = set, sub_el = #xmlel{name = <<"join">>} = SubEl} = IQ) ->
+    Nodes = lists:flatmap(
+             fun(#xmlel{name = <<"subscribe">>, attrs = Attrs}) ->
+                     Node = fxml:get_attr_s(<<"node">>, Attrs),
+                     case lists:member(Node, ?NODES) of
+                         true -> [Node];
+                         false -> []
+                     end;
+                (_) ->
+                     []
+             end, SubEl#xmlel.children),
+    case subscribe_nodes(From, To, Nodes) of
+       {result, _} ->
+           case publish_participant(From, To) of
+               {result, _} ->
+                   LFrom_s = jid:to_string(jid:tolower(jid:remove_resource(From))),
+                   Subscribe = [#xmlel{name = <<"subscribe">>,
+                                       attrs = [{<<"node">>, Node}]} || Node <- Nodes],
+                   IQ#iq{type = result,
+                         sub_el = [#xmlel{name = <<"join">>,
+                                          attrs = [{<<"jid">>, LFrom_s},
+                                                   {<<"xmlns">>, ?NS_MIX_0}],
+                                          children = Subscribe}]};
+               {error, Err} ->
+                   IQ#iq{type = error, sub_el = [SubEl, Err]}
+           end;
+       {error, Err} ->
+           IQ#iq{type = error, sub_el = [SubEl, Err]}
+    end;
+process_iq(From, To,
+          #iq{type = set, sub_el = #xmlel{name = <<"leave">>} = SubEl} = IQ) ->
+    case delete_participant(From, To) of
+       {result, _} ->
+           case unsubscribe_nodes(From, To, ?NODES) of
+               {result, _} ->
+                   IQ#iq{type = result, sub_el = []};
+               {error, Err} ->
+                   IQ#iq{type = error, sub_el = [SubEl, Err]}
+           end;
+       {error, Err} ->
+           IQ#iq{type = error, sub_el = [SubEl, Err]}
+    end;
+process_iq(_From, _To, #iq{sub_el = SubEl} = IQ) ->
+    IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}.
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+init([ServerHost, Opts]) ->
+    Host = gen_mod:get_opt_host(ServerHost, Opts, <<"mix.@HOST@">>),
+    IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1,
+                             one_queue),
+    ConfigTab = gen_mod:get_module_proc(Host, config),
+    ets:new(ConfigTab, [named_table]),
+    ets:insert(ConfigTab, {plugins, [<<"mix">>]}),
+    ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 100),
+    ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 100),
+    ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 100),
+    ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, disco_items, 100),
+    ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_features, 100),
+    ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, disco_identity, 100),
+    ejabberd_hooks:add(disco_info, Host, ?MODULE, disco_info, 100),
+    gen_iq_handler:add_iq_handler(ejabberd_local, Host,
+                                 ?NS_DISCO_ITEMS, mod_disco,
+                                 process_local_iq_items, IQDisc),
+    gen_iq_handler:add_iq_handler(ejabberd_local, Host,
+                                 ?NS_DISCO_INFO, mod_disco,
+                                 process_local_iq_info, IQDisc),
+    gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
+                                 ?NS_DISCO_ITEMS, mod_disco,
+                                 process_local_iq_items, IQDisc),
+    gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
+                                 ?NS_DISCO_INFO, mod_disco,
+                                 process_local_iq_info, IQDisc),
+    gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
+                                 ?NS_PUBSUB, mod_pubsub, iq_sm, IQDisc),
+    gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
+                                 ?NS_MIX_0, ?MODULE, process_iq, IQDisc),
+    ejabberd_router:register_route(Host),
+    {ok, #state{server_host = ServerHost, host = Host}}.
+
+handle_call(_Request, _From, State) ->
+    Reply = ok,
+    {reply, Reply, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info({route, From, To, Packet}, State) ->
+    case catch do_route(State, From, To, Packet) of
+       {'EXIT', _} = Err ->
+           try
+               ?ERROR_MSG("failed to route packet ~p from '~s' to '~s': ~p",
+                          [Packet, jid:to_string(From), jid:to_string(To), Err]),
+               ErrPkt = jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR),
+               ejabberd_router:route_error(To, From, ErrPkt, Packet)
+           catch _:_ ->
+                   ok
+           end;
+       _ ->
+           ok
+    end,
+    {noreply, State};
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+do_route(_State, From, To, #xmlel{name = <<"iq">>} = Packet) ->
+    if To#jid.luser == <<"">> ->
+           ejabberd_local:process_iq(From, To, Packet);
+       true ->
+           ejabberd_sm:process_iq(From, To, Packet)
+    end;
+do_route(_State, From, To, #xmlel{name = <<"presence">>} = Packet)
+  when To#jid.luser /= <<"">> ->
+    case fxml:get_tag_attr_s(<<"type">>, Packet) of
+       <<"unavailable">> ->
+           delete_presence(From, To);
+       _ ->
+           ok
+    end;
+do_route(_State, _From, _To, _Packet) ->
+    ok.
+
+subscribe_nodes(From, To, Nodes) ->
+    LTo = jid:tolower(jid:remove_resource(To)),
+    LFrom = jid:tolower(jid:remove_resource(From)),
+    From_s = jid:to_string(LFrom),
+    lists:foldl(
+      fun(_Node, {error, _} = Err) ->
+             Err;
+        (Node, {result, _}) ->
+             case mod_pubsub:subscribe_node(LTo, Node, From, From_s, []) of
+                 {error, _} = Err ->
+                     case is_item_not_found(Err) of
+                         true ->
+                             case mod_pubsub:create_node(
+                                    LTo, To#jid.lserver, Node, LFrom, <<"mix">>) of
+                                 {result, _} ->
+                                     mod_pubsub:subscribe_node(LTo, Node, From, From_s, []);
+                                 Error ->
+                                     Error
+                             end;
+                         false ->
+                             Err
+                     end;
+                 {result, _} = Result ->
+                     Result
+             end
+      end, {result, []}, Nodes).
+
+unsubscribe_nodes(From, To, Nodes) ->
+    LTo = jid:tolower(jid:remove_resource(To)),
+    LFrom = jid:tolower(jid:remove_resource(From)),
+    From_s = jid:to_string(LFrom),
+    lists:foldl(
+      fun(_Node, {error, _} = Err) ->
+             Err;
+        (Node, {result, _} = Result) ->
+             case mod_pubsub:unsubscribe_node(LTo, Node, From, From_s, <<"">>) of
+                 {error, _} = Err ->
+                     case is_not_subscribed(Err) of
+                         true -> Result;
+                         _ -> Err
+                     end;
+                 {result, _} = Res ->
+                     Res
+             end
+      end, {result, []}, Nodes).
+
+publish_participant(From, To) ->
+    LFrom = jid:tolower(jid:remove_resource(From)),
+    LTo = jid:tolower(jid:remove_resource(To)),
+    Participant = #xmlel{name = <<"participant">>,
+                        attrs = [{<<"xmlns">>, ?NS_MIX_0},
+                                 {<<"jid">>, jid:to_string(LFrom)}]},
+    ItemID = p1_sha:sha(jid:to_string(LFrom)),
+    mod_pubsub:publish_item(
+      LTo, To#jid.lserver, ?NS_MIX_NODES_PARTICIPANTS,
+      From, ItemID, [Participant]).
+
+delete_presence(From, To) ->
+    LFrom = jid:tolower(From),
+    LTo = jid:tolower(jid:remove_resource(To)),
+    case mod_pubsub:get_items(LTo, ?NS_MIX_NODES_PRESENCE) of
+       Items when is_list(Items) ->
+           lists:foreach(
+             fun(#pubsub_item{modification = {_, LJID},
+                              itemid = {ItemID, _}}) when LJID == LFrom ->
+                     delete_item(From, To, ?NS_MIX_NODES_PRESENCE, ItemID);
+                (_) ->
+                     ok
+             end, Items);
+       _ ->
+           ok
+    end.
+
+delete_participant(From, To) ->
+    LFrom = jid:tolower(jid:remove_resource(From)),
+    ItemID = p1_sha:sha(jid:to_string(LFrom)),
+    delete_presence(From, To),
+    delete_item(From, To, ?NS_MIX_NODES_PARTICIPANTS, ItemID).
+
+delete_item(From, To, Node, ItemID) ->
+    LTo = jid:tolower(jid:remove_resource(To)),
+    case mod_pubsub:delete_item(
+          LTo, Node, From, ItemID, true) of
+       {result, _} = Res ->
+           Res;
+       {error, _} = Err ->
+           case is_item_not_found(Err) of
+               true -> {result, []};
+               false -> Err
+           end
+    end.
+
+is_item_not_found({error, ErrEl}) ->
+    case fxml:get_subtag_with_xmlns(
+          ErrEl, <<"item-not-found">>, ?NS_STANZAS) of
+       #xmlel{} -> true;
+       _ -> false
+    end.
+
+is_not_subscribed({error, ErrEl}) ->
+    case fxml:get_subtag_with_xmlns(
+          ErrEl, <<"not-subscribed">>, ?NS_PUBSUB_ERRORS) of
+       #xmlel{} -> true;
+       _ -> false
+    end.
index 61546353f61880ae98512f855020d02d83396b61..ed3debbf15d8487fea63c8078482d2357f961247 100644 (file)
@@ -63,7 +63,7 @@
 %% exports for console debug manual use
 -export([create_node/5, create_node/7, delete_node/3,
     subscribe_node/5, unsubscribe_node/5, publish_item/6,
-    delete_item/4, send_items/7, get_items/2, get_item/3,
+    delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3,
     get_cached_item/2, get_configure/5, set_configure/5,
     tree_action/3, node_action/4, node_call/4]).
 
diff --git a/src/node_mix.erl b/src/node_mix.erl
new file mode 100644 (file)
index 0000000..b0410a8
--- /dev/null
@@ -0,0 +1,167 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created :  8 Mar 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(node_mix).
+
+-behaviour(gen_pubsub_node).
+
+%% API
+-export([init/3, terminate/2, options/0, features/0,
+    create_node_permission/6, create_node/2, delete_node/1,
+    purge_node/2, subscribe_node/8, unsubscribe_node/4,
+    publish_item/6, delete_item/4, remove_extra_items/3,
+    get_entity_affiliations/2, get_node_affiliations/1,
+    get_affiliation/2, set_affiliation/3,
+    get_entity_subscriptions/2, get_node_subscriptions/1,
+    get_subscriptions/2, set_subscriptions/4,
+    get_pending_nodes/2, get_states/1, get_state/2,
+    set_state/1, get_items/7, get_items/3, get_item/7,
+    get_item/2, set_item/1, get_item_name/3, node_to_path/1,
+    path_to_node/1]).
+
+-include("pubsub.hrl").
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+init(Host, ServerHost, Opts) ->
+    node_flat:init(Host, ServerHost, Opts).
+
+terminate(Host, ServerHost) ->
+    node_flat:terminate(Host, ServerHost).
+
+options() ->
+    [{deliver_payloads, true},
+       {notify_config, false},
+       {notify_delete, false},
+       {notify_retract, true},
+       {purge_offline, false},
+       {persist_items, true},
+       {max_items, ?MAXITEMS},
+       {subscribe, true},
+       {access_model, open},
+       {roster_groups_allowed, []},
+       {publish_model, open},
+       {notification_type, headline},
+       {max_payload_size, ?MAX_PAYLOAD_SIZE},
+       {send_last_published_item, never},
+       {deliver_notifications, true},
+        {broadcast_all_resources, true},
+       {presence_based_delivery, false}].
+
+features() ->
+    [<<"create-nodes">>,
+       <<"delete-nodes">>,
+       <<"delete-items">>,
+       <<"instant-nodes">>,
+       <<"item-ids">>,
+       <<"outcast-affiliation">>,
+       <<"persistent-items">>,
+       <<"publish">>,
+       <<"purge-nodes">>,
+       <<"retract-items">>,
+       <<"retrieve-affiliations">>,
+       <<"retrieve-items">>,
+       <<"retrieve-subscriptions">>,
+       <<"subscribe">>,
+       <<"subscription-notifications">>].
+
+create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) ->
+    node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access).
+
+create_node(Nidx, Owner) ->
+    node_flat:create_node(Nidx, Owner).
+
+delete_node(Removed) ->
+    node_flat:delete_node(Removed).
+
+subscribe_node(Nidx, Sender, Subscriber, AccessModel,
+           SendLast, PresenceSubscription, RosterGroup, Options) ->
+    node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+       PresenceSubscription, RosterGroup, Options).
+
+unsubscribe_node(Nidx, Sender, Subscriber, SubId) ->
+    node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId).
+
+publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) ->
+    node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload).
+
+remove_extra_items(Nidx, MaxItems, ItemIds) ->
+    node_flat:remove_extra_items(Nidx, MaxItems, ItemIds).
+
+delete_item(Nidx, Publisher, PublishModel, ItemId) ->
+    node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId).
+
+purge_node(Nidx, Owner) ->
+    node_flat:purge_node(Nidx, Owner).
+
+get_entity_affiliations(Host, Owner) ->
+    node_flat:get_entity_affiliations(Host, Owner).
+
+get_node_affiliations(Nidx) ->
+    node_flat:get_node_affiliations(Nidx).
+
+get_affiliation(Nidx, Owner) ->
+    node_flat:get_affiliation(Nidx, Owner).
+
+set_affiliation(Nidx, Owner, Affiliation) ->
+    node_flat:set_affiliation(Nidx, Owner, Affiliation).
+
+get_entity_subscriptions(Host, Owner) ->
+    node_flat:get_entity_subscriptions(Host, Owner).
+
+get_node_subscriptions(Nidx) ->
+    node_flat:get_node_subscriptions(Nidx).
+
+get_subscriptions(Nidx, Owner) ->
+    node_flat:get_subscriptions(Nidx, Owner).
+
+set_subscriptions(Nidx, Owner, Subscription, SubId) ->
+    node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId).
+
+get_pending_nodes(Host, Owner) ->
+    node_flat:get_pending_nodes(Host, Owner).
+
+get_states(Nidx) ->
+    node_flat:get_states(Nidx).
+
+get_state(Nidx, JID) ->
+    node_flat:get_state(Nidx, JID).
+
+set_state(State) ->
+    node_flat:set_state(State).
+
+get_items(Nidx, From, RSM) ->
+    node_flat:get_items(Nidx, From, RSM).
+
+get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) ->
+    node_flat:get_items(Nidx, JID, AccessModel,
+       PresenceSubscription, RosterGroup, SubId, RSM).
+
+get_item(Nidx, ItemId) ->
+    node_flat:get_item(Nidx, ItemId).
+
+get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) ->
+    node_flat:get_item(Nidx, ItemId, JID, AccessModel,
+       PresenceSubscription, RosterGroup, SubId).
+
+set_item(Item) ->
+    node_flat:set_item(Item).
+
+get_item_name(Host, Node, Id) ->
+    node_flat:get_item_name(Host, Node, Id).
+
+node_to_path(Node) ->
+    node_flat:node_to_path(Node).
+
+path_to_node(Path) ->
+    node_flat:path_to_node(Path).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================