From 765770aaa5c13267677293223802e63638e873ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 27 Sep 2023 18:36:30 +0200 Subject: [PATCH] Add support for xep-0402 - PEP Native Bookmarks --- mix.exs | 2 +- rebar.config | 2 +- src/mod_private.erl | 176 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 166 insertions(+), 14 deletions(-) diff --git a/mix.exs b/mix.exs index 4f065cd8d..e1f19e8d7 100644 --- a/mix.exs +++ b/mix.exs @@ -114,7 +114,7 @@ defmodule Ejabberd.MixProject do {:p1_utils, "~> 1.0"}, {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, - {:xmpp, git: "https://github.com/processone/xmpp.git", ref: "ef62a043e93c0f472b987847a2a6718714772dcc", override: true}, + {:xmpp, git: "https://github.com/processone/xmpp.git", ref: "68cb07d5d0f36d5c51bfea496c638f3ee9b36027", override: true}, {:yconf, "~> 1.0"}] ++ cond_deps() end diff --git a/rebar.config b/rebar.config index c974b0001..eaf513c2f 100644 --- a/rebar.config +++ b/rebar.config @@ -77,7 +77,7 @@ {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.29"}}}, {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.2.7"}}}}, - {xmpp, ".*", {git, "https://github.com/processone/xmpp", "ef62a043e93c0f472b987847a2a6718714772dcc"}}, + {xmpp, ".*", {git, "https://github.com/processone/xmpp", "68cb07d5d0f36d5c51bfea496c638f3ee9b36027"}}, {yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.15"}}} ]}. diff --git a/src/mod_private.erl b/src/mod_private.erl index d5e252669..ccb3f5bfb 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -29,13 +29,15 @@ -protocol({xep, 49, '1.2'}). -protocol({xep, 411, '0.2.0', '18.12', "", ""}). +-protocol({xep, 402, '1.1.3', '23.09', "", ""}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_sm_iq/1, import_info/0, remove_user/2, get_data/2, get_data/3, export/1, mod_doc/0, import/5, import_start/2, mod_opt_type/1, set_data/2, - mod_options/1, depends/2, get_sm_features/5, pubsub_publish_item/6]). + mod_options/1, depends/2, get_sm_features/5, pubsub_publish_item/6, + pubsub_delete_item/5]). -export([get_commands_spec/0, bookmarks_to_pep/2]). @@ -44,6 +46,7 @@ -include("mod_private.hrl"). -include("ejabberd_commands.hrl"). -include("translate.hrl"). +-include("pubsub.hrl"). -define(PRIVATE_CACHE, private_cache). @@ -66,6 +69,7 @@ start(Host, Opts) -> {ok, [{hook, remove_user, remove_user, 50}, {hook, disco_sm_features, get_sm_features, 50}, {hook, pubsub_publish_item, pubsub_publish_item, 50}, + {hook, pubsub_delete_item, pubsub_delete_item, 50}, {iq_handler, ejabberd_sm, ?NS_PRIVATE, process_sm_iq}]}. stop(Host) -> @@ -150,7 +154,7 @@ get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> get_sm_features(Acc, _From, To, <<"">>, _Lang) -> case gen_mod:is_loaded(To#jid.lserver, mod_pubsub) of true -> - {result, [?NS_BOOKMARKS_CONVERSION_0 | + {result, [?NS_BOOKMARKS_CONVERSION_0, ?NS_PEP_BOOKMARKS_COMPAT, ?NS_PEP_BOOKMARKS_COMPAT_PEP | case Acc of {result, Features} -> Features; empty -> [] @@ -207,17 +211,21 @@ filter_xmlels(Els) -> -spec set_data(jid(), [{binary(), xmlel()}]) -> ok | {error, _}. set_data(JID, Data) -> - set_data(JID, Data, true). + set_data(JID, Data, true, true). --spec set_data(jid(), [{binary(), xmlel()}], boolean()) -> ok | {error, _}. -set_data(JID, Data, Publish) -> +-spec set_data(jid(), [{binary(), xmlel()}], boolean(), boolean()) -> ok | {error, _}. +set_data(JID, Data, PublishPepStorageBookmarks, PublishPepXmppBookmarks) -> {LUser, LServer, _} = jid:tolower(JID), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_data(LUser, LServer, Data) of ok -> delete_cache(Mod, LUser, LServer, Data), - case Publish of - true -> publish_data(JID, Data); + case PublishPepStorageBookmarks of + true -> publish_pep_storage_bookmarks(JID, Data); + false -> ok + end, + case PublishPepXmppBookmarks of + true -> publish_pep_xmpp_bookmarks(JID, Data); false -> ok end; {error, _} = Err -> @@ -278,8 +286,8 @@ remove_user(User, Server) -> %%%=================================================================== %%% Pubsub %%%=================================================================== --spec publish_data(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. -publish_data(JID, Data) -> +-spec publish_pep_storage_bookmarks(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. +publish_pep_storage_bookmarks(JID, Data) -> {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case gen_mod:is_loaded(LServer, mod_pubsub) of true -> @@ -299,16 +307,154 @@ publish_data(JID, Data) -> ok end. +-spec publish_pep_xmpp_bookmarks(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. +publish_pep_xmpp_bookmarks(JID, Data) -> + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case gen_mod:is_loaded(LServer, mod_pubsub) of + true -> + case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of + {_, Bookmarks0} -> + Bookmarks = case xmpp:decode(Bookmarks0) of + #bookmark_storage{conference = C} -> C; + _ -> [] + end, + PubOpts = [{persist_items, true}, + {access_model, whitelist}, + {max_items, max}, + {notify_retract,true}, + {notify_delete,true}, + {send_last_published_item, never}], + case mod_pubsub:get_items(LBJID, ?NS_PEP_BOOKMARKS) of + PepBookmarks when is_list(PepBookmarks) -> + put(mod_private_pep_update, true), + PepBookmarksMap = lists:foldl(fun pubsub_item_to_map/2, #{}, PepBookmarks), + ToDelete = + lists:foldl( + fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, Map2) -> + PB = storage_bookmark_to_xmpp_bookmark(Bookmark), + case maps:take(jid:tolower(BookmarkJID), Map2) of + {StoredBookmark, Map3} when StoredBookmark == PB -> + Map3; + {_, Map4} -> + mod_pubsub:publish_item( + LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, + jid:encode(BookmarkJID), [xmpp:encode(PB)], PubOpts, all), + Map4; + _ -> + mod_pubsub:publish_item( + LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, + jid:encode(BookmarkJID), [xmpp:encode(PB)], PubOpts, all), + Map2 + end + end, PepBookmarksMap, Bookmarks), + maps:fold( + fun(DeleteJid, _, _) -> + mod_pubsub:delete_item(LBJID, ?NS_PEP_BOOKMARKS, JID, jid:encode(DeleteJid)) + end, ok, ToDelete), + erase(mod_private_pep_update), + ok; + {error, #stanza_error{reason = 'item-not-found'}} -> + put(mod_private_pep_update, true), + lists:foreach( + fun(#bookmark_conference{jid = BookmarkJID} = Bookmark) -> + PB = storage_bookmark_to_xmpp_bookmark(Bookmark), + mod_pubsub:publish_item( + LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, + jid:encode(BookmarkJID), [xmpp:encode(PB)], PubOpts, all) + end, Bookmarks), + erase(mod_private_pep_update), + ok; + _ -> + ok + end; + _ -> + ok + end; + false -> + ok + end. + -spec pubsub_publish_item(binary(), binary(), jid(), jid(), binary(), [xmlel()]) -> any(). pubsub_publish_item(LServer, ?NS_STORAGE_BOOKMARKS, #jid{luser = LUser, lserver = LServer} = From, #jid{luser = LUser, lserver = LServer}, _ItemId, [Payload|_]) -> - set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], false); + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], false, true); +pubsub_publish_item(LServer, ?NS_PEP_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId, _Payload) -> + NotRecursion = get(mod_private_pep_update) == undefined, + case mod_pubsub:get_items({LUser, LServer, <<>>}, ?NS_PEP_BOOKMARKS) of + Bookmarks when is_list(Bookmarks), NotRecursion -> + Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), + Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); + _ -> + ok + end; pubsub_publish_item(_, _, _, _, _, _) -> ok. +-spec pubsub_delete_item(binary(), binary(), jid(), jid(), binary()) -> any(). +pubsub_delete_item(LServer, ?NS_PEP_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId) -> + NotRecursion = get(mod_private_pep_update) == undefined, + case mod_pubsub:get_items({LUser, LServer, <<>>}, ?NS_PEP_BOOKMARKS) of + Bookmarks when is_list(Bookmarks), NotRecursion -> + Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), + Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); + _ -> + ok + end; +pubsub_delete_item(_, _, _, _, _) -> + ok. + +-spec pubsub_item_to_storage_bookmark(#pubsub_item{}) -> {true, bookmark_conference()} | false. +pubsub_item_to_storage_bookmark(#pubsub_item{itemid = {Id, _}, payload = [#xmlel{} = B | _]}) -> + case xmpp:decode(B) of + #pep_bookmarks_conference{name = Name, autojoin = AutoJoin, nick = Nick, + password = Password} -> + try jid:decode(Id) of + #jid{} = Jid -> + {true, #bookmark_conference{jid = Jid, name = Name, autojoin = AutoJoin, nick = Nick, + password = Password}} + catch _:_ -> + false + end; + _ -> + false + end; +pubsub_item_to_storage_bookmark(_) -> + false. + +-spec storage_bookmark_to_xmpp_bookmark(bookmark_conference()) -> pep_bookmarks_conference(). +storage_bookmark_to_xmpp_bookmark(#bookmark_conference{name = Name, autojoin = AutoJoin, nick = Nick, + password = Password}) -> + #pep_bookmarks_conference{name = Name, autojoin = AutoJoin, nick = Nick, + password = Password}. + +-spec pubsub_item_to_map(#pubsub_item{}, map()) -> map(). +pubsub_item_to_map(#pubsub_item{itemid = {Id, _}, payload = [#xmlel{} = B | _]}, Map) -> + ?INFO_MSG("DECODING ~p", [B]), + case xmpp:decode(B) of + #pep_bookmarks_conference{} = B2 -> + try jid:decode(Id) of + #jid{} = Jid -> + maps:put(jid:tolower(Jid), B2#pep_bookmarks_conference{extensions = undefined}, Map) + catch _:_ -> + Map + end; + _ -> + Map + end; +pubsub_item_to_map(_, Map) -> + Map. + %%%=================================================================== %%% Commands %%%=================================================================== @@ -344,11 +490,17 @@ bookmarks_to_pep(User, Server) -> case Res of {ok, El} -> Data = [{?NS_STORAGE_BOOKMARKS, El}], - case publish_data(jid:make(User, Server), Data) of + case publish_pep_storage_bookmarks(jid:make(User, Server), Data) of ok -> - {ok, <<"Bookmarks exported to PEP node">>}; + case publish_pep_xmpp_bookmarks(jid:make(User, Server), Data) of + ok -> + {ok, <<"Bookmarks exported to PEP node">>}; + {error, Err} -> + {error, xmpp:format_stanza_error(Err)} + end; {error, Err} -> {error, xmpp:format_stanza_error(Err)} + end; _ -> {error, <<"Cannot retrieve bookmarks from private XML storage">>}