% @doc % controller for chat -module(fd_chat). -vsn("0.1.0"). -behavior(gen_server). -author("Peter Harpending "). -copyright("Peter Harpending "). -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.