1:- module(bitrix24_auth, [
    2          load_context/3,
    3          check_context/3,
    4          refresh_context/3,
    5          context_expiring_soon/1
    6      ]).    7
    8:- use_module(library(http/http_client)).    9:- use_module(bitrix24_utils).   10
   11load_context(Provider, ContextRef, Context) :-
   12    call(Provider:load_context(ContextRef, Context)).
   13
   14check_context(Provider, ContextRef, Context) :-
   15    load_context(Provider, ContextRef, Context0),
   16    (   context_expiring_soon(Context0)
   17    ->  refresh_context(Provider, ContextRef, Context)
   18    ;   Context = Context0
   19    ).
   20
   21context_expiring_soon(Context) :-
   22    get_dict(expires_at, Context, ExpiresAt0),
   23    normalize_integer(ExpiresAt0, ExpiresAt),
   24    get_time(Now),
   25    Diff is ExpiresAt - Now,
   26    Diff < 100.
   27
   28refresh_context(Provider, ContextRef, Context) :-
   29    load_context(Provider, ContextRef, Context0),
   30    call(Provider:get_config(client_id, ClientID)),
   31    call(Provider:get_config(client_secret, ClientSecret)),
   32    oauth_token_url(Provider, Url),
   33    RefreshToken = Context0.refresh_token,
   34    Attempts = 3,
   35    refresh_attempt(Attempts, Url, ClientID, ClientSecret, RefreshToken, Context0, Context),
   36    call(Provider:save_context(ContextRef, Context)).
   37
   38oauth_token_url(Provider, Url) :-
   39    (   call(Provider:get_config(oauth_token_url, Url0))
   40    ->  Url = Url0
   41    ;   Url = 'https://oauth.bitrix.info/oauth/token/'
   42    ).
   43
   44refresh_attempt(AttemptsLeft, Url, ClientID, ClientSecret, RefreshToken, Context0, Context) :-
   45    Body = form([ grant_type=refresh_token,
   46                  client_id=ClientID,
   47                  client_secret=ClientSecret,
   48                  refresh_token=RefreshToken
   49                ]),
   50    catch(
   51        http_post(Url, Body, Reply, [status_code(StatusCode)]),
   52        Error,
   53        true
   54    ),
   55    (   var(Error)
   56    ->  handle_refresh_reply(StatusCode, Reply, Context0, Context, AttemptsLeft,
   57                             Url, ClientID, ClientSecret, RefreshToken)
   58    ;   handle_refresh_transport_error(Error, AttemptsLeft, Url, ClientID, ClientSecret,
   59                                       RefreshToken, Context0, Context)
   60    ).
   61
   62handle_refresh_transport_error(Error, AttemptsLeft, Url, ClientID, ClientSecret, RefreshToken,
   63                               Context0, Context) :-
   64    (   AttemptsLeft > 1
   65    ->  sleep(1),
   66        NextAttempts is AttemptsLeft - 1,
   67        refresh_attempt(NextAttempts, Url, ClientID, ClientSecret, RefreshToken, Context0, Context)
   68    ;   throw(error(bitrix24_auth_transport_error(Error), _))
   69    ).
   70
   71handle_refresh_reply(200, Reply, Context0, Context, _AttemptsLeft, _Url, _ClientID,
   72                     _ClientSecret, _RefreshToken) :-
   73    !,
   74    bitrix24_utils:decode_response(Reply, Response),
   75    merge_refresh_response(Context0, Response, Context).
   76handle_refresh_reply(StatusCode, Reply, Context0, Context, AttemptsLeft, Url, ClientID,
   77                     ClientSecret, RefreshToken) :-
   78    bitrix24_utils:decode_response(Reply, Response),
   79    (   AttemptsLeft > 1
   80    ->  sleep(1),
   81        NextAttempts is AttemptsLeft - 1,
   82        refresh_attempt(NextAttempts, Url, ClientID, ClientSecret, RefreshToken, Context0, Context)
   83    ;   throw(error(bitrix24_auth_failed(StatusCode, Response), _))
   84    ).
   85
   86merge_refresh_response(Context0, Response, Context) :-
   87    response_field(Response, access_token, AccessToken),
   88    response_field(Response, refresh_token, RefreshToken),
   89    response_field(Response, expires_in, ExpiresIn0),
   90    normalize_integer(ExpiresIn0, ExpiresIn),
   91    get_time(Now),
   92    ExpiresAt is floor(Now) + ExpiresIn - 60,
   93    Context1 = Context0.put(_{
   94        access_token: AccessToken,
   95        refresh_token: RefreshToken,
   96        expires_at: ExpiresAt
   97    }),
   98    maybe_put_field(Context1, Response, client_endpoint, Context2),
   99    maybe_put_field(Context2, Response, server_endpoint, Context3),
  100    maybe_put_field(Context3, Response, domain, Context4),
  101    maybe_put_field(Context4, Response, member_id, Context5),
  102    maybe_put_field(Context5, Response, user_id, Context).
  103
  104maybe_put_field(Context0, Response, Field, Context) :-
  105    (   response_optional_field(Response, Field, Value)
  106    ->  Context = Context0.put(Field, Value)
  107    ;   Context = Context0
  108    ).
  109
  110response_field(Response, Field, Value) :-
  111    is_dict(Response),
  112    !,
  113    get_dict(Field, Response, Value).
  114response_field(Response, Field, Value) :-
  115    memberchk(Field=Value, Response).
  116
  117response_optional_field(Response, Field, Value) :-
  118    is_dict(Response),
  119    !,
  120    get_dict(Field, Response, Value).
  121response_optional_field(Response, Field, Value) :-
  122    memberchk(Field=Value, Response).
  123
  124normalize_integer(Value, Integer) :-
  125    integer(Value),
  126    !,
  127    Integer = Value.
  128normalize_integer(Value, Integer) :-
  129    number(Value),
  130    !,
  131    Integer is floor(Value).
  132normalize_integer(Value, Integer) :-
  133    string(Value),
  134    !,
  135    number_string(Integer, Value).
  136normalize_integer(Value, Integer) :-
  137    atom(Value),
  138    atom_number(Value, Integer)