src/support/z_mqtt_ticket.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2020 Marc Worrell
%% @doc Tickets for MQTT out of band publish via HTTP
%% @end

%% Copyright 2020 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(z_mqtt_ticket).

-behaviour(gen_server).

-export([
    start_link/1,
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    code_change/3,
    terminate/2
    ]).

-export([
    new_ticket/1,
    exchange_ticket/2,
    delete_ticket/2
    ]).

% Unused tickets timeout in 30 seconds
-define(TICKET_TIMEOUT, 30000).

-record(state, {
    site :: atom(),
    tickets :: #{ binary() := z:context()}
    }).

%%====================================================================
%% API
%%====================================================================

%% @doc Return a new ticket id for out of band MQTT calls.
-spec new_ticket( z:context() ) -> {ok, binary()} | {error, term()}.
new_ticket(Context) ->
    Name = z_utils:name_for_site(?MODULE, Context),
    Context1 = z_context:prune_for_scomp(Context),
    gen_server:call(Name, {new_ticket, Context1}).


%% @doc Exchange a ticket for a stored context record.
-spec exchange_ticket( binary(), z:context() ) -> {ok, z:context()} | {error, term()}.
exchange_ticket(Ticket, Context) ->
    Name = z_utils:name_for_site(?MODULE, Context),
    gen_server:call(Name, {exchange_ticket, Ticket}).

%% @doc Delete a stored context record.
-spec delete_ticket( binary(), z:context() ) -> ok | {error, term()}.
delete_ticket(Ticket, Context) ->
    Name = z_utils:name_for_site(?MODULE, Context),
    gen_server:call(Name, {delete_ticket, Ticket}).

%% @doc Starts the MQTT ticket server server
start_link(Site) ->
    Name = z_utils:name_for_site(?MODULE, Site),
    gen_server:start_link({local, Name}, ?MODULE, [ Site ], []).


%%====================================================================
%% gen_server callbacks
%%====================================================================

init([ Site ]) ->
    State = #state{
        site = Site,
        tickets = #{}
    },
    {ok, State}.


handle_call({new_ticket, Context}, _From, #state{ tickets = Tickets } = State) ->
    Ticket = z_ids:id(32),
    Tickets1 = Tickets#{ Ticket => Context },
    {ok, _} = timer:send_after(?TICKET_TIMEOUT, {ticket_timeout, Ticket}),
    State1 = State#state{ tickets = Tickets1 },
    {reply, {ok, Ticket}, State1};

handle_call({exchange_ticket, Ticket}, _From, #state{ tickets = Tickets } = State) ->
    case maps:find(Ticket, Tickets) of
        {ok, Context} ->
            Tickets1 = maps:remove(Ticket, Tickets),
            {reply, {ok, Context}, State#state{ tickets = Tickets1 }};
        error ->
            {reply, {error, enoent}, State}
    end;

handle_call({delete_ticket, Ticket}, _From, #state{ tickets = Tickets } = State) ->
    case maps:find(Ticket, Tickets) of
        {ok, _Context} ->
            Tickets1 = maps:remove(Ticket, Tickets),
            {reply, ok, State#state{ tickets = Tickets1 }};
        error ->
            {reply, {error, enoent}, State}
    end;

handle_call(Msg, _From, State) ->
    {stop, {unknown_call, Msg}, State}.

handle_cast(Msg, State) ->
    {stop, {unknown_cast, Msg}, State}.

handle_info({ticket_timeout, Ticket}, #state{ tickets = Tickets } = State) ->
    Tickets1 = maps:remove(Ticket, Tickets),
    {noreply, State#state{ tickets = Tickets1 }};
handle_info(_Msg, State) ->
    {noreply, State}.

code_change(_OldVersion, State, _Extra) ->
    {ok, State}.

terminate(_Reason, _State) ->
    ok.