263 lines
7.2 KiB
Erlang
263 lines
7.2 KiB
Erlang
% @doc
|
|
% controller for chat
|
|
-module(fd_chat).
|
|
-vsn("0.1.0").
|
|
-behavior(gen_server).
|
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
|
-license("BSD-2-Clause-FreeBSD").
|
|
|
|
-export([
|
|
join/1,
|
|
relay/1,
|
|
nick_available/1
|
|
]).
|
|
-export([start_link/0]).
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
code_change/3, terminate/2]).
|
|
|
|
-include("$zx_include/zx_logger.hrl").
|
|
|
|
|
|
-record(o, {pid :: pid(),
|
|
nick :: string()}).
|
|
-type orator() :: #o{}.
|
|
|
|
-record(s, {orators = [] :: [orator()]}).
|
|
|
|
-type state() :: #s{}.
|
|
|
|
|
|
|
|
%%% Service Interface
|
|
|
|
-spec join(Nick) -> Result
|
|
when Nick :: string(),
|
|
Result :: ok
|
|
| {error, Reason :: any()}.
|
|
|
|
join(Nick) ->
|
|
gen_server:call(?MODULE, {join, Nick}).
|
|
|
|
|
|
|
|
-spec nick_available(Nick) -> Result
|
|
when Nick :: string(),
|
|
Result :: boolean().
|
|
|
|
nick_available(Nick) ->
|
|
gen_server:call(?MODULE, {nick_available, Nick}).
|
|
|
|
|
|
|
|
-spec relay(Message) -> ok
|
|
when Message :: string().
|
|
|
|
relay(Message) ->
|
|
gen_server:cast(?MODULE, {relay, self(), Message}).
|
|
|
|
|
|
%%% Startup Functions
|
|
|
|
|
|
-spec start_link() -> Result
|
|
when Result :: {ok, pid()}
|
|
| {error, Reason :: term()}.
|
|
%% @private
|
|
%% This should only ever be called by fd_chat_orators (the service-level supervisor).
|
|
|
|
start_link() ->
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
|
|
|
|
|
|
-spec init(none) -> {ok, state()}.
|
|
%% @private
|
|
%% Called by the supervisor process to give the process a chance to perform any
|
|
%% preparatory work necessary for proper function.
|
|
|
|
init(none) ->
|
|
ok = tell("~p Starting.", [?MODULE]),
|
|
State = #s{},
|
|
{ok, State}.
|
|
|
|
|
|
|
|
%%% gen_server Message Handling Callbacks
|
|
|
|
|
|
-spec handle_call(Message, From, State) -> Result
|
|
when Message :: term(),
|
|
From :: {pid(), reference()},
|
|
State :: state(),
|
|
Result :: {reply, Response, NewState}
|
|
| {noreply, State},
|
|
Response :: term(),
|
|
NewState :: state().
|
|
%% @private
|
|
%% The gen_server:handle_call/3 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3
|
|
|
|
handle_call({join, Nick}, {Pid, _}, State) ->
|
|
{Reply, NewState} = do_join(Pid, Nick, State),
|
|
{reply, Reply, NewState};
|
|
handle_call({nick_available, Nick}, _, State = #s{orators = Orators}) ->
|
|
Reply = is_nick_available(Nick, Orators),
|
|
{reply, Reply, State};
|
|
handle_call(Unexpected, From, State) ->
|
|
ok = tell("~p Unexpected call from ~tp: ~tp~n", [?MODULE, From, Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
-spec handle_cast(Message, State) -> {noreply, NewState}
|
|
when Message :: term(),
|
|
State :: state(),
|
|
NewState :: state().
|
|
%% @private
|
|
%% The gen_server:handle_cast/2 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2
|
|
|
|
handle_cast({relay, From, Message}, State = #s{orators = Orators}) ->
|
|
do_relay(From, Message, Orators),
|
|
{noreply, State};
|
|
handle_cast(Unexpected, State) ->
|
|
ok = tell("~p Unexpected cast: ~tp~n", [?MODULE, Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
-spec handle_info(Message, State) -> {noreply, NewState}
|
|
when Message :: term(),
|
|
State :: state(),
|
|
NewState :: state().
|
|
%% @private
|
|
%% The gen_server:handle_info/2 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2
|
|
|
|
handle_info(Msg = {'DOWN', _Mon, process, _Pid, _Reason}, State) ->
|
|
NewState = handle_down(Msg, State),
|
|
{noreply, NewState};
|
|
handle_info(Unexpected, State) ->
|
|
ok = tell("~p Unexpected info: ~tp~n", [?MODULE, Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
|
|
%%% OTP Service Functions
|
|
|
|
-spec code_change(OldVersion, State, Extra) -> Result
|
|
when OldVersion :: {down, Version} | Version,
|
|
Version :: term(),
|
|
State :: state(),
|
|
Extra :: term(),
|
|
Result :: {ok, NewState}
|
|
| {error, Reason :: term()},
|
|
NewState :: state().
|
|
%% @private
|
|
%% The gen_server:code_change/3 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:code_change-3
|
|
|
|
code_change(_, State, _) ->
|
|
{ok, State}.
|
|
|
|
|
|
-spec terminate(Reason, State) -> no_return()
|
|
when Reason :: normal
|
|
| shutdown
|
|
| {shutdown, term()}
|
|
| term(),
|
|
State :: state().
|
|
%% @private
|
|
%% The gen_server:terminate/2 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:terminate-2
|
|
|
|
terminate(_, _) ->
|
|
ok.
|
|
|
|
|
|
%%% internals
|
|
|
|
-spec do_join(Pid, Nick, State) -> {Reply, NewState}
|
|
when Pid :: pid(),
|
|
Nick :: string(),
|
|
Reply :: ok | {error, Reason :: any()},
|
|
NewState :: State.
|
|
|
|
do_join(Pid, Nick, State = #s{orators = Orators}) ->
|
|
case ensure_can_join(Pid, Nick, Orators) of
|
|
ok -> do_join2(Pid, Nick, State);
|
|
Error -> {Error, State}
|
|
end.
|
|
|
|
|
|
do_join2(Pid, Nick, State = #s{orators = Orators}) ->
|
|
_Monitor = erlang:monitor(process, Pid),
|
|
NewOrator = #o{pid = Pid, nick = Nick},
|
|
NewOrators = [NewOrator | Orators],
|
|
NewState = State#s{orators = NewOrators},
|
|
{ok, NewState}.
|
|
|
|
|
|
-spec ensure_can_join(Pid, Nick, Orators) -> Result
|
|
when Pid :: pid(),
|
|
Nick :: string(),
|
|
Orators :: [orator()],
|
|
Result :: ok
|
|
| {error, Reason},
|
|
Reason :: any().
|
|
% @private
|
|
% ensures both Pid and Nick are unique
|
|
|
|
ensure_can_join(Pid, _ , [#o{pid = Pid} | _ ]) -> {error, already_joined};
|
|
ensure_can_join(_ , Nick, [#o{nick = Nick} | _ ]) -> {error, {nick_taken, Nick}};
|
|
ensure_can_join(Pid, Nick, [_ | Rest]) -> ensure_can_join(Pid, Nick, Rest);
|
|
ensure_can_join(_ , _ , [] ) -> ok.
|
|
|
|
|
|
-spec is_nick_available(Nick, Orators) -> boolean()
|
|
when Nick :: string(),
|
|
Orators :: [orator()].
|
|
|
|
is_nick_available(Nick, [#o{nick = Nick} | _ ]) -> false;
|
|
is_nick_available(Nick, [_ | Rest]) -> is_nick_available(Nick, Rest);
|
|
is_nick_available(_ , [] ) -> true.
|
|
|
|
|
|
|
|
-spec handle_down(Msg, State) -> NewState
|
|
when Msg :: {'DOWN', Mon, process, Pid, Reason},
|
|
Mon :: erlang:monitor(),
|
|
Pid :: pid(),
|
|
Reason :: any(),
|
|
State :: state(),
|
|
NewState :: State.
|
|
|
|
handle_down(Msg = {'DOWN', _, process, Pid, _}, State = #s{orators = Orators}) ->
|
|
NewOrators = hdn(Msg, Pid, Orators, []),
|
|
NewState = State#s{orators = NewOrators},
|
|
NewState.
|
|
|
|
% encountered item, removing
|
|
hdn(_, Pid, [#o{pid = Pid} | Rest], Acc) -> Rest ++ Acc;
|
|
hdn(Msg, Pid, [Skip | Rest], Acc) -> hdn(Msg, Pid, Rest, [Skip | Acc]);
|
|
hdn(Msg, _, [] , Acc) ->
|
|
log("~tp: Unexpected message: ~tp", [?MODULE, Msg]),
|
|
Acc.
|
|
|
|
|
|
do_relay(Pid, Message, Orators) ->
|
|
case lists:keyfind(Pid, #o.pid, Orators) of
|
|
#o{nick = Nick} ->
|
|
do_relay2(Nick, Message, Orators);
|
|
false ->
|
|
tell("~tp: Message received from outsider ~tp: ~tp", [?MODULE, Pid, Message]),
|
|
error
|
|
end.
|
|
|
|
% skip
|
|
do_relay2(Nick, Msg, [#o{nick = Nick} | Rest]) ->
|
|
do_relay2(Nick, Msg, Rest);
|
|
do_relay2(Nick, Msg, [#o{pid = Pid} | Rest]) ->
|
|
Pid ! {chat, {relay, Nick, Msg}},
|
|
do_relay2(Nick, Msg, Rest);
|
|
do_relay2(_, _, []) ->
|
|
ok.
|