-module(client_manager).
-behaviour(gen_server).
-export([start_link/0]).
-export([init/1, format_status/1, handle_call/3, handle_cast/2]).
start_link() ->
gen_server:start_link({local, client_manager}, client_manager, [], []).
init(_Args) ->
%% Read the wordlist to generate IDs from
WordsPath = os:getenv("WORDLIST", "./words.txt"),
{ok, WordBlob} = file:read_file(WordsPath),
WordList = binary:split(WordBlob, <<"\n">>, [global]),
Words = array:from_list(WordList),
{ok, {Words, #{}, #{}, #{}}}.
format_status(Status) ->
maps:update_with(state, fun({_, R, P, H}) -> {scrubbed_words, R, P, H} end, Status).
-record(room,
{ host %% String
, watchers %% [String]
}).
%% - Rooms :: map(room_id, record room) -- all room info by id
%% - Parties :: map(party_id, thread handle) -- the thread handling this party
%% - Hosts :: map(party_id, room_id) -- the room in which this party is in
handle_call(new_room, From, {Words, Rooms, Parties, Hosts}) ->
NewRoom = make_id(Words),
NewClient = make_id(Words),
{Pid, _} = From,
Reply = {room_created, NewRoom, NewClient},
{ reply
, Reply
, { Words
, maps:put(NewRoom, #room{host = NewClient, watchers = []}, Rooms)
, maps:put(NewClient, Pid, Parties)
, maps:put(NewClient, NewRoom, Hosts)
}
};
handle_call({join_room, RoomId, ClientId}, _From, {Words, Rooms, Parties, Hosts}) ->
OldRoomId = maps:get(ClientId, Hosts),
OldRoom = maps:get(OldRoomId, Rooms),
%% Delete old room if we were the host
Rooms_ = if
OldRoom#room.host =:= ClientId ->
%% TODO notify all members
maps:remove(OldRoomId, Rooms);
true -> Rooms
end,
MbRoom = maps:get(RoomId, Rooms_, not_found),
case MbRoom of
not_found ->
{reply, {join_failure, ClientId}, {Words, Rooms_, Parties, Hosts}};
Room ->
WatchersNew = [ ClientId | Room#room.watchers ],
RoomNew = Room#room{ watchers = WatchersNew },
%% Update the joined room
RoomsNew = maps:put(RoomId, RoomNew, Rooms_),
HostsNew = maps:put(ClientId, RoomId, Hosts),
%% Notify host
HostR = maps:get(Room#room.host, Parties),
HostR ! {party_joins, ClientId},
%% Notify watcher
{reply, {join_success, ClientId}, {Words, RoomsNew, Parties, HostsNew}}
end;
handle_call(Other, _From, State) ->
%% Ignore unknown calls
io:format("Unexpected message: ~p~n", [Other]),
{noreply, State}.
handle_cast({remove_party, PartyId}, {Words, Rooms, Parties, Hosts}) ->
%% FIXME this randomly failed in one of my tests, dunno how
{RoomId, NewHosts} = maps:take(PartyId, Hosts),
NewParties = maps:remove(PartyId, Parties),
%% If the host left before the client, the room doesn't exist anymore
Room = maps:get(RoomId, Rooms, #room{host = nil, watchers = []}),
HostId = Room#room.host,
if
HostId =:= nil ->
%% This room is already closed
{noreply, {Words, Rooms, NewParties, NewHosts}};
PartyId =:= HostId ->
%% We're the host
%% Notify all watchers that the room is closed
WatcherIds = Room#room.watchers,
Watchers = lists:map(fun(Id) -> maps:get(Id, Parties) end, WatcherIds),
lists:foreach(fun(H) -> H ! {room_deleted, HostId} end, Watchers),
%% Delete the room
NewRooms = maps:remove(RoomId, Rooms),
{noreply, {Words, NewRooms, NewParties, NewHosts}};
true ->
%% We're a watcher
%% Remove us from the room
NewWatchers = lists:delete(PartyId, Room#room.watchers),
NewRoom = Room#room{ watchers = NewWatchers },
NewRooms = maps:put(RoomId, NewRoom, Rooms),
%% And notify the host
HostProcess = maps:get(HostId, Parties),
HostProcess ! {watcher_left, PartyId},
{noreply, {Words, NewRooms, NewParties, NewHosts}}
end;
handle_cast({forward_ice, null, SenderId, Body}, State) ->
{_, _, Parties, _} = State,
%% Watcher message which doesn't include TargetId, needs to be
%% forwarded to the room owner
R = maps:get(get_host(SenderId, State), Parties),
R ! {forward_ice, SenderId, Body},
{noreply, State};
handle_cast({forward_ice, TargetId, SenderId, Body}, State) ->
{_, _, Parties, _} = State,
R = maps:get(TargetId, Parties),
R ! {forward_ice, SenderId, Body},
{noreply, State};
handle_cast({forward_sdp, null, SenderId, Body}, State) ->
{_, _, Parties, _} = State,
%% Watcher message which doesn't include TargetId, needs to be
%% forwarded to the room owner
R = maps:get(get_host(SenderId, State), Parties),
R ! {forward_sdp, SenderId, Body},
{noreply, State};
handle_cast({forward_sdp, TargetId, SenderId, Body}, State) ->
{_, _, Parties, _} = State,
R = maps:get(TargetId, Parties),
R ! {forward_sdp, SenderId, Body},
{noreply, State};
handle_cast(Other, State) ->
io:format("Unexpected message: ~p~n", [Other]),
{noreply, State}.
make_id(Words) ->
I1 = rand:uniform(array:size(Words)) - 1,
I2 = rand:uniform(array:size(Words)) - 1,
W1 = array:get(I1, Words),
W2 = array:get(I2, Words),
<< W1/binary, "-", W2/binary >>.
get_host(PartyId, {_, Rooms, _, Hosts}) ->
RoomId = maps:get(PartyId, Hosts),
Room = maps:get(RoomId, Rooms),
Room#room.host.