-module(client).
-export([main/1, initial_state/2]).
-include_lib("./defs.hrl").
-spec main(_InitialState) -> _.
-spec listen(_State) -> _.
-spec initial_state(_Nick, _GuiName) -> _InitialClientState.
-spec loop(_State, _Request, _Ref) -> _.
-spec do_join(_State, _Ref, _ChatName) -> _.
-spec do_leave(_State, _Ref, _ChatName) -> _.
-spec do_new_nick(_State, _Ref, _NewNick) -> _.
-spec do_new_incoming_msg(_State, _Ref, _SenderNick, _ChatName, _Message) -> _.
%% Receive messages from GUI and handle them accordingly
%% All handling can be done in loop(...)
main(InitialState) ->
%% The client tells the server it is connecting with its initial nickname.
%% This nickname is guaranteed unique system-wide as long as you do not assign a client
%% the nickname in the form "user[number]" manually such that a new client happens
%% to generate the same random number as you assigned to your client.
whereis(server)!{self(), connect, InitialState#cl_st.nick},
%% if running test suite, tell test suite that client is up
case whereis(testsuite) of
undefined -> ok;
TestSuitePID -> TestSuitePID!{client_up, self()}
end,
%% Begins listening
listen(InitialState).
%% This method handles all incoming messages from either the GUI or the
%% chatrooms that are not directly tied to an ongoing request cycle.
listen(State) ->
receive
{request, From, Ref, Request} ->
%% the loop method will return a response as well as an updated
%% state to pass along to the next cycle
{Response, NextState} = loop(State, Request, Ref),
case Response of
{dummy_target, Resp} ->
io:format("Use this for whatever you would like~n"),
From!{result, self(), Ref, {dummy_target, Resp}},
listen(NextState);
%% if shutdown is received, terminate
shutdown ->
ok_shutdown;
%% do nothing if GUI receives a quit instruction
ack_quit ->
From!{self(), Ref, ack_quit},
listen(NextState);
%% if ok_msg_received, then we don't need to reply to sender.
ok_msg_received ->
listen(NextState);
%% otherwise, reply to sender with response
_ ->
From!{result, self(), Ref, Response},
listen(NextState)
end
end.
%% This function just initializes the default state of a client.
%% This should only be used by the GUI. Do not change it, as the
%% GUI code we provide depends on it.
initial_state(Nick, GUIName) ->
#cl_st { gui = GUIName, nick = Nick, con_ch = maps:new() }.
%% ------------------------------------------
%% loop handles each kind of request from GUI
%% ------------------------------------------
loop(State, Request, Ref) ->
case Request of
%% GUI requests to join a chatroom with name ChatName
{join, ChatName} ->
do_join(State, Ref, ChatName);
%% GUI requests to leave a chatroom with name ChatName
{leave, ChatName} ->
do_leave(State, Ref, ChatName);
%% GUI requests to send an outgoing message Message to chatroom ChatName
{outgoing_msg, ChatName, Message} ->
do_msg_send(State, Ref, ChatName, Message);
%% GUI requests the nickname of client
whoami ->
% - should this be done here? If not, where else? -
GuiPid = whereis(list_to_atom(State#cl_st.gui)),
CurrentNick = State#cl_st.nick,
GuiPid!{result, self(), Ref, CurrentNick},
% - end sus location-
{{dummy_target, dummy_response}, State};
%% GUI requests to update nickname to Nick
{nick, Nick} ->
do_new_nick(State, Ref, Nick);
%% GUI requesting to quit completely
quit ->
do_quit(State, Ref);
%% Chatroom with name ChatName has sent an incoming message Message
%% from sender with nickname SenderNick
{incoming_msg, SenderNick, ChatName, Message} ->
do_new_incoming_msg(State, Ref, SenderNick, ChatName, Message);
{get_state} ->
{{get_state, State}, State};
%% Somehow reached a state where we have an unhandled request.
%% Without bugs, this should never be reached.
_ ->
io:format("Client: Unhandled Request: ~w~n", [Request]),
{unhandled_request, State}
end.
%% executes `/join` protocol from client perspective
do_join(State, Ref, ChatName) ->
GuiPid = whereis(list_to_atom(State#cl_st.gui)),
ConnectedChats = State#cl_st.con_ch,
case maps:get(ChatName, ConnectedChats, error) of
error ->
% User wants to attempt to join a chatroom
Server = whereis(server),
Server!{self(), Ref, join, ChatName},
receive
{ChatPid, IncomingRef, connect, History} ->
GuiPid!{result, self(), IncomingRef, History},
{{dummy_target, dummy_response}, % Update connected chats
State#cl_st{con_ch = maps:put(ChatName, ChatPid, ConnectedChats)}}
end;
_ ->
% Error, already connected to the specified chatroom
GuiPid!{result, self(), Ref, err},
{{dummy_target, dummy_response}, State}
end.
%% executes `/leave` protocol from client perspective
do_leave(State, Ref, ChatName) ->
GuiPid = whereis(list_to_atom(State#cl_st.gui)),
ConnectedChats = State#cl_st.con_ch,
case maps:get(ChatName, ConnectedChats, error) of
error -> % User is not in chatroom, error.
GuiPid!{result, self(), Ref, err},
{{dummy_target, dummy_response}, State};
% Note, this will also proc if they typed it in the System tab, not sure if anything can be done about this
_ ->
Server = whereis(server),
Server!{self(), Ref, leave, ChatName},
receive
{_ServerPid, IncomingRef, ack_leave} ->
GuiPid!{result, self(), IncomingRef, ok}, % Left successfully
% Update state to remove the chatroom from connected chats.
{{dummy_target, dummy_response},
State#cl_st{con_ch = maps:remove(ChatName, ConnectedChats)}}
end
end.
%% executes `/nick` protocol from client perspective
do_new_nick(State, Ref, NewNick) ->
GuiPid = whereis(list_to_atom(State#cl_st.gui)),
CurrentNick = State#cl_st.nick,
case CurrentNick of
NewNick -> % If the user's desired nickname is the same as the current one, notify user/give them an error message
GuiPid!{result, self(), Ref, err_same},
{{dummy_target, dummy_response}, State};
_ ->
ServerPid = whereis(server),
ServerPid!{self(), Ref, nick, NewNick},
receive
{ServerPid, Ref, err_nick_used} -> % Nick already taken
GuiPid!{result, self(), Ref, err_nick_used},
{{dummy_target, dummy_response}, State};
{ServerPid, Ref, ok_nick} -> % Nick successfully changed
GuiPid!{result, self(), Ref, ok_nick},
{{dummy_target, dummy_response},
State#cl_st{nick = NewNick}}
end
end.
%% executes send message protocol from client perspective.
do_msg_send(State, Ref, ChatName, Message) ->
GuiPid = whereis(list_to_atom(State#cl_st.gui)),
ConnectedChats = State#cl_st.con_ch,
% Find the chatroom PID from the connected chats map
case maps:get(ChatName, ConnectedChats, undefined) of
undefined ->
% This should literally not be possible (would require the user sending a message to a chat room that doesn't exist / isn't registered,
% but maybe if the server dies and the clients/GUIs try typing in the chatroom it could proc
GuiPid!{result, self(), Ref, {error, "Connection to the chatroom " ++ ChatName ++ " has been severed. The message was not sent."}},
{{dummy_target, dummy_response}, State};
ChatPid ->
% Send the message to the chatroom
ChatPid!{self(), Ref, message, Message},
receive
{ChatPid, IncomingRef, ack_msg} ->
% Push the message to the GUI so the client actually sees the message
GuiPid!{result, self(), IncomingRef, {msg_sent, State#cl_st.nick}}
end,
{{dummy_target, dummy_response}, State}
end.
%% executes new incoming message protocol from client perspective
do_new_incoming_msg(State, _Ref, CliNick, ChatName, Msg) ->
%% pass message along to gui
gen_server:call(list_to_atom(State#cl_st.gui), {msg_to_GUI, ChatName, CliNick, Msg}),
{ok_msg_received, State}.
%% executes quit protocol from client perspective
do_quit(ClientState, RequestRef) ->
Server = whereis(server),
GuiPid = whereis(list_to_atom(ClientState#cl_st.gui)),
% Send quit request to the server as user wants to quit
Server!{self(), RequestRef, quit},
% Wait until server acknowledges the request
receive
{Server, RequestRef, ack_quit} ->
% Tell the user that they have quit successfully (pdf mentions that this may not work due to GUI issues*)
GuiPid!{self(), RequestRef, ack_quit}
end,
{shutdown, ClientState}.