{"action":"created","comment":{"url":"https://api.github.com/repos/esl/MongooseIM/pulls/comments/90653015","pull_request_review_id":11172013,"id":90653015,"diff_hunk":"@@ -0,0 +1,835 @@\n+%%%-------------------------------------------------------------------\n+%%% @author Uvarov Michael <arcusfelis@gmail.com>\n+%%% @copyright (C) 2013, Uvarov Michael\n+%%% @doc ODBC backend for Message Archive Management.\n+%%% @end\n+%%%-------------------------------------------------------------------\n+-module(mod_mam_muc_cassandra_arch).\n+-behaviour(mongoose_cassandra).\n+\n+%% gen_mod handlers\n+-export([start/2, stop/1]).\n+\n+%% MAM hook handlers\n+-export([archive_size/4,\n+         archive_message/9,\n+         lookup_messages/14,\n+         remove_archive/3,\n+         purge_single_message/6,\n+         purge_multiple_messages/9]).\n+\n+%% mongoose_cassandra_worker callbacks\n+-export([prepared_queries/0]).\n+\n+%% ----------------------------------------------------------------------\n+%% Imports\n+\n+%% UID\n+-import(mod_mam_utils,\n+        [encode_compact_uuid/2]).\n+\n+%% JID serialization\n+-import(mod_mam_utils,\n+        [jid_to_opt_binary/2,\n+         expand_minified_jid/2]).\n+\n+%% Other\n+-import(mod_mam_utils,\n+        [maybe_min/2,\n+         maybe_max/2,\n+         apply_start_border/2,\n+         apply_end_border/2]).\n+\n+-include_lib(\"ejabberd/include/ejabberd.hrl\").\n+-include_lib(\"ejabberd/include/jlib.hrl\").\n+-include_lib(\"exml/include/exml.hrl\").\n+\n+-record(mam_muc_ca_filter, {\n+    room_jid,\n+    with_nick,\n+    nick_name,\n+    start_id,\n+    end_id\n+}).\n+\n+-record(mam_muc_message, {\n+  id :: non_neg_integer(),\n+  room_jid :: binary(),\n+  nick_name :: binary(),\n+  with_nick :: binary(),\n+  message :: binary()\n+}).\n+\n+-type worker() :: pid() | atom().\n+\n+%% ----------------------------------------------------------------------\n+%% Types\n+\n+-type filter() :: #mam_muc_ca_filter{}.\n+-type message_id() :: non_neg_integer().\n+-type user_id() :: non_neg_integer().\n+-type server_hostname() :: binary().\n+-type server_host() :: binary().\n+-type unix_timestamp() :: non_neg_integer().\n+\n+\n+%% ----------------------------------------------------------------------\n+%% gen_mod callbacks\n+%% Starting and stopping functions for users' archives\n+\n+start(Host, Opts) ->\n+    compile_params_module(Opts),\n+    start_muc(Host, Opts).\n+\n+stop(Host) ->\n+    stop_muc(Host).\n+\n+%% ----------------------------------------------------------------------\n+%% Add hooks for mod_mam_muc\n+\n+start_muc(Host, _Opts) ->\n+    case gen_mod:get_module_opt(Host, ?MODULE, no_writer, false) of\n+        true ->\n+            ok;\n+        false ->\n+            ejabberd_hooks:add(mam_muc_archive_message, Host, ?MODULE, archive_message, 50)\n+    end,\n+    ejabberd_hooks:add(mam_muc_archive_size, Host, ?MODULE, archive_size, 50),\n+    ejabberd_hooks:add(mam_muc_lookup_messages, Host, ?MODULE, lookup_messages, 50),\n+    ejabberd_hooks:add(mam_muc_remove_archive, Host, ?MODULE, remove_archive, 50),\n+    ejabberd_hooks:add(mam_muc_purge_single_message, Host, ?MODULE, purge_single_message, 50),\n+    ejabberd_hooks:add(mam_muc_purge_multiple_messages, Host, ?MODULE, purge_multiple_messages, 50),\n+    ok.\n+\n+stop_muc(Host) ->\n+    case gen_mod:get_module_opt(Host, ?MODULE, no_writer, false) of\n+        true ->\n+            ok;\n+        false ->\n+            ejabberd_hooks:delete(mam_muc_archive_message, Host, ?MODULE, archive_message, 50)\n+    end,\n+    ejabberd_hooks:delete(mam_muc_archive_size, Host, ?MODULE, archive_size, 50),\n+    ejabberd_hooks:delete(mam_muc_lookup_messages, Host, ?MODULE, lookup_messages, 50),\n+    ejabberd_hooks:delete(mam_muc_remove_archive, Host, ?MODULE, remove_archive, 50),\n+    ejabberd_hooks:delete(mam_muc_purge_single_message, Host, ?MODULE, purge_single_message, 50),\n+    ejabberd_hooks:delete(mam_muc_purge_multiple_messages, Host, ?MODULE, purge_multiple_messages, 50),\n+    ok.\n+\n+%% ----------------------------------------------------------------------\n+%% mongoose_cassandra_worker callbacks\n+\n+prepared_queries() ->\n+    [{insert_query, insert_query_cql()},\n+     {delete_query, delete_query_cql()},\n+     {remove_archive_query, remove_archive_query_cql()},\n+     {message_id_to_nick_name_query, message_id_to_nick_name_cql()}]\n+    ++ extract_messages_queries()\n+    ++ extract_messages_r_queries()\n+    ++ calc_count_queries()\n+    ++ list_message_ids_queries().\n+\n+%% ----------------------------------------------------------------------\n+%% Helpers\n+\n+select_worker(UserJID) ->\n+    PoolName = pool_name(UserJID),\n+    mongoose_cassandra_sup:select_worker(PoolName, UserJID).\n+\n+%%====================================================================\n+%% Internal functions\n+%%====================================================================\n+\n+%% ----------------------------------------------------------------------\n+%% Internal functions and callbacks\n+\n+archive_size(Size, Host, _RoomID, RoomJID) when is_integer(Size) ->\n+    Worker = select_worker(RoomJID),\n+    Borders = Start = End = WithNick = undefined,\n+    Filter = prepare_filter(RoomJID, Borders, Start, End, WithNick),\n+    calc_count(Worker, RoomJID, Host, Filter).\n+\n+\n+%% ----------------------------------------------------------------------\n+%% INSERT MESSAGE\n+\n+insert_query_cql() ->\n+    \"INSERT INTO mam_muc_message \"\n+        \"(id, room_jid, nick_name, with_nick, message) \"\n+        \"VALUES (?,?,?,?,?)\".\n+\n+archive_message(Result, Host, MessID, _RoomID,\n+                     LocJID, NickName, NickName, Dir, Packet) ->\n+    try\n+        archive_message_2(Result, Host, MessID,\n+                        LocJID, NickName, NickName, Dir, Packet)\n+    catch _Type:Reason ->\n+        {error, Reason}\n+    end.\n+\n+archive_message_2(_Result, _Host, MessID,\n+                  LocJID=#jid{},\n+                  _RemJID=#jid{},\n+                  _SrcJID=#jid{lresource=BNick}, _Dir, Packet) ->\n+    BLocJID = bare_jid(LocJID),\n+    BPacket = packet_to_stored_binary(Packet),\n+    Message = #mam_muc_message{\n+      id = MessID,\n+      room_jid = BLocJID,\n+      nick_name = BNick,\n+      message = BPacket\n+    },\n+    WithNicks = [<<>>, BNick],\n+    Messages = [Message#mam_muc_message{with_nick = BWithNick} || BWithNick <- WithNicks],\n+    Worker = select_worker(LocJID),\n+    write_messages(Worker, Messages).\n+\n+write_messages(RoomJID, Messages) ->\n+    PoolName = pool_name(RoomJID),\n+    MultiParams = [message_to_params(M) || M <- Messages],\n+    mongoose_cassandra_worker:cql_query_pool_multi_async(PoolName, RoomJID, ?MODULE, insert_query, MultiParams).\n+\n+message_to_params(#mam_muc_message{\n+      id = MessID,\n+      room_jid = BLocJID,\n+      nick_name = BNick,\n+      with_nick = BWithNick,\n+      message = BPacket\n+    }) ->\n+    [MessID, BLocJID, BNick, BWithNick, BPacket].\n+\n+\n+%% ----------------------------------------------------------------------\n+%% DELETE MESSAGE\n+\n+delete_query_cql() ->\n+    \"DELETE FROM mam_muc_message \"\n+        \"WHERE room_jid = ? AND with_nick = ? AND id = ?\".\n+\n+delete_messages(Worker, RoomJID, Messages) ->\n+    MultiParams = [delete_message_to_params(M) || M <- Messages],\n+    mongoose_cassandra_worker:cql_query_multi_async(Worker, RoomJID, ?MODULE, delete_query, MultiParams).\n+\n+delete_message_to_params(#mam_muc_message{\n+      id = MessID,\n+      room_jid = BLocJID,\n+      with_nick = BWithNick\n+    }) ->\n+    [BLocJID, BWithNick, MessID].\n+\n+\n+%% ----------------------------------------------------------------------\n+%% REMOVE ARCHIVE\n+\n+remove_archive_query_cql() ->\n+    \"DELETE FROM mam_muc_message WHERE room_jid = ?\".\n+\n+remove_archive(_Host, _RoomID, RoomJID) ->\n+    BRoomJID = bare_jid(RoomJID),\n+    PoolName = pool_name(RoomJID),\n+    Params = [BRoomJID],\n+    %% Wait until deleted\n+    mongoose_cassandra_worker:cql_query_pool(PoolName, RoomJID, ?MODULE, remove_archive_query, Params),\n+    ok.\n+\n+\n+%% ----------------------------------------------------------------------\n+%% GET NICK NAME\n+\n+message_id_to_nick_name_cql() ->\n+    \"SELECT nick_name FROM mam_muc_message \"\n+        \"WHERE room_jid = ? AND with_nick = '' AND id = ?\".\n+\n+message_id_to_nick_name(Worker, RoomJID, BRoomJID, MessID) ->\n+    Params = [BRoomJID, MessID],\n+    {ok, Rows} = mongoose_cassandra_worker:cql_query(Worker, RoomJID, ?MODULE, message_id_to_nick_name_query, Params),\n+    case Rows of\n+        [] ->\n+            {error, not_found};\n+        [[BNickName]] ->\n+            {ok, BNickName}\n+    end.\n+\n+\n+%% ----------------------------------------------------------------------\n+%% SELECT MESSAGES\n+\n+-spec lookup_messages(Result :: any(), Host :: ejabberd:server(),\n+                      ArchiveID :: mod_mam_muc:archive_id(),\n+                      ArchiveJID :: ejabberd:jid(),\n+                      RSM :: jlib:rsm_in()  | undefined,\n+                      Borders :: mod_mam_muc:borders()  | undefined,\n+                      Start :: mod_mam_muc:unix_timestamp()  | undefined,\n+                      End :: mod_mam_muc:unix_timestamp()  | undefined,\n+                      Now :: mod_mam_muc:unix_timestamp(),\n+                      WithJID :: ejabberd:jid()  | undefined,\n+                      PageSize :: non_neg_integer(), LimitPassed :: boolean(),\n+                      MaxResultLimit :: non_neg_integer(),\n+                      IsSimple :: boolean()  | opt_count) ->\n+    {ok, mod_mam_muc:lookup_result()} | {error, 'policy-violation'}.\n+lookup_messages({error, _Reason}=Result, _Host,\n+                     _RoomID, _RoomJID, _RSM, _Borders,\n+                     _Start, _End, _Now, _WithJID,\n+                     _PageSize, _LimitPassed, _MaxResultLimit,\n+                     _IsSimple) ->\n+    Result;\n+lookup_messages(_Result, Host,\n+                _RoomID, RoomJID, RSM, Borders,\n+                Start, End, _Now, WithJID,\n+                PageSize, LimitPassed, MaxResultLimit,\n+                IsSimple) ->\n+    try\n+        WithNick = maybe_jid_to_nick(WithJID),\n+        Worker = select_worker(RoomJID),\n+        lookup_messages_2(Worker, Host,\n+                          RoomJID, RSM, Borders,\n+                          Start, End, WithNick,\n+                          PageSize, LimitPassed, MaxResultLimit,\n+                          IsSimple)\n+    catch _Type:Reason ->\n+        S = erlang:get_stacktrace(),\n+        {error, {Reason, S}}\n+    end.\n+\n+maybe_jid_to_nick(#jid{lresource=BNick}) -> BNick;\n+maybe_jid_to_nick(undefined)             -> undefined.\n+\n+\n+lookup_messages_2(Worker, Host,\n+                  RoomJID = #jid{}, RSM, Borders,\n+                  Start, End, WithNick,\n+                  PageSize, _LimitPassed, _MaxResultLimit,\n+                  _IsSimple=true) ->\n+    %% Simple query without calculating offset and total count\n+    Filter = prepare_filter(RoomJID, Borders, Start, End, WithNick),\n+    lookup_messages_simple(Worker, Host, RoomJID, RSM, PageSize, Filter);\n+lookup_messages_2(Worker, Host,\n+                  RoomJID = #jid{}, RSM, Borders,\n+                  Start, End, WithNick,\n+                  PageSize, LimitPassed, MaxResultLimit,\n+                  _IsSimple) ->\n+    %% Query with offset calculation\n+    %% We cannot just use ODBC code because \"LIMIT X,Y\" is not supported by cassandra\n+    %% Not all queries are optimal. You would like to disable something for production\n+    %% once you know how you will call bd\n+    Strategy = rsm_to_strategy(RSM),\n+    Filter = prepare_filter(RoomJID, Borders, Start, End, WithNick),\n+    Result =\n+    case Strategy of\n+        last_page ->\n+            lookup_messages_last_page(Worker, Host, RoomJID, RSM, PageSize, Filter);\n+        by_offset ->\n+            lookup_messages_by_offset(Worker, Host, RoomJID, RSM, PageSize, Filter);\n+        first_page ->\n+            lookup_messages_first_page(Worker, Host, RoomJID, RSM, PageSize, Filter);\n+        before_id ->\n+            lookup_messages_before_id(Worker, Host, RoomJID, RSM, PageSize, Filter);\n+        after_id ->\n+            lookup_messages_after_id(Worker, Host, RoomJID, RSM, PageSize, Filter)\n+    end,\n+    check_result_for_policy_violation(Result, MaxResultLimit, LimitPassed).\n+\n+rsm_to_strategy(#rsm_in{direction = before, id = undefined}) ->\n+    last_page;\n+rsm_to_strategy(#rsm_in{direction = undefined, index = 0}) ->\n+    first_page;\n+rsm_to_strategy(#rsm_in{direction = undefined, index = Offset}) when is_integer(Offset) ->\n+    by_offset;\n+rsm_to_strategy(#rsm_in{direction = before, id = Id}) when is_integer(Id) ->\n+    before_id;\n+rsm_to_strategy(#rsm_in{direction = aft, id = Id}) when is_integer(Id) ->\n+    after_id;\n+rsm_to_strategy(#rsm_in{}) ->\n+    first_page;\n+rsm_to_strategy(undefined) ->\n+    first_page.\n+\n+lookup_messages_simple(Worker, Host, RoomJID,\n+                       #rsm_in{direction = aft, id = ID},\n+                       PageSize, Filter) ->\n+    %% Get last rows from result set\n+    MessageRows = extract_messages(Worker, RoomJID, Host, after_id(ID, Filter), PageSize, false),\n+    {ok, {undefined, undefined, rows_to_uniform_format(MessageRows, RoomJID)}};\n+lookup_messages_simple(Worker, Host, RoomJID,\n+                       #rsm_in{direction = before, id = ID},\n+                       PageSize, Filter) ->\n+    MessageRows = extract_messages(Worker, RoomJID, Host, before_id(ID, Filter), PageSize, true),\n+    {ok, {undefined, undefined, rows_to_uniform_format(MessageRows, RoomJID)}};\n+lookup_messages_simple(Worker, Host, RoomJID,\n+                       #rsm_in{direction = undefined, index = Offset},\n+                       PageSize, Filter) ->\n+    %% Apply offset\n+    StartId = offset_to_start_id(Worker, RoomJID, Filter, Offset), %% POTENTIALLY SLOW AND NOT SIMPLE :)\n+    MessageRows = extract_messages(Worker, RoomJID, Host, from_id(StartId, Filter), PageSize, false),\n+    {ok, {undefined, undefined, rows_to_uniform_format(MessageRows, RoomJID)}};\n+lookup_messages_simple(Worker, Host, RoomJID,\n+                _,\n+                PageSize, Filter) ->\n+    MessageRows = extract_messages(Worker, RoomJID, Host, Filter, PageSize, false),\n+    {ok, {undefined, undefined, rows_to_uniform_format(MessageRows, RoomJID)}}.\n+\n+lookup_messages_last_page(Worker, Host, RoomJID,\n+                          #rsm_in{direction = before, id = undefined},\n+                          0, Filter) ->\n+    %% Last page\n+    TotalCount = calc_count(Worker, RoomJID, Host, Filter),\n+    {ok, {TotalCount, TotalCount, []}};\n+lookup_messages_last_page(Worker, Host, RoomJID,\n+                          #rsm_in{direction = before, id = undefined},\n+                          PageSize, Filter) ->\n+    %% Last page\n+    MessageRows = extract_messages(Worker, RoomJID, Host, Filter, PageSize, true),\n+    MessageRowsCount = length(MessageRows),\n+    case MessageRowsCount < PageSize of\n+        true ->\n+            {ok, {MessageRowsCount, 0,\n+                  rows_to_uniform_format(MessageRows, RoomJID)}};\n+        false ->\n+            FirstID = row_to_message_id(hd(MessageRows)),\n+            Offset = calc_count(Worker, RoomJID, Host, before_id(FirstID, Filter)),\n+            {ok, {Offset + MessageRowsCount, Offset,\n+                  rows_to_uniform_format(MessageRows, RoomJID)}}\n+    end.\n+\n+lookup_messages_by_offset(Worker, Host, RoomJID,\n+                          #rsm_in{direction = undefined, index = Offset},\n+                          0, Filter) when is_integer(Offset) ->\n+    %% By offset\n+    TotalCount = calc_count(Worker, RoomJID, Host, Filter),\n+    {ok, {TotalCount, Offset, []}};\n+lookup_messages_by_offset(Worker, Host, RoomJID,\n+                          #rsm_in{direction = undefined, index = Offset},\n+                          PageSize, Filter) when is_integer(Offset) ->\n+    %% By offset\n+    StartId = offset_to_start_id(Worker, RoomJID, Filter, Offset), %% POTENTIALLY SLOW\n+    MessageRows = extract_messages(Worker, RoomJID, Host, from_id(StartId, Filter), PageSize, false),\n+    MessageRowsCount = length(MessageRows),\n+    case MessageRowsCount < PageSize of\n+        true ->\n+            {ok, {Offset + MessageRowsCount, Offset,\n+                  rows_to_uniform_format(MessageRows, RoomJID)}};\n+        false ->\n+            LastID = row_to_message_id(lists:last(MessageRows)),\n+            CountAfterLastID = calc_count(Worker, RoomJID, Host, after_id(LastID, Filter)),\n+            {ok, {Offset + MessageRowsCount + CountAfterLastID, Offset,\n+                  rows_to_uniform_format(MessageRows, RoomJID)}}\n+    end.\n+\n+lookup_messages_first_page(Worker, Host, RoomJID,\n+                           _,\n+                           0, Filter) ->\n+    %% First page, just count\n+    TotalCount = calc_count(Worker, RoomJID, Host, Filter),\n+    {ok, {TotalCount, 0, []}};\n+lookup_messages_first_page(Worker, Host, RoomJID,\n+                           _,\n+                           PageSize, Filter) ->\n+    %% First page\n+    MessageRows = extract_messages(Worker, RoomJID, Host, Filter, PageSize, false),\n+    MessageRowsCount = length(MessageRows),\n+    case MessageRowsCount < PageSize of\n+        true ->\n+            {ok, {MessageRowsCount, 0,\n+                  rows_to_uniform_format(MessageRows, RoomJID)}};\n+        false ->\n+            LastID = row_to_message_id(lists:last(MessageRows)),\n+            CountAfterLastID = calc_count(Worker, RoomJID, Host, after_id(LastID, Filter)),\n+            {ok, {MessageRowsCount + CountAfterLastID, 0,\n+                  rows_to_uniform_format(MessageRows, RoomJID)}}\n+    end.\n+\n+lookup_messages_before_id(Worker, Host, RoomJID,\n+                          RSM = #rsm_in{direction = before, id = ID},\n+                          PageSize, Filter) ->\n+    TotalCount = calc_count(Worker, RoomJID, Host, Filter),\n+    Offset     = calc_offset(Worker, RoomJID, Host, Filter, PageSize, TotalCount, RSM),\n+    MessageRows = extract_messages(Worker, RoomJID, Host, before_id(ID, Filter), PageSize, true),\n+    {ok, {TotalCount, Offset, rows_to_uniform_format(MessageRows, RoomJID)}}.\n+\n+lookup_messages_after_id(Worker, Host, RoomJID,\n+                         RSM = #rsm_in{direction = aft, id = ID},\n+                         PageSize, Filter) ->\n+    Worker = select_worker(RoomJID),\n+    TotalCount = calc_count(Worker, RoomJID, Host, Filter),\n+    Offset     = calc_offset(Worker, RoomJID, Host, Filter, PageSize, TotalCount, RSM),\n+    MessageRows = extract_messages(Worker, RoomJID, Host, after_id(ID, Filter), PageSize, false),\n+    {ok, {TotalCount, Offset, rows_to_uniform_format(MessageRows, RoomJID)}}.\n+\n+check_result_for_policy_violation(Result={ok, {TotalCount, Offset, _}},\n+                                  MaxResultLimit, LimitPassed)\n+    when is_integer(TotalCount), is_integer(Offset) ->\n+    %% If a query returns a number of stanzas greater than this limit and the\n+    %% client did not specify a limit using RSM then the server should return\n+    %% a policy-violation error to the client.\n+    case is_policy_violation(TotalCount, Offset, MaxResultLimit, LimitPassed) of\n+        true ->\n+            {error, 'policy-violation'};\n+        false ->\n+            Result\n+    end;\n+check_result_for_policy_violation(Result, _MaxResultLimit, _LimitPassed) ->\n+    Result.\n+\n+is_policy_violation(TotalCount, Offset, MaxResultLimit, LimitPassed) ->\n+    TotalCount - Offset > MaxResultLimit andalso not LimitPassed.\n+\n+\n+after_id(ID, Filter=#mam_muc_ca_filter{start_id = AfterID}) ->\n+    Filter#mam_muc_ca_filter{start_id = maybe_max(ID + 1, AfterID)}.\n+\n+before_id(undefined, Filter) ->\n+    Filter;\n+before_id(ID, Filter=#mam_muc_ca_filter{end_id = BeforeID}) ->\n+    Filter#mam_muc_ca_filter{end_id = maybe_min(ID - 1, BeforeID)}.\n+\n+to_id(ID, Filter=#mam_muc_ca_filter{end_id = BeforeID}) ->\n+    Filter#mam_muc_ca_filter{end_id = maybe_min(ID, BeforeID)}.\n+\n+from_id(ID, Filter=#mam_muc_ca_filter{start_id = AfterID}) ->\n+    Filter#mam_muc_ca_filter{start_id = maybe_max(ID, AfterID)}.\n+\n+\n+rows_to_uniform_format(MessageRows, RoomJID) ->\n+    [row_to_uniform_format(Row, RoomJID) || Row <- MessageRows].\n+\n+row_to_uniform_format([MessID,BNick,Data], RoomJID) ->\n+    SrcJID = jid:replace_resource(RoomJID, BNick),\n+    Packet = stored_binary_to_packet(Data),\n+    {MessID, SrcJID, Packet}.\n+\n+row_to_message_id([MessID,_,_]) ->\n+    MessID.\n+\n+-spec purge_single_message(_Result, Host, MessID, _RoomID, RoomJID,\n+                           Now) ->\n+    ok  | {error, 'not-supported'} when\n+      Host :: server_host(), MessID :: message_id(),\n+      _RoomID :: user_id(), RoomJID :: #jid{},\n+      Now :: unix_timestamp().\n+purge_single_message(_Result, _Host, MessID, _RoomID, RoomJID, _Now) ->\n+    Worker = select_worker(RoomJID),\n+    BRoomJID = bare_jid(RoomJID),\n+    Result = message_id_to_nick_name(Worker, RoomJID, BRoomJID, MessID),\n+    case Result of\n+        {ok, BNick} ->\n+            BWithNicks = lists:usort([BNick, <<>>]),\n+            %% Set some fields\n+            %% To remove record we need to know room_jid, with_nick and id.\n+            Messages = [#mam_muc_message{\n+              id = MessID,\n+              room_jid = BRoomJID,\n+              nick_name = BNick, %% set the field for debugging\n+              with_nick = BWithNick\n+              } || BWithNick <- BWithNicks],\n+            delete_messages(Worker, RoomJID, Messages),\n+            ok;\n+        {error, _} ->\n+            ok\n+    end.\n+\n+-spec purge_multiple_messages(_Result, Host, _RoomID, RoomJID, Borders,\n+                              Start, End, Now, WithNick) ->\n+    ok when\n+      Host :: server_host(), _RoomID :: user_id(),\n+      RoomJID :: #jid{}, Borders :: #mam_borders{},\n+      Start :: unix_timestamp()  | undefined,\n+      End :: unix_timestamp()  | undefined,\n+      Now :: unix_timestamp(),\n+      WithNick :: #jid{}  | undefined.\n+purge_multiple_messages(_Result, Host, RoomID, RoomJID, Borders,\n+                        Start, End, Now, WithNick) ->\n+    %% Simple query without calculating offset and total count\n+    Filter = prepare_filter(RoomJID, Borders, Start, End, WithNick),\n+    PoolName = pool_name(RoomJID),\n+    Limit = 500, %% TODO something smarter\n+    QueryName = {list_message_ids_query, select_filter(Filter)},\n+    Params = eval_filter_params(Filter) ++ [Limit],\n+    {ok, Rows} = mongoose_cassandra_worker:cql_query_pool(PoolName, RoomJID, ?MODULE, QueryName, Params),\n+    %% TODO can be faster\n+    %% TODO rate limiting\n+    [purge_single_message(ok, Host, MessID, RoomID, RoomJID, Now) || [MessID] <- Rows],\n+    ok.\n+\n+\n+%% Offset is not supported\n+%% Each record is a tuple of form\n+%% `{<<\"13663125233\">>,<<\"bob@localhost\">>,<<\"res1\">>,<<binary>>}'.\n+%% Columns are `[\"id\",\"nick_name\",\"message\"]'.\n+-spec extract_messages(Worker, RoomJID, Host, Filter, IMax, ReverseLimit) ->\n+    [Row] when\n+    Worker  :: worker(),\n+    RoomJID :: jlib:jid(),\n+    Host    :: server_hostname(),\n+    Filter  :: filter(),\n+    IMax    :: pos_integer(),\n+    ReverseLimit :: boolean(),\n+    Row :: list().\n+extract_messages(_Worker, _RoomJID, _Host, _Filter, 0, _) ->\n+    [];\n+extract_messages(Worker, RoomJID, _Host, Filter, IMax, false) ->\n+    QueryName = {extract_messages_query, select_filter(Filter)},\n+    Params = eval_filter_params(Filter) ++ [IMax],\n+    {ok, Rows} = mongoose_cassandra_worker:cql_query(Worker, RoomJID, ?MODULE, QueryName, Params),\n+    Rows;\n+extract_messages(Worker, RoomJID, _Host, Filter, IMax, true) ->\n+    QueryName = {extract_messages_r_query, select_filter(Filter)},\n+    Params = eval_filter_params(Filter) ++ [IMax],\n+    {ok, Rows} = mongoose_cassandra_worker:cql_query(Worker, RoomJID, ?MODULE, QueryName, Params),\n+    lists:reverse(Rows).\n+\n+\n+%% @doc Calculate a zero-based index of the row with UID in the result test.\n+%%\n+%% If the element does not exists, the ID of the next element will\n+%% be returned instead.\n+%% @end\n+-spec calc_index(Worker, RoomJID, Host, Filter, MessID) -> Count\n+    when\n+    Worker       :: worker(),\n+    RoomJID      :: jlib:jid(),\n+    Host         :: server_hostname(),\n+    Filter       :: filter(),\n+    MessID       :: message_id(),\n+    Count        :: non_neg_integer().\n+calc_index(Worker, RoomJID, Host, Filter, MessID) ->\n+    calc_count(Worker, RoomJID, Host, to_id(MessID, Filter)).\n+\n+%% @doc Count of elements in RSet before the passed element.\n+%%\n+%% The element with the passed UID can be already deleted.\n+%% @end\n+-spec calc_before(Worker, RoomJID, Host, Filter, MessID) -> Count\n+    when\n+    Worker       :: worker(),\n+    RoomJID      :: jlib:jid(),\n+    Host         :: server_hostname(),\n+    Filter       :: filter(),\n+    MessID       :: message_id(),\n+    Count        :: non_neg_integer().\n+calc_before(Worker, RoomJID, Host, Filter, MessID) ->\n+    calc_count(Worker, RoomJID, Host, before_id(MessID, Filter)).\n+\n+\n+%% @doc Get the total result set size.\n+%% \"SELECT COUNT(*) as \"count\" FROM mam_muc_message WHERE \"\n+-spec calc_count(Worker, RoomJID, Host, Filter) -> Count\n+    when\n+    Worker       :: worker(),\n+    RoomJID      :: jlib:jid(),\n+    Host         :: server_hostname(),\n+    Filter       :: filter(),\n+    Count        :: non_neg_integer().\n+calc_count(Worker, RoomJID, _Host, Filter) ->\n+    QueryName = {calc_count_query, select_filter(Filter)},\n+    Params = eval_filter_params(Filter),\n+    {ok, [[Count]]} = mongoose_cassandra_worker:cql_query(Worker, RoomJID, ?MODULE, QueryName, Params),\n+    Count.\n+\n+%% @doc Convert offset to index of the first entry\n+%% Returns undefined if not there are not enough rows\n+-spec offset_to_start_id(Worker, RoomJID, Filter, Offset) -> Id\n+    when\n+    Worker       :: worker(),\n+    RoomJID      :: jlib:jid(),\n+    Offset       :: non_neg_integer(),\n+    Filter       :: filter(),\n+    Id           :: non_neg_integer() | undefined.\n+offset_to_start_id(Worker, RoomJID, Filter, Offset) when is_integer(Offset), Offset >= 0 ->\n+    QueryName = {list_message_ids_query, select_filter(Filter)},\n+    Params = eval_filter_params(Filter) ++ [Offset+1],\n+    {ok, RowsIds} = mongoose_cassandra_worker:cql_query(Worker, RoomJID, ?MODULE, QueryName, Params),\n+    case RowsIds of\n+        [] -> unfefined;\n+        [_|_] -> [StartId] = lists:last(RowsIds), StartId\n+    end.\n+\n+prepare_filter(RoomJID, Borders, Start, End, WithNick) ->\n+    BRoomJID = bare_jid(RoomJID),\n+    StartID = maybe_encode_compact_uuid(Start, 0),\n+    EndID   = maybe_encode_compact_uuid(End, 255),\n+    StartID2 = apply_start_border(Borders, StartID),\n+    EndID2   = apply_end_border(Borders, EndID),\n+    BWithNick = maybe_nick(WithNick),\n+    prepare_filter_params(BRoomJID, BWithNick, StartID2, EndID2).\n+\n+prepare_filter_params(BRoomJID, BWithNick, StartID, EndID) ->\n+    #mam_muc_ca_filter{\n+        room_jid = BRoomJID,\n+        with_nick = BWithNick,\n+        start_id = StartID,\n+        end_id = EndID\n+    }.\n+\n+eval_filter_params(#mam_muc_ca_filter{\n+        room_jid = BRoomJID,\n+        with_nick = BWithNick,\n+        start_id = StartID,\n+        end_id = EndID\n+    }) ->\n+    Optional = [Value || Value <- [StartID, EndID], Value =/= undefined],\n+    [BRoomJID, BWithNick|Optional].\n+\n+select_filter(#mam_muc_ca_filter{\n+        start_id = StartID,\n+        end_id = EndID\n+    }) ->\n+    select_filter(StartID, EndID).\n+\n+\n+-spec select_filter(StartID, EndID) ->\n+    all  | 'end'  | start  | start_end when\n+      StartID :: integer()  | undefined,\n+      EndID :: integer()  | undefined.\n+select_filter(undefined, undefined) ->\n+    all;\n+select_filter(undefined, _) ->\n+    'end';\n+select_filter(_, undefined) ->\n+    start;\n+select_filter(_, _) ->\n+    start_end.\n+\n+prepare_filter_cql(StartID, EndID) ->\n+    case StartID of\n+       undefined -> \"\";\n+       _         -> \" AND id >= ?\"\n+    end ++\n+    case EndID of\n+       undefined -> \"\";\n+       _         -> \" AND id <= ?\"\n+    end.\n+\n+filter_to_cql() ->\n+    [{select_filter(StartID, EndID),\n+      prepare_filter_cql(StartID, EndID)}\n+     || StartID        <- [undefined, 0],\n+          EndID        <- [undefined, 0]].\n+\n+-spec calc_offset(Worker, RoomJID, Host, Filter, PageSize, TotalCount, RSM) -> Offset\n+    when\n+    Worker       :: worker(),\n+    RoomJID      :: jlib:jid(),\n+    Host         :: server_hostname(),\n+    Filter       :: filter(),\n+    PageSize     :: non_neg_integer(),\n+    TotalCount   :: non_neg_integer(),\n+    RSM          :: #rsm_in{} | undefined,\n+    Offset       :: non_neg_integer().\n+calc_offset(_W, _RoomJID, _LS, _F, _PS, _TC, #rsm_in{direction = undefined, index = Index})\n+    when is_integer(Index) ->\n+    Index;\n+%% Requesting the Last Page in a Result Set\n+calc_offset(_W, _RoomJID, _LS, _F, PS, TC, #rsm_in{direction = before, id = undefined}) ->\n+    max(0, TC - PS);\n+calc_offset(Worker, RoomJID, Host, F, PS, _TC, #rsm_in{direction = before, id = ID})\n+    when is_integer(ID) ->\n+    max(0, calc_before(Worker, RoomJID, Host, F, ID) - PS);\n+calc_offset(Worker, RoomJID, Host, F, _PS, _TC, #rsm_in{direction = aft, id = ID})\n+    when is_integer(ID) ->\n+    calc_index(Worker, RoomJID, Host, F, ID);\n+calc_offset(_W, _RoomJID, _LS, _F, _PS, _TC, _RSM) ->\n+    0.\n+\n+maybe_encode_compact_uuid(undefined, _) ->\n+    undefined;\n+maybe_encode_compact_uuid(Microseconds, NodeID) ->\n+    encode_compact_uuid(Microseconds, NodeID).\n+\n+maybe_nick(undefined) ->\n+    <<>>;\n+maybe_nick(WithNick) when is_binary(WithNick) ->\n+    WithNick.\n+\n+bare_jid(undefined) -> undefined;\n+bare_jid(JID) ->\n+    jid:to_binary(jid:to_bare(jid:to_lower(JID))).\n+\n+\n+%%====================================================================\n+%% Internal SQL part\n+%%====================================================================\n+\n+extract_messages_queries() ->\n+    [{{extract_messages_query, FilterName}, extract_messages_cql(Filter)}\n+     || {FilterName, Filter} <- filter_to_cql()].\n+\n+extract_messages_r_queries() ->\n+    [{{extract_messages_r_query, FilterName}, extract_messages_r_cql(Filter)}\n+     || {FilterName, Filter} <- filter_to_cql()].\n+\n+calc_count_queries() ->\n+    [{{calc_count_query, FilterName}, calc_count_cql(Filter)}\n+     || {FilterName, Filter} <- filter_to_cql()].\n+\n+list_message_ids_queries() ->\n+    [{{list_message_ids_query, FilterName}, list_message_ids_cql(Filter)}\n+     || {FilterName, Filter} <- filter_to_cql()].\n+\n+extract_messages_cql(Filter) ->\n+    \"SELECT id, nick_name, message FROM mam_muc_message \"\n+        \"WHERE room_jid = ? AND with_nick = ? \" ++\n+        Filter ++ \" ORDER BY with_nick, id LIMIT ?\".\n+\n+extract_messages_r_cql(Filter) ->\n+    \"SELECT id, nick_name, message FROM mam_muc_message \"\n+        \"WHERE room_jid = ? AND with_nick = ? \" ++\n+        Filter ++ \" ORDER BY with_nick DESC, id DESC LIMIT ?\".\n+\n+calc_count_cql(Filter) ->\n+    \"SELECT COUNT(*) FROM mam_muc_message \"\n+        \"WHERE room_jid = ? AND with_nick = ? \" ++ Filter.\n+\n+list_message_ids_cql(Filter) ->\n+    \"SELECT id FROM mam_muc_message \"\n+        \"WHERE room_jid = ? AND with_nick = ? \" ++ Filter ++\n+        \" ORDER BY with_nick, id LIMIT ?\".\n+\n+%% ----------------------------------------------------------------------\n+%% Optimizations\n+\n+packet_to_stored_binary(Packet) ->\n+    %% Module implementing mam_muc_message behaviour\n+    Module = db_message_format(),\n+    Module:encode(Packet).\n+\n+stored_binary_to_packet(Bin) ->\n+    %% Module implementing mam_muc_message behaviour\n+    Module = db_message_format(),\n+    Module:decode(Bin).","path":"apps/ejabberd/src/mod_mam_muc_cassandra_arch.erl","position":797,"original_position":797,"commit_id":"4b83e9218d9d72df0431ee248cf207fd3a84456e","original_commit_id":"4b83e9218d9d72df0431ee248cf207fd3a84456e","user":{"login":"gadgetci","id":10134338,"avatar_url":"https://avatars.githubusercontent.com/u/10134338?v=3","gravatar_id":"","url":"https://api.github.com/users/gadgetci","html_url":"https://github.com/gadgetci","followers_url":"https://api.github.com/users/gadgetci/followers","following_url":"https://api.github.com/users/gadgetci/following{/other_user}","gists_url":"https://api.github.com/users/gadgetci/gists{/gist_id}","starred_url":"https://api.github.com/users/gadgetci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gadgetci/subscriptions","organizations_url":"https://api.github.com/users/gadgetci/orgs","repos_url":"https://api.github.com/users/gadgetci/repos","events_url":"https://api.github.com/users/gadgetci/events{/privacy}","received_events_url":"https://api.github.com/users/gadgetci/received_events","type":"User","site_admin":false},"body":"According to **Elvis**:\n> Remove the dynamic function call on line 797. Only modules that define callbacks should make dynamic calls.","created_at":"2016-12-02T14:44:54Z","updated_at":"2016-12-02T14:44:54Z","html_url":"https://github.com/esl/MongooseIM/pull/1098#discussion_r90653015","pull_request_url":"https://api.github.com/repos/esl/MongooseIM/pulls/1098","_links":{"self":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/comments/90653015"},"html":{"href":"https://github.com/esl/MongooseIM/pull/1098#discussion_r90653015"},"pull_request":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1098"}}},"pull_request":{"url":"https://api.github.com/repos/esl/MongooseIM/pulls/1098","id":96291424,"html_url":"https://github.com/esl/MongooseIM/pull/1098","diff_url":"https://github.com/esl/MongooseIM/pull/1098.diff","patch_url":"https://github.com/esl/MongooseIM/pull/1098.patch","issue_url":"https://api.github.com/repos/esl/MongooseIM/issues/1098","number":1098,"state":"open","locked":false,"title":"Add cassandra mam backend","user":{"login":"RoXeon","id":1565663,"avatar_url":"https://avatars.githubusercontent.com/u/1565663?v=3","gravatar_id":"","url":"https://api.github.com/users/RoXeon","html_url":"https://github.com/RoXeon","followers_url":"https://api.github.com/users/RoXeon/followers","following_url":"https://api.github.com/users/RoXeon/following{/other_user}","gists_url":"https://api.github.com/users/RoXeon/gists{/gist_id}","starred_url":"https://api.github.com/users/RoXeon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/RoXeon/subscriptions","organizations_url":"https://api.github.com/users/RoXeon/orgs","repos_url":"https://api.github.com/users/RoXeon/repos","events_url":"https://api.github.com/users/RoXeon/events{/privacy}","received_events_url":"https://api.github.com/users/RoXeon/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-12-02T14:44:29Z","updated_at":"2016-12-02T14:44:54Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c5c0cc244c7896ad14e233c177252a37cace134d","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/esl/MongooseIM/pulls/1098/commits","review_comments_url":"https://api.github.com/repos/esl/MongooseIM/pulls/1098/comments","review_comment_url":"https://api.github.com/repos/esl/MongooseIM/pulls/comments{/number}","comments_url":"https://api.github.com/repos/esl/MongooseIM/issues/1098/comments","statuses_url":"https://api.github.com/repos/esl/MongooseIM/statuses/4b83e9218d9d72df0431ee248cf207fd3a84456e","head":{"label":"esl:arc-mam-ca-ci-rebased2","ref":"arc-mam-ca-ci-rebased2","sha":"4b83e9218d9d72df0431ee248cf207fd3a84456e","user":{"login":"esl","id":283412,"avatar_url":"https://avatars.githubusercontent.com/u/283412?v=3","gravatar_id":"","url":"https://api.github.com/users/esl","html_url":"https://github.com/esl","followers_url":"https://api.github.com/users/esl/followers","following_url":"https://api.github.com/users/esl/following{/other_user}","gists_url":"https://api.github.com/users/esl/gists{/gist_id}","starred_url":"https://api.github.com/users/esl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/esl/subscriptions","organizations_url":"https://api.github.com/users/esl/orgs","repos_url":"https://api.github.com/users/esl/repos","events_url":"https://api.github.com/users/esl/events{/privacy}","received_events_url":"https://api.github.com/users/esl/received_events","type":"Organization","site_admin":false},"repo":{"id":2354240,"name":"MongooseIM","full_name":"esl/MongooseIM","owner":{"login":"esl","id":283412,"avatar_url":"https://avatars.githubusercontent.com/u/283412?v=3","gravatar_id":"","url":"https://api.github.com/users/esl","html_url":"https://github.com/esl","followers_url":"https://api.github.com/users/esl/followers","following_url":"https://api.github.com/users/esl/following{/other_user}","gists_url":"https://api.github.com/users/esl/gists{/gist_id}","starred_url":"https://api.github.com/users/esl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/esl/subscriptions","organizations_url":"https://api.github.com/users/esl/orgs","repos_url":"https://api.github.com/users/esl/repos","events_url":"https://api.github.com/users/esl/events{/privacy}","received_events_url":"https://api.github.com/users/esl/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/esl/MongooseIM","description":"MongooseIM is a mobile messaging platform with focus on performance and scalability","fork":false,"url":"https://api.github.com/repos/esl/MongooseIM","forks_url":"https://api.github.com/repos/esl/MongooseIM/forks","keys_url":"https://api.github.com/repos/esl/MongooseIM/keys{/key_id}","collaborators_url":"https://api.github.com/repos/esl/MongooseIM/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/esl/MongooseIM/teams","hooks_url":"https://api.github.com/repos/esl/MongooseIM/hooks","issue_events_url":"https://api.github.com/repos/esl/MongooseIM/issues/events{/number}","events_url":"https://api.github.com/repos/esl/MongooseIM/events","assignees_url":"https://api.github.com/repos/esl/MongooseIM/assignees{/user}","branches_url":"https://api.github.com/repos/esl/MongooseIM/branches{/branch}","tags_url":"https://api.github.com/repos/esl/MongooseIM/tags","blobs_url":"https://api.github.com/repos/esl/MongooseIM/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/esl/MongooseIM/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/esl/MongooseIM/git/refs{/sha}","trees_url":"https://api.github.com/repos/esl/MongooseIM/git/trees{/sha}","statuses_url":"https://api.github.com/repos/esl/MongooseIM/statuses/{sha}","languages_url":"https://api.github.com/repos/esl/MongooseIM/languages","stargazers_url":"https://api.github.com/repos/esl/MongooseIM/stargazers","contributors_url":"https://api.github.com/repos/esl/MongooseIM/contributors","subscribers_url":"https://api.github.com/repos/esl/MongooseIM/subscribers","subscription_url":"https://api.github.com/repos/esl/MongooseIM/subscription","commits_url":"https://api.github.com/repos/esl/MongooseIM/commits{/sha}","git_commits_url":"https://api.github.com/repos/esl/MongooseIM/git/commits{/sha}","comments_url":"https://api.github.com/repos/esl/MongooseIM/comments{/number}","issue_comment_url":"https://api.github.com/repos/esl/MongooseIM/issues/comments{/number}","contents_url":"https://api.github.com/repos/esl/MongooseIM/contents/{+path}","compare_url":"https://api.github.com/repos/esl/MongooseIM/compare/{base}...{head}","merges_url":"https://api.github.com/repos/esl/MongooseIM/merges","archive_url":"https://api.github.com/repos/esl/MongooseIM/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/esl/MongooseIM/downloads","issues_url":"https://api.github.com/repos/esl/MongooseIM/issues{/number}","pulls_url":"https://api.github.com/repos/esl/MongooseIM/pulls{/number}","milestones_url":"https://api.github.com/repos/esl/MongooseIM/milestones{/number}","notifications_url":"https://api.github.com/repos/esl/MongooseIM/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/esl/MongooseIM/labels{/name}","releases_url":"https://api.github.com/repos/esl/MongooseIM/releases{/id}","deployments_url":"https://api.github.com/repos/esl/MongooseIM/deployments","created_at":"2011-09-09T08:19:18Z","updated_at":"2016-12-02T09:04:26Z","pushed_at":"2016-12-02T14:44:29Z","git_url":"git://github.com/esl/MongooseIM.git","ssh_url":"git@github.com:esl/MongooseIM.git","clone_url":"https://github.com/esl/MongooseIM.git","svn_url":"https://github.com/esl/MongooseIM","homepage":"","size":33809,"stargazers_count":622,"watchers_count":622,"language":"Erlang","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":244,"mirror_url":null,"open_issues_count":97,"forks":244,"open_issues":97,"watchers":622,"default_branch":"master"}},"base":{"label":"esl:master","ref":"master","sha":"d20ec88346cc339793ea24e4e39920fb5fb1d727","user":{"login":"esl","id":283412,"avatar_url":"https://avatars.githubusercontent.com/u/283412?v=3","gravatar_id":"","url":"https://api.github.com/users/esl","html_url":"https://github.com/esl","followers_url":"https://api.github.com/users/esl/followers","following_url":"https://api.github.com/users/esl/following{/other_user}","gists_url":"https://api.github.com/users/esl/gists{/gist_id}","starred_url":"https://api.github.com/users/esl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/esl/subscriptions","organizations_url":"https://api.github.com/users/esl/orgs","repos_url":"https://api.github.com/users/esl/repos","events_url":"https://api.github.com/users/esl/events{/privacy}","received_events_url":"https://api.github.com/users/esl/received_events","type":"Organization","site_admin":false},"repo":{"id":2354240,"name":"MongooseIM","full_name":"esl/MongooseIM","owner":{"login":"esl","id":283412,"avatar_url":"https://avatars.githubusercontent.com/u/283412?v=3","gravatar_id":"","url":"https://api.github.com/users/esl","html_url":"https://github.com/esl","followers_url":"https://api.github.com/users/esl/followers","following_url":"https://api.github.com/users/esl/following{/other_user}","gists_url":"https://api.github.com/users/esl/gists{/gist_id}","starred_url":"https://api.github.com/users/esl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/esl/subscriptions","organizations_url":"https://api.github.com/users/esl/orgs","repos_url":"https://api.github.com/users/esl/repos","events_url":"https://api.github.com/users/esl/events{/privacy}","received_events_url":"https://api.github.com/users/esl/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/esl/MongooseIM","description":"MongooseIM is a mobile messaging platform with focus on performance and scalability","fork":false,"url":"https://api.github.com/repos/esl/MongooseIM","forks_url":"https://api.github.com/repos/esl/MongooseIM/forks","keys_url":"https://api.github.com/repos/esl/MongooseIM/keys{/key_id}","collaborators_url":"https://api.github.com/repos/esl/MongooseIM/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/esl/MongooseIM/teams","hooks_url":"https://api.github.com/repos/esl/MongooseIM/hooks","issue_events_url":"https://api.github.com/repos/esl/MongooseIM/issues/events{/number}","events_url":"https://api.github.com/repos/esl/MongooseIM/events","assignees_url":"https://api.github.com/repos/esl/MongooseIM/assignees{/user}","branches_url":"https://api.github.com/repos/esl/MongooseIM/branches{/branch}","tags_url":"https://api.github.com/repos/esl/MongooseIM/tags","blobs_url":"https://api.github.com/repos/esl/MongooseIM/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/esl/MongooseIM/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/esl/MongooseIM/git/refs{/sha}","trees_url":"https://api.github.com/repos/esl/MongooseIM/git/trees{/sha}","statuses_url":"https://api.github.com/repos/esl/MongooseIM/statuses/{sha}","languages_url":"https://api.github.com/repos/esl/MongooseIM/languages","stargazers_url":"https://api.github.com/repos/esl/MongooseIM/stargazers","contributors_url":"https://api.github.com/repos/esl/MongooseIM/contributors","subscribers_url":"https://api.github.com/repos/esl/MongooseIM/subscribers","subscription_url":"https://api.github.com/repos/esl/MongooseIM/subscription","commits_url":"https://api.github.com/repos/esl/MongooseIM/commits{/sha}","git_commits_url":"https://api.github.com/repos/esl/MongooseIM/git/commits{/sha}","comments_url":"https://api.github.com/repos/esl/MongooseIM/comments{/number}","issue_comment_url":"https://api.github.com/repos/esl/MongooseIM/issues/comments{/number}","contents_url":"https://api.github.com/repos/esl/MongooseIM/contents/{+path}","compare_url":"https://api.github.com/repos/esl/MongooseIM/compare/{base}...{head}","merges_url":"https://api.github.com/repos/esl/MongooseIM/merges","archive_url":"https://api.github.com/repos/esl/MongooseIM/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/esl/MongooseIM/downloads","issues_url":"https://api.github.com/repos/esl/MongooseIM/issues{/number}","pulls_url":"https://api.github.com/repos/esl/MongooseIM/pulls{/number}","milestones_url":"https://api.github.com/repos/esl/MongooseIM/milestones{/number}","notifications_url":"https://api.github.com/repos/esl/MongooseIM/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/esl/MongooseIM/labels{/name}","releases_url":"https://api.github.com/repos/esl/MongooseIM/releases{/id}","deployments_url":"https://api.github.com/repos/esl/MongooseIM/deployments","created_at":"2011-09-09T08:19:18Z","updated_at":"2016-12-02T09:04:26Z","pushed_at":"2016-12-02T14:44:29Z","git_url":"git://github.com/esl/MongooseIM.git","ssh_url":"git@github.com:esl/MongooseIM.git","clone_url":"https://github.com/esl/MongooseIM.git","svn_url":"https://github.com/esl/MongooseIM","homepage":"","size":33809,"stargazers_count":622,"watchers_count":622,"language":"Erlang","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":244,"mirror_url":null,"open_issues_count":97,"forks":244,"open_issues":97,"watchers":622,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1098"},"html":{"href":"https://github.com/esl/MongooseIM/pull/1098"},"issue":{"href":"https://api.github.com/repos/esl/MongooseIM/issues/1098"},"comments":{"href":"https://api.github.com/repos/esl/MongooseIM/issues/1098/comments"},"review_comments":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1098/comments"},"review_comment":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1098/commits"},"statuses":{"href":"https://api.github.com/repos/esl/MongooseIM/statuses/4b83e9218d9d72df0431ee248cf207fd3a84456e"}}}}
{"actor":{"display_login":"gadgetci"}}