_ ->
case StateData#state.resource of
"" ->
+ RosterVersioningFeature =
+ case roster_versioning:is_enabled(Server) of
+ true -> [roster_versioning:stream_feature()];
+ false -> []
+ end,
+ StreamFeatures = [{xmlelement, "bind",
+ [{"xmlns", ?NS_BIND}], []},
+ {xmlelement, "session",
+ [{"xmlns", ?NS_SESSION}], []} | RosterVersioningFeature],
send_element(
StateData,
{xmlelement, "stream:features", [],
- [{xmlelement, "bind",
- [{"xmlns", ?NS_BIND}], []},
- {xmlelement, "session",
- [{"xmlns", ?NS_SESSION}], []}]}),
+ StreamFeatures}),
fsm_next_state(wait_for_bind,
StateData#state{
server = Server,
-define(NS_REGISTER, "jabber:iq:register").
-define(NS_SEARCH, "jabber:iq:search").
-define(NS_ROSTER, "jabber:iq:roster").
+-define(NS_ROSTER_VER, "urn:xmpp:features:rosterver").
-define(NS_PRIVACY, "jabber:iq:privacy").
-define(NS_PRIVATE, "jabber:iq:private").
-define(NS_VERSION, "jabber:iq:version").
get_jid_info/4,
item_to_xml/1,
webadmin_page/3,
- webadmin_user/4]).
+ webadmin_user/4,
+ roster_versioning_enabled/1,
+ roster_version/2]).
-include("ejabberd.hrl").
-include("jlib.hrl").
IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
mnesia:create_table(roster,[{disc_copies, [node()]},
{attributes, record_info(fields, roster)}]),
+ mnesia:create_table(roster_version, [{disc_copies, [node()]},
+ {attributes, record_info(fields, roster_version)}]),
+
update_table(),
mnesia:add_table_index(roster, us),
+ mnesia:add_table_index(roster_version, us),
ejabberd_hooks:add(roster_get, Host,
?MODULE, get_user_roster, 50),
ejabberd_hooks:add(roster_in_subscription, Host,
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER).
+roster_versioning_enabled(Host) ->
+ gen_mod:get_module_opt(Host, ?MODULE, versioning, false).
+
+roster_version_on_db(Host) ->
+ gen_mod:get_module_opt(Host, ?MODULE, store_current_id, false).
+
process_iq(From, To, IQ) ->
#iq{sub_el = SubEl} = IQ,
#jid{lserver = LServer} = From,
process_iq_get(From, To, IQ)
end.
-
-
+roster_hash(Items) ->
+ sha:sha(term_to_binary(
+ lists:sort(
+ [R#roster{groups = lists:sort(Grs)} ||
+ R = #roster{groups = Grs} <- Items]))).
+
+roster_version(LServer ,LUser) ->
+ US = {LUser, LServer},
+ case roster_version_on_db(LServer) of
+ true ->
+ case mnesia:dirty_read(roster_version, US) of
+ [#roster_version{version = V}] -> V;
+ [] -> not_found
+ end;
+ false ->
+ roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, [], [US]))
+ end.
+
+%% Load roster from DB only if neccesary.
+%% It is neccesary if
+%% - roster versioning is disabled in server OR
+%% - roster versioning is not used by the client OR
+%% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR
+%% - the roster version from client don't match current version.
process_iq_get(From, To, #iq{sub_el = SubEl} = IQ) ->
LUser = From#jid.luser,
LServer = From#jid.lserver,
US = {LUser, LServer},
- case catch ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US]) of
- Items when is_list(Items) ->
- XItems = lists:map(fun item_to_xml/1, Items),
- IQ#iq{type = result,
- sub_el = [{xmlelement, "query",
- [{"xmlns", ?NS_ROSTER}],
- XItems}]};
- _ ->
- IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
+ try
+ {ItemsToSend, VersionToSend} =
+ case {xml:get_tag_attr("ver", SubEl),
+ roster_versioning_enabled(LServer),
+ roster_version_on_db(LServer)} of
+ {{value, RequestedVersion}, true, true} ->
+ %% Retrieve version from DB. Only load entire roster
+ %% when neccesary.
+ case mnesia:dirty_read(roster_version, US) of
+ [#roster_version{version = RequestedVersion}] ->
+ {false, false};
+ [#roster_version{version = NewVersion}] ->
+ {lists:map(fun item_to_xml/1,
+ ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US])), NewVersion};
+ [] ->
+ RosterVersion = sha:sha(term_to_binary(now())),
+ mnesia:dirty_write(#roster_version{us = US, version = RosterVersion}),
+ {lists:map(fun item_to_xml/1,
+ ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US])), RosterVersion}
+ end;
+
+ {{value, RequestedVersion}, true, false} ->
+ RosterItems = ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [] , [US]),
+ case roster_hash(RosterItems) of
+ RequestedVersion ->
+ {false, false};
+ New ->
+ {lists:map(fun item_to_xml/1, RosterItems), New}
+ end;
+
+ _ ->
+ {lists:map(fun item_to_xml/1,
+ ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US])), false}
+ end,
+ IQ#iq{type = result, sub_el = case {ItemsToSend, VersionToSend} of
+ {false, false} -> [];
+ {Items, false} -> [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], Items}];
+ {Items, Version} -> [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}, {"ver", Version}], Items}]
+ end}
+ catch
+ _:_ ->
+ IQ#iq{type =error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
end.
+
get_user_roster(Acc, US) ->
case catch mnesia:dirty_index_read(roster, US, #roster.us) of
Items when is_list(Items) ->
%% subscription information from there:
Item3 = ejabberd_hooks:run_fold(roster_process_item,
LServer, Item2, [LServer]),
+ case roster_version_on_db(LServer) of
+ true -> mnesia:write(#roster_version{us = {LUser, LServer}, version = sha:sha(term_to_binary(now()))});
+ false -> ok
+ end,
{Item, Item3}
end,
case mnesia:transaction(F) of
[{item,
Item#roster.jid,
Item#roster.subscription}]}),
- lists:foreach(fun(Resource) ->
+ case roster_versioning_enabled(Server) of
+ true ->
+ roster_versioning:push_item(Server, User, From, Item, roster_version(Server, User));
+ false ->
+ lists:foreach(fun(Resource) ->
push_item(User, Server, Resource, From, Item)
- end, ejabberd_sm:get_user_resources(User, Server)).
+ end, ejabberd_sm:get_user_resources(User, Server))
+ end.
% TODO: don't push to those who didn't load roster
push_item(User, Server, Resource, From, Item) ->
ask = Pending,
askmessage = list_to_binary(AskMessage)},
mnesia:write(NewItem),
+ case roster_version_on_db(LServer) of
+ true -> mnesia:write(#roster_version{us = {LUser, LServer}, version = sha:sha(term_to_binary(now()))});
+ false -> ok
+ end,
{{push, NewItem}, AutoReply}
end
end,
askmessage = [],
xs = []}).
+-record(roster_version, {us,
+ version}).
remove_user/2,
get_jid_info/4,
webadmin_page/3,
- webadmin_user/4]).
+ webadmin_user/4,
+ roster_versioning_enabled/1]).
-include("ejabberd.hrl").
-include("jlib.hrl").
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER).
+roster_versioning_enabled(Host) ->
+ gen_mod:get_module_opt(Host, ?MODULE, versioning, false).
+
+roster_version_on_db(Host) ->
+ gen_mod:get_module_opt(Host, ?MODULE, store_current_id, false).
+
process_iq(From, To, IQ) ->
#iq{sub_el = SubEl} = IQ,
#jid{lserver = LServer} = From,
end.
-
+roster_hash(Items) ->
+ sha:sha(term_to_binary(
+ lists:sort(
+ [R#roster{groups = lists:sort(Grs)} ||
+ R = #roster{groups = Grs} <- Items]))).
+
+roster_version(LServer ,LUser) ->
+ US = {LUser, LServer},
+ case roster_version_on_db(LServer) of
+ true ->
+ case odbc_queries:get_roster_version(ejabberd_odbc:escape(LServer), ejabberd_odbc:escape(LUser)) of
+ {selected, ["version"], [{Version}]} -> Version;
+ {selected, ["version"], []} -> not_found
+ end;
+ false ->
+ roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, [], [US]))
+ end.
+
+%% Load roster from DB only if neccesary.
+%% It is neccesary if
+%% - roster versioning is disabled in server OR
+%% - roster versioning is not used by the client OR
+%% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR
+%% - the roster version from client don't match current version.
process_iq_get(From, To, #iq{sub_el = SubEl} = IQ) ->
LUser = From#jid.luser,
LServer = From#jid.lserver,
US = {LUser, LServer},
- case catch ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US]) of
- Items when is_list(Items) ->
- XItems = lists:map(fun item_to_xml/1, Items),
- IQ#iq{type = result,
- sub_el = [{xmlelement, "query",
- [{"xmlns", ?NS_ROSTER}],
- XItems}]};
- _ ->
- IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
+
+ try
+ {ItemsToSend, VersionToSend} =
+ case {xml:get_tag_attr("ver", SubEl),
+ roster_versioning_enabled(LServer),
+ roster_version_on_db(LServer)} of
+ {{value, RequestedVersion}, true, true} ->
+ %% Retrieve version from DB. Only load entire roster
+ %% when neccesary.
+ case odbc_queries:get_roster_version(ejabberd_odbc:escape(LServer), ejabberd_odbc:escape(LUser)) of
+ {selected, ["version"], [{RequestedVersion}]} ->
+ {false, false};
+ {selected, ["version"], [{NewVersion}]} ->
+ {lists:map(fun item_to_xml/1,
+ ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US])), NewVersion};
+ {selected, ["version"], []} ->
+ RosterVersion = sha:sha(term_to_binary(now())),
+ {atomic, {updated,1}} = odbc_queries:sql_transaction(LServer, fun() ->
+ odbc_queries:set_roster_version(ejabberd_odbc:escape(LUser), RosterVersion)
+ end),
+
+ {lists:map(fun item_to_xml/1,
+ ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US])), RosterVersion}
+ end;
+
+ {{value, RequestedVersion}, true, false} ->
+ RosterItems = ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [] , [US]),
+ case roster_hash(RosterItems) of
+ RequestedVersion ->
+ {false, false};
+ New ->
+ {lists:map(fun item_to_xml/1, RosterItems), New}
+ end;
+
+ _ ->
+ {lists:map(fun item_to_xml/1,
+ ejabberd_hooks:run_fold(roster_get, To#jid.lserver, [], [US])), false}
+ end,
+ IQ#iq{type = result, sub_el = case {ItemsToSend, VersionToSend} of
+ {false, false} -> [];
+ {Items, false} -> [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], Items}];
+ {Items, Version} -> [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}, {"ver", Version}], Items}]
+ end}
+ catch
+ _:_ ->
+ IQ#iq{type =error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
end.
+
get_user_roster(Acc, {LUser, LServer}) ->
Items = get_roster(LUser, LServer),
lists:filter(fun(#roster{subscription = none, ask = in}) ->
Item2 = process_item_els(Item1, Els),
case Item2#roster.subscription of
remove ->
+ io:format("del_roster: ~p ~p ~p \n", [LServer, Username, SJID]),
odbc_queries:del_roster(LServer, Username, SJID);
_ ->
ItemVals = record_to_string(Item2),
%% subscription information from there:
Item3 = ejabberd_hooks:run_fold(roster_process_item,
LServer, Item2, [LServer]),
+ case roster_version_on_db(LServer) of
+ true -> odbc_queries:set_roster_version(ejabberd_odbc:escape(LUser), sha:sha(term_to_binary(now())));
+ false -> ok
+ end,
{Item, Item3}
end,
case odbc_queries:sql_transaction(LServer, F) of
[{item,
Item#roster.jid,
Item#roster.subscription}]}),
- lists:foreach(fun(Resource) ->
+ case roster_versioning_enabled(Server) of
+ true ->
+ roster_versioning:push_item(Server, User, From, Item, roster_version(Server, User));
+ false ->
+ lists:foreach(fun(Resource) ->
push_item(User, Server, Resource, From, Item)
- end, ejabberd_sm:get_user_resources(User, Server)).
+ end, ejabberd_sm:get_user_resources(User, Server))
+ end.
% TODO: don't push to those who not load roster
push_item(User, Server, Resource, From, Item) ->
askmessage = AskMessage},
ItemVals = record_to_string(NewItem),
odbc_queries:roster_subscribe(LServer, Username, SJID, ItemVals),
+ case roster_version_on_db(LServer) of
+ true -> odbc_queries:set_roster_version(ejabberd_odbc:escape(LUser), sha:sha(term_to_binary(now())));
+ false -> ok
+ end,
{{push, NewItem}, AutoReply}
end
end,
) ON [PRIMARY]\r
GO\r
\r
+/* Not tested on mssql */\r
+CREATE TABLE [dbo].[roster_version] (\r
+ [username] [varchar] (250) NOT NULL ,\r
+ [version] [varchar] (64) NOT NULL \r
+) ON [PRIMARY]\r
+GO\r
+\r
+\r
/* Constraints to add:\r
- id in privacy_list is a SERIAL autogenerated number\r
- id in privacy_list_data must exist in the table privacy_list */\r
) WITH FILLFACTOR = 90 ON [PRIMARY] \r
GO\r
\r
+ALTER TABLE [dbo].[roster_version] WITH NOCHECK ADD \r
+ CONSTRAINT [PK_roster_version] PRIMARY KEY CLUSTERED \r
+ (\r
+ [username]\r
+ ) WITH FILLFACTOR = 90 ON [PRIMARY] \r
+GO\r
+\r
ALTER TABLE [dbo].[vcard] WITH NOCHECK ADD \r
CONSTRAINT [PK_vcard] PRIMARY KEY CLUSTERED \r
(\r
) WITH FILLFACTOR = 90 ON [PRIMARY] \r
GO\r
\r
+\r
+\r
CREATE INDEX [IX_rostergroups_jid] ON [dbo].[rostergroups]([jid]) WITH FILLFACTOR = 90 ON [PRIMARY]\r
GO\r
\r
CREATE INDEX i_private_storage_username USING BTREE ON private_storage(username);
CREATE UNIQUE INDEX i_private_storage_username_namespace USING BTREE ON private_storage(username(75), namespace(75));
+-- Not tested in mysql
+CREATE TABLE roster_version (
+ username varchar(250) PRIMARY KEY,
+ version text NOT NULL
+) CHARACTER SET utf8;
+
-- To update from 1.x:
-- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask;
-- UPDATE rosterusers SET askmessage = '';
set_vcard/26,
get_vcard/2,
escape/1,
- count_records_where/3]).
+ count_records_where/3,
+ get_roster_version/2,
+ set_roster_version/2]).
%% We have only two compile time options for db queries:
%-define(generic, true).
ejabberd_odbc:sql_query(
LServer,
["select count(*) from ", Table, " ", WhereClause, ";"]).
+
+
+get_roster_version(LServer, LUser) ->
+ ejabberd_odbc:sql_query(LServer,
+ ["select version from roster_version where username = '", LUser, "'"]).
+set_roster_version(LUser, Version) ->
+ update_t("roster_version", ["username", "version"], [LUser, Version], ["username = '", LUser, "'"]).
-endif.
%% -----------------
ejabberd_odbc:sql_query(
LServer,
["select count(*) from ", Table, " ", WhereClause, " with (nolock)"]).
+
+get_roster_version(LServer, LUser) ->
+ ejabberd_odbc:sql_query(LServer,
+ ["select version from dbo.roster_version where username = '", LUser, "'"]).
+set_roster_version(LUser, Version) ->
+ update_t("dbo.roster_version", ["username", "version"], [LUser, Version], ["username = '", LUser, "'"]).
-endif.
CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage USING btree (username, namespace);
+CREATE TABLE roster_version (
+ username text PRIMARY KEY,
+ version text NOT NULL
+);
+
-- To update from 0.9.8:
-- CREATE SEQUENCE spool_seq_seq;
-- ALTER TABLE spool ADD COLUMN seq integer;
--- /dev/null
+%%%----------------------------------------------------------------------
+%%% File : mod_roster.erl
+%%% Author : Pablo Polvorin <pablo.polvorin@process-one.net>
+%%% Purpose : Common utility functions for XEP-0237 (Roster Versioning)
+%%% Created : 19 Jul 2009 by Pablo Polvorin <pablo.polvorin@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2009 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%
+%%% @doc The roster versioning follows an all-or-nothing strategy:
+%%% - If the version supplied by the client is the lastest, return an empty response
+%%% - If not, return the entire new roster (with updated version string).
+%%% Roster version is a hash digest of the entire roster.
+%%% No additional data is stored in DB.
+%%%----------------------------------------------------------------------
+-module(roster_versioning).
+-author('pablo.polvorin@process-one.net').
+
+%%API
+-export([is_enabled/1,
+ stream_feature/0,
+ push_item/5]).
+
+
+-include("mod_roster.hrl").
+-include("jlib.hrl").
+
+%%@doc is roster versioning enabled?
+is_enabled(Host) ->
+ case gen_mod:is_loaded(Host, mod_roster) of
+ true -> mod_roster:roster_versioning_enabled(Host);
+ false -> mod_roster_odbc:roster_versioning_enabled(Host)
+ end.
+
+stream_feature() ->
+ {xmlelement,
+ "ver",
+ [{"xmlns", ?NS_ROSTER_VER}],
+ [{xmlelement, "optional", [], []}]}.
+
+
+
+
+%% @doc Roster push, calculate and include the version attribute.
+%% TODO: don't push to those who didn't load roster
+push_item(Server, User, From, Item, RosterVersion) ->
+ lists:foreach(fun(Resource) ->
+ push_item(User, Server, Resource, From, Item, RosterVersion)
+ end, ejabberd_sm:get_user_resources(User, Server)).
+
+push_item(User, Server, Resource, From, Item, RosterVersion) ->
+ IQPush = #iq{type = 'set', xmlns = ?NS_ROSTER,
+ id = "push" ++ randoms:get_string(),
+ sub_el = [{xmlelement, "query",
+ [{"xmlns", ?NS_ROSTER},
+ {"ver", RosterVersion}],
+ [mod_roster:item_to_xml(Item)]}]},
+ ejabberd_router:route(
+ From,
+ jlib:make_jid(User, Server, Resource),
+ jlib:iq_to_xml(IQPush)).