{"action":"created","comment":{"url":"https://api.github.com/repos/esl/MongooseIM/pulls/comments/97556026","pull_request_review_id":18168264,"id":97556026,"diff_hunk":"@@ -0,0 +1,484 @@\n+%%%----------------------------------------------------------------------\n+%%% File    : ejabberd_odbc.erl\n+%%% Author  : Alexey Shchepin <alexey@process-one.net>\n+%%% Purpose : Serve ODBC connection\n+%%% Created :  8 Dec 2004 by Alexey Shchepin <alexey@process-one.net>\n+%%%\n+%%%\n+%%% ejabberd, Copyright (C) 2002-2011   ProcessOne\n+%%%\n+%%% This program is free software; you can redistribute it and/or\n+%%% modify it under the terms of the GNU General Public License as\n+%%% published by the Free Software Foundation; either version 2 of the\n+%%% License, or (at your option) any later version.\n+%%%\n+%%% This program is distributed in the hope that it will be useful,\n+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of\n+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n+%%% General Public License for more details.\n+%%%\n+%%% You should have received a copy of the GNU General Public License\n+%%% along with this program; if not, write to the Free Software\n+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA\n+%%% 02111-1307 USA\n+%%%\n+%%%----------------------------------------------------------------------\n+\n+-module(ejabberd_odbc).\n+-author('alexey@process-one.net').\n+\n+-behaviour(gen_server).\n+\n+-callback escape_format(Host :: ejabberd:server()) -> atom().\n+-callback connect(Args :: any()) ->\n+    {ok, Connection :: term()} | {error, Reason :: any()}.\n+-callback disconnect(Connection :: term()) -> any().\n+-callback query(Connection :: term(), Query :: any()) -> term().\n+\n+%% External exports\n+-export([sql_query/2,\n+         sql_query_t/1,\n+         sql_transaction/2,\n+         sql_bloc/2,\n+         escape/1,\n+         escape_like/1,\n+         to_bool/1,\n+         db_engine/1,\n+         print_state/1]).\n+\n+%% BLOB escaping\n+-export([escape_format/1,\n+         escape_binary/2,\n+         unescape_binary/2,\n+         unescape_odbc_binary/2]).\n+\n+%% count / integra types decoding\n+-export([result_to_integer/1]).\n+\n+%% gen_server callbacks\n+-export([init/1,\n+         handle_call/3,\n+         handle_cast/2,\n+         handle_info/2,\n+         terminate/2,\n+         code_change/3]).\n+\n+%% internal usage\n+-export([get_db_info/1]).\n+\n+-include(\"ejabberd.hrl\").\n+\n+-record(state, {db_ref,\n+                db_type :: atom(),\n+                host :: ejabberd:server(),\n+                backend :: module()\n+               }).\n+-type state() :: #state{}.\n+\n+-define(STATE_KEY, ejabberd_odbc_state).\n+-define(MAX_TRANSACTION_RESTARTS, 10).\n+\n+-define(TRANSACTION_TIMEOUT, 60000). % milliseconds\n+-define(KEEPALIVE_TIMEOUT, 60000).\n+-define(KEEPALIVE_QUERY, <<\"SELECT 1;\">>).\n+\n+%% Points to ODBC server process\n+-type odbc_server() :: binary() | atom().\n+-type odbc_msg() :: {sql_bloc, _} | {sql_query, _} | {sql_transaction, fun()}.\n+\n+%%%----------------------------------------------------------------------\n+%%% API\n+%%%----------------------------------------------------------------------\n+\n+-spec sql_query(HostOrPool :: odbc_server(), Query :: any()) -> any().\n+sql_query(HostOrPool, Query) ->\n+    sql_call(HostOrPool, {sql_query, Query}).\n+\n+%% @doc SQL transaction based on a list of queries\n+-spec sql_transaction(odbc_server(), fun() | maybe_improper_list()) -> any().\n+sql_transaction(HostOrPool, Queries) when is_list(Queries) ->\n+    F = fun() -> lists:map(fun sql_query_t/1, Queries) end,\n+    sql_transaction(HostOrPool, F);\n+%% SQL transaction, based on a erlang anonymous function (F = fun)\n+sql_transaction(HostOrPool, F) when is_function(F) ->\n+    sql_call(HostOrPool, {sql_transaction, F}).\n+\n+%% @doc SQL bloc, based on a erlang anonymous function (F = fun)\n+-spec sql_bloc(HostOrPool :: odbc_server(), F :: any()) -> any().\n+sql_bloc(HostOrPool, F) ->\n+    sql_call(HostOrPool, {sql_bloc, F}).\n+\n+\n+%% TODO: Better spec for RPC calls\n+-spec sql_call(HostOrPool :: binary() | atom(), Msg :: odbc_msg()) -> any().\n+sql_call(HostOrPool, Msg) ->\n+    case get(?STATE_KEY) of\n+        undefined -> sql_call0(HostOrPool, Msg);\n+        State     -> nested_op(Msg, State)\n+    end.\n+\n+\n+-spec sql_call0(HostOrPool :: binary() | atom(), Msg :: odbc_msg()) -> any().\n+sql_call0(Host, Msg) when is_binary(Host) ->\n+    sql_call0(ejabberd_odbc_sup:default_pool(Host), Msg);\n+sql_call0(Pool, Msg) when is_atom(Pool) ->\n+    case whereis(Pool) of\n+        undefined -> {error, {no_odbc_pool, Pool}};\n+        _ ->\n+            Timestamp = p1_time_compat:monotonic_time(milli_seconds),\n+            wpool:call(Pool, {sql_cmd, Msg, Timestamp}, best_worker, ?TRANSACTION_TIMEOUT)\n+    end.\n+\n+\n+-spec get_db_info(Target :: odbc_server() | pid()) ->\n+                         {ok, DbType :: atom(), DbRef :: term()} | {error, any()}.\n+get_db_info(Host) when is_binary(Host) ->\n+    get_db_info(ejabberd_odbc_sup:default_pool(Host));\n+get_db_info(Pool) when is_atom(Pool) ->\n+    case whereis(Pool) of\n+        undefined -> {error, {no_odbc_pool, Pool}};\n+        _ -> wpool:call(Pool, get_db_info)\n+    end;\n+get_db_info(Pid) when is_pid(Pid) ->\n+    wpool_process:call(Pid, get_db_info, 5000).\n+\n+\n+\n+%% This function is intended to be used from inside an sql_transaction:\n+sql_query_t(Query) ->\n+    sql_query_t(Query, get(?STATE_KEY)).\n+\n+sql_query_t(Query, State) ->\n+    QRes = sql_query_internal(Query, State),\n+    case QRes of\n+        {error, Reason} ->\n+            throw({aborted, Reason});\n+        _ when is_list(QRes) ->\n+            case lists:keysearch(error, 1, QRes) of\n+                {value, {error, Reason}} ->\n+                    throw({aborted, Reason});\n+                _ ->\n+                    QRes\n+            end;\n+        _ ->\n+            QRes\n+    end.\n+\n+\n+%% @doc Escape character that will confuse an SQL engine\n+-spec escape(binary() | string()) -> binary() | string().\n+escape(S) ->\n+    odbc_queries:escape_string(S).\n+\n+\n+%% @doc Escape character that will confuse an SQL engine\n+%% Percent and underscore only need to be escaped for\n+%% pattern matching like statement\n+%% INFO: Used in mod_vcard_odbc.\n+-spec escape_like(binary() | string()) -> binary() | string().\n+escape_like(S) ->\n+    odbc_queries:escape_like_string(S).\n+\n+\n+-spec escape_format(odbc_server()) -> hex | simple_escape.\n+escape_format(Host) ->\n+    Backend = backend(Host),\n+    Backend:escape_format(Host).\n+\n+\n+-spec escape_binary('hex' | 'simple_escape', binary()) -> binary() | string().\n+escape_binary(hex, Bin) when is_binary(Bin) ->\n+    <<\"\\\\\\\\x\", (bin_to_hex:bin_to_hex(Bin))/binary>>;\n+escape_binary(mssql_hex, Bin) when is_binary(Bin) ->\n+    bin_to_hex:bin_to_hex(Bin);\n+escape_binary(simple_escape, Bin) when is_binary(Bin) ->\n+    escape(Bin).\n+\n+-spec unescape_binary('hex' | 'simple_escape', binary()) -> binary().\n+unescape_binary(hex, <<\"\\\\x\", Bin/binary>>) when is_binary(Bin) ->\n+    hex_to_bin(Bin);\n+unescape_binary(_, Bin) ->\n+    Bin.\n+\n+-spec unescape_odbc_binary(atom(), binary()) -> binary().\n+unescape_odbc_binary(odbc, Bin) when is_binary(Bin)->\n+    hex_to_bin(Bin);\n+unescape_odbc_binary(_, Bin) ->\n+    Bin.\n+\n+-spec result_to_integer(binary() | integer()) -> integer().\n+result_to_integer(Int) when is_integer(Int) ->\n+    Int;\n+result_to_integer(Bin) when is_binary(Bin) ->\n+    binary_to_integer(Bin).\n+\n+-spec hex_to_bin(binary()) -> <<_:_*1>>.\n+hex_to_bin(Bin) when is_binary(Bin) ->\n+    << <<(hex_to_int(X, Y))>> || <<X, Y>> <= Bin>>.\n+\n+-spec hex_to_int(byte(), byte()) -> integer().\n+hex_to_int(X, Y) when is_integer(X), is_integer(Y) ->\n+    list_to_integer([X, Y], 16).\n+\n+-spec to_bool(binary() | string() | atom() | integer() | any()) -> boolean().\n+to_bool(B) when is_binary(B) ->\n+    to_bool(binary_to_list(B));\n+to_bool(\"t\") -> true;\n+to_bool(\"true\") -> true;\n+to_bool(\"1\") -> true;\n+to_bool(true) -> true;\n+to_bool(1) -> true;\n+to_bool(_) -> false.\n+\n+%%%----------------------------------------------------------------------\n+%%% Callback functions from gen_server\n+%%%----------------------------------------------------------------------\n+-spec init(ejabberd:server()) -> {ok, state()}.\n+init(Host) ->\n+    process_flag(trap_exit, true),\n+    proc_lib:init_ack({ok, self()}),\n+    Backend = backend(Host),\n+    Settings = ejabberd_config:get_local_option({odbc_server, Host}),\n+    RetryAfterSeconds = get_start_interval(Host),\n+    {ok, DbRef} = connect(Backend, Settings, 3, RetryAfterSeconds),\n+    schedule_keepalive(Host),\n+    {ok, #state{db_type = db_engine(Host), host = Host, db_ref = DbRef, backend = Backend}}.\n+\n+\n+handle_call({sql_cmd, Command, Timestamp}, From, State) ->\n+    run_sql_cmd(Command, From, State, Timestamp);\n+handle_call(get_db_info, _, #state{db_ref = DbRef, db_type = DbType} = State) ->\n+    {reply, {ok, DbType, DbRef}, State};\n+handle_call(_Event, _From, State) ->\n+    {reply, {error, badarg}, State}.\n+\n+handle_cast(Request, State) ->\n+    ?WARNING_MSG(\"unexpected cast: ~p\", [Request]),\n+    {noreply, State}.\n+\n+code_change(_OldVsn, State, _Extra) ->\n+    {ok, State}.\n+\n+handle_info(keepalive, State) ->\n+    case sql_query_internal([?KEEPALIVE_QUERY], State) of\n+        {selected, _} ->\n+            schedule_keepalive(State#state.host),\n+            {noreply, State};\n+        {error, _} = Error ->\n+            {stop, {keepalive_failed, Error}, State}\n+    end;\n+handle_info({'EXIT', _Pid, _Reason} = Reason, State) ->\n+    {stop, Reason, State};\n+handle_info(Info, State) ->\n+    ?WARNING_MSG(\"unexpected info: ~p\", [Info]),\n+    {noreply, State}.\n+\n+-spec terminate(Reason :: term(), state()) -> any().\n+terminate(_Reason, #state{backend = Backend, db_ref = DbRef}) ->\n+    catch Backend:disconnect(DbRef).\n+\n+%%----------------------------------------------------------------------\n+%% Func: print_state/1\n+%% Purpose: Prepare the state to be printed on error log\n+%% Returns: State to print\n+%%----------------------------------------------------------------------\n+print_state(State) ->\n+    State.\n+%%%----------------------------------------------------------------------\n+%%% Internal functions\n+%%%----------------------------------------------------------------------\n+\n+-spec run_sql_cmd(Command :: any(), From :: any(), State :: state(), Timestamp :: integer()) ->\n+                         {reply, Reply :: any(), state()} | {stop, Reason :: term(), state()} |\n+                         {noreply, state()}.\n+run_sql_cmd(Command, _From, State, Timestamp) ->\n+    Now = p1_time_compat:monotonic_time(milli_seconds),\n+    case Now - Timestamp of\n+        Age when Age  < ?TRANSACTION_TIMEOUT ->\n+            abort_on_driver_error(outer_op(Command, State), State);\n+        Age ->\n+            ?ERROR_MSG(\"Database was not available or too slow,\"\n+                       \" discarding ~p milliseconds old request~n~p~n\",\n+                       [Age, Command]),\n+            {reply, {error, timeout}, State}\n+    end.\n+\n+%% @doc Only called by handle_call, only handles top level operations.\n+-spec outer_op({'sql_bloc', _} | {'sql_query', _} | {'sql_transaction', fun()}, state()) ->\n+                      {error | aborted | atomic, _}.\n+outer_op({sql_query, Query}, State) ->\n+    sql_query_internal(Query, State);\n+outer_op({sql_transaction, F}, State) ->\n+    outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, \"\", State);\n+outer_op({sql_bloc, F}, State) ->\n+    execute_bloc(F, State).\n+\n+%% @doc Called via sql_query/transaction/bloc from client code when inside a\n+%% nested operation\n+-spec nested_op({'sql_bloc', _} | {'sql_query', _} | {'sql_transaction', fun()}, state()) -> any().\n+nested_op({sql_query, Query}, State) ->\n+    %% XXX - use sql_query_t here insted? Most likely would break\n+    %% callers who expect {error, _} tuples (sql_query_t turns\n+    %% these into throws)\n+    sql_query_internal(Query, State);\n+nested_op({sql_transaction, F}, State) ->\n+    case in_transaction() of\n+        false ->\n+            %% First transaction inside a (series of) sql_blocs\n+            outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, \"\", State);\n+        true ->\n+            %% Transaction inside a transaction\n+            inner_transaction(F, State)\n+    end;\n+nested_op({sql_bloc, F}, State) ->\n+    execute_bloc(F, State).\n+\n+%% @doc Never retry nested transactions - only outer transactions\n+-spec inner_transaction(fun(), state()) -> {'EXIT', _} | {'aborted', _} | {'atomic', _}.\n+inner_transaction(F, _State) ->\n+    case catch F() of\n+        {aborted, Reason} ->\n+            {aborted, Reason};\n+        {'EXIT', Reason} ->\n+            {'EXIT', Reason};\n+        {atomic, Res} ->\n+            {atomic, Res};\n+        Res ->\n+            {atomic, Res}\n+    end.\n+\n+-spec outer_transaction(F :: fun(),\n+                        NRestarts :: 0..10,\n+                        Reason :: any(), state()) -> {'aborted', _} | {'atomic', _}.\n+outer_transaction(F, NRestarts, _Reason, State) ->\n+    sql_query_internal(odbc_queries:begin_trans(), State),\n+    put(?STATE_KEY, State),\n+    Result = (catch F()),\n+    erase(?STATE_KEY),\n+    case Result of\n+        {aborted, Reason} when NRestarts > 0 ->\n+            %% Retry outer transaction upto NRestarts times.\n+            sql_query_internal([<<\"rollback;\">>], State),\n+            outer_transaction(F, NRestarts - 1, Reason, State);\n+        {aborted, Reason} when NRestarts =:= 0 ->\n+            %% Too many retries of outer transaction.\n+            ?ERROR_MSG(\"SQL transaction restarts exceeded~n\"\n+                       \"** Restarts: ~p~n\"\n+                       \"** Last abort reason: ~p~n\"\n+                       \"** Stacktrace: ~p~n\"\n+                       \"** When State == ~p\",\n+                       [?MAX_TRANSACTION_RESTARTS, Reason,\n+                        erlang:get_stacktrace(), State]),\n+            sql_query_internal([<<\"rollback;\">>], State),\n+            {aborted, Reason};\n+        {'EXIT', Reason} ->\n+            %% Abort sql transaction on EXIT from outer txn only.\n+            sql_query_internal([<<\"rollback;\">>], State),\n+            {aborted, Reason};\n+        Res ->\n+            %% Commit successful outer txn\n+            sql_query_internal([<<\"commit;\">>], State),\n+            {atomic, Res}\n+    end.\n+\n+-spec execute_bloc(fun(), state()) -> {'aborted', _} | {'atomic', _}.\n+execute_bloc(F, _State) ->\n+    case catch F() of\n+        {aborted, Reason} ->\n+            {aborted, Reason};\n+        {'EXIT', Reason} ->\n+            {aborted, Reason};\n+        Res ->\n+            {atomic, Res}\n+    end.\n+\n+sql_query_internal(Query, #state{backend = Backend, db_ref = DBRef}) ->\n+    case Backend:query(DBRef, Query) of\n+        {error, \"No SQL-driver information available.\"} ->\n+            {updated, 0}; %% workaround for odbc bug\n+        Result ->\n+            Result\n+    end.\n+\n+%% @doc Generate the OTP callback return tuple depending on the driver result.\n+-spec abort_on_driver_error(_, state()) ->\n+                                   {reply, Reply :: term(), state()} |\n+                                   {stop, timeout | closed, state()}.\n+abort_on_driver_error({error, \"query timed out\"} = Reply, State) ->\n+    %% mysql driver error\n+    {stop, timeout, Reply, State};\n+abort_on_driver_error({error, \"Failed sending data on socket\" ++ _} = Reply, State) ->\n+    %% mysql driver error\n+    {stop, closed, Reply, State};\n+abort_on_driver_error(Reply, State) ->\n+    {reply, Reply, State}.\n+\n+\n+-spec db_engine(Host :: odbc_server()) -> ejabberd_config:value().\n+db_engine(Pool) when is_atom(Pool) ->\n+    {ok, DbType, _} = wpool:call(Pool, get_db_info),\n+    DbType;\n+db_engine(Host) when is_binary(Host) ->\n+    case ejabberd_config:get_local_option({odbc_server, Host}) of\n+        SQLServer when is_list(SQLServer) ->\n+            odbc;\n+        Other when is_tuple(Other) ->\n+            element(1, Other)\n+    end.\n+\n+\n+-spec connect(Backend :: module(), Settings :: term(), Retry :: non_neg_integer(),\n+              RetryAfterSeconds :: non_neg_integer()) -> {ok, term()} | {error, any()}.\n+connect(Backend, Settings, Retry, RetryAfterSeconds) ->\n+    case Backend:connect(Settings) of\n+        {ok, _} = Ok ->\n+            Ok;\n+        Error when Retry =:= 0 ->\n+            Error;\n+        Error ->\n+            ?ERROR_MSG(\"Database connection attempt with ~p resulted in ~p.\"\n+                       \" Retrying in ~p seconds.\", [Settings, Error, RetryAfterSeconds]),\n+            timer:sleep(timer:seconds(RetryAfterSeconds)),\n+            connect(Backend, Settings, Retry - 1, RetryAfterSeconds)\n+    end.\n+\n+\n+-spec backend(Host :: ejabberd:server()) -> module().\n+backend(Host) when is_binary(Host) ->\n+    Engine = atom_to_binary(db_engine(Host), latin1),\n+    binary_to_atom(<<\"ejabberd_odbc_\", Engine/binary>>, latin1).","path":"apps/ejabberd/src/odbc/ejabberd_odbc.erl","position":449,"original_position":449,"commit_id":"d36c970a256b311b71ed47e26fedb4947d659902","original_commit_id":"be739710c5458fcc3d85fdeed4e441da974559d4","user":{"login":"rslota","id":1565663,"avatar_url":"https://avatars.githubusercontent.com/u/1565663?v=3","gravatar_id":"","url":"https://api.github.com/users/rslota","html_url":"https://github.com/rslota","followers_url":"https://api.github.com/users/rslota/followers","following_url":"https://api.github.com/users/rslota/following{/other_user}","gists_url":"https://api.github.com/users/rslota/gists{/gist_id}","starred_url":"https://api.github.com/users/rslota/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rslota/subscriptions","organizations_url":"https://api.github.com/users/rslota/orgs","repos_url":"https://api.github.com/users/rslota/repos","events_url":"https://api.github.com/users/rslota/events{/privacy}","received_events_url":"https://api.github.com/users/rslota/received_events","type":"User","site_admin":false},"body":"In my humble opinion even if it wouldn't, we should find a way to make it work (e.g. defining somewhere list of possible engines).  Every *_to_atom with variable argument is a threat in some way. ","created_at":"2017-01-24T14:24:22Z","updated_at":"2017-01-24T14:24:23Z","html_url":"https://github.com/esl/MongooseIM/pull/1154#discussion_r97556026","pull_request_url":"https://api.github.com/repos/esl/MongooseIM/pulls/1154","_links":{"self":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/comments/97556026"},"html":{"href":"https://github.com/esl/MongooseIM/pull/1154#discussion_r97556026"},"pull_request":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1154"}}},"pull_request":{"url":"https://api.github.com/repos/esl/MongooseIM/pulls/1154","id":101711384,"html_url":"https://github.com/esl/MongooseIM/pull/1154","diff_url":"https://github.com/esl/MongooseIM/pull/1154.diff","patch_url":"https://github.com/esl/MongooseIM/pull/1154.patch","issue_url":"https://api.github.com/repos/esl/MongooseIM/issues/1154","number":1154,"state":"closed","locked":false,"title":"Refactor ejabberd odbc","user":{"login":"kzemek","id":1658995,"avatar_url":"https://avatars.githubusercontent.com/u/1658995?v=3","gravatar_id":"","url":"https://api.github.com/users/kzemek","html_url":"https://github.com/kzemek","followers_url":"https://api.github.com/users/kzemek/followers","following_url":"https://api.github.com/users/kzemek/following{/other_user}","gists_url":"https://api.github.com/users/kzemek/gists{/gist_id}","starred_url":"https://api.github.com/users/kzemek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kzemek/subscriptions","organizations_url":"https://api.github.com/users/kzemek/orgs","repos_url":"https://api.github.com/users/kzemek/repos","events_url":"https://api.github.com/users/kzemek/events{/privacy}","received_events_url":"https://api.github.com/users/kzemek/received_events","type":"User","site_admin":false},"body":"This PR refactors ejabberd_odbc module. The highlights are:\r\n* Introduces new drivers with parametrized queries support:\r\n  * [MySQL driver](https://github.com/dizzyd/erlang-mysql-driver)\r\n  * [PostgreSQL driver](http://github.com/semiocast/pgsql)\r\n* Replaces manual worker management with a [worker_pool](https://github.com/inaka/worker_pool) library\r\n* Splits SQL worker backends into separate modules implementing a behaviour\r\n* Changes underlying worker implementation to gen_server from p1_fsm\r\n* General code style and quality improvements","created_at":"2017-01-16T15:33:58Z","updated_at":"2017-01-24T14:24:23Z","closed_at":"2017-01-24T10:24:56Z","merged_at":null,"merge_commit_sha":"49d34214d9e5957ac061a3afbf828c99f0efe560","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/esl/MongooseIM/pulls/1154/commits","review_comments_url":"https://api.github.com/repos/esl/MongooseIM/pulls/1154/comments","review_comment_url":"https://api.github.com/repos/esl/MongooseIM/pulls/comments{/number}","comments_url":"https://api.github.com/repos/esl/MongooseIM/issues/1154/comments","statuses_url":"https://api.github.com/repos/esl/MongooseIM/statuses/d36c970a256b311b71ed47e26fedb4947d659902","head":{"label":"esl:refactor_ejabberd_odbc","ref":"refactor_ejabberd_odbc","sha":"d36c970a256b311b71ed47e26fedb4947d659902","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":"2017-01-24T06:19:02Z","pushed_at":"2017-01-24T14:05:57Z","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":22583,"stargazers_count":663,"watchers_count":663,"language":"Erlang","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":252,"mirror_url":null,"open_issues_count":70,"forks":252,"open_issues":70,"watchers":663,"default_branch":"master"}},"base":{"label":"esl:master","ref":"master","sha":"e8765d10d359500c705be59294379664b725625a","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":"2017-01-24T06:19:02Z","pushed_at":"2017-01-24T14:05:57Z","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":22583,"stargazers_count":663,"watchers_count":663,"language":"Erlang","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":252,"mirror_url":null,"open_issues_count":70,"forks":252,"open_issues":70,"watchers":663,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1154"},"html":{"href":"https://github.com/esl/MongooseIM/pull/1154"},"issue":{"href":"https://api.github.com/repos/esl/MongooseIM/issues/1154"},"comments":{"href":"https://api.github.com/repos/esl/MongooseIM/issues/1154/comments"},"review_comments":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1154/comments"},"review_comment":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/esl/MongooseIM/pulls/1154/commits"},"statuses":{"href":"https://api.github.com/repos/esl/MongooseIM/statuses/d36c970a256b311b71ed47e26fedb4947d659902"}}}}
{"actor":{"display_login":"rslota"}}