-module(websocket_handler).
-behavior(cowboy_handler).
-export([init/2, websocket_init/1, websocket_handle/2, websocket_info/2, terminate/3]).
init(Req, State) ->
case cowboy_req:parse_header(<<"upgrade">>, Req) of
[<<"websocket">>] ->
{cowboy_websocket, Req, State};
_ ->
% Serve index page when upgrade not demanded
{ok, Body} = file:read_file(code:priv_dir(erlang_server) ++ "/client/host.html"),
Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/html">>}, Body, Req),
{ok, Req2, State}
end.
%% State is:
%% - ClientId - id of this client, local to this process. Set it
%% websocket_info, upon 'room_created' response received after a 'new_room'
%% request is sent in 'websocket_init'
websocket_init(_State) ->
{room_created, RoomId, ClientId} = gen_server:call(client_manager, new_room),
io:format("Created room for new connection: clientId = ~p, roomId = ~p~n", [ClientId, RoomId]),
Resp =
#{ senderId => ClientId
, body =>
#{ t => <<"identity">>
, roomId => RoomId
}
},
%% Remember the client id for this connection
State = {ClientId},
%% Send the identity to the client
io:format("Sending id to client: ~p~n", [Resp]),
{[{text, json:encode(Resp)}], State}.
websocket_handle(Data, State) ->
{ClientId} = State,
case ClientId of
no_id_assigned ->
error("No id assigned to client when it sends a message");
_ -> ok
end,
%% We may receive different kinds of data and need to handle that by hand
Msg = case Data of
{text, Text} -> json:decode(Text);
{binary, Text} -> json:decode(Text); %% I do hope this actually works
ping -> #{};
pong -> #{}
end,
io:format("~nwebsocket_handle| decoded data: ~p~n", [Msg]),
{TargetId, Body} = case Msg of
#{ <<"targetId">> := Tid
, <<"body">> := B
}
-> {Tid, B};
_ -> error("Bad client message")
end,
Type = maps:get(<<"t">>, Body),
case Type of
<<"ice">> ->
gen_server:cast(client_manager, {forward_ice, TargetId, ClientId, Body}),
{[], State};
<<"sdp">> ->
gen_server:cast(client_manager, {forward_sdp, TargetId, ClientId, Body}),
{[], State};
<<"joinRoom">> ->
RoomId = maps:get(<<"roomId">>, Body),
Resp = gen_server:call(client_manager, {join_room, RoomId, ClientId}),
case Resp of
{join_success, SelfId} -> %% I could extract self id from state, but that's less convenient
Notif =
#{ senderId => SelfId
, body =>
#{ t => <<"joinRoom">>
, status => <<"success">>
}
},
io:format("Sending join success to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State};
{join_failure, SelfId} -> %% I could extract self id from state, but that's less convenient
Notif =
#{ senderId => SelfId
, body =>
#{ t => <<"joinRoom">>
, status => <<"404">>
}
},
io:format("Sending join failure to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State}
end
end.
websocket_info(Msg, State) ->
case Msg of
{watcher_left, PartyId} ->
%% Notify the client that the watcher has left
Notif =
#{ senderId => PartyId
, body => #{ t => <<"leaveRoom">> }
},
io:format("Sending watcher left to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State};
{room_deleted, HostId} ->
%% Notify the client that the room it's in has been deleted
Notif =
#{ senderId => HostId
, body => #{ t => <<"leaveRoom">> }
},
io:format("Sending host left to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State};
{forward_ice, SenderId, Body} ->
Notif =
#{ senderId => SenderId
, body => Body
},
io:format("Sending ice to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State};
{forward_sdp, SenderId, Body} ->
Notif =
#{ senderId => SenderId
, body => Body
},
io:format("Sending sdp to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State};
{party_joins, ClientId} ->
Notif =
#{ senderId => ClientId
, body => #{ t => <<"partyJoins">> }
},
io:format("Sending join to client: ~p~n", [Notif]),
{[{text, json:encode(Notif)}], State};
Other ->
io:format("Unexpected message: ~p~n", [Other]),
{[], State}
end.
terminate(_Reason, _Req, {ClientId}) ->
io:format("~nWebsocket terminated for party ~p~n", [ClientId]),
gen_server:cast(client_manager, {remove_party, ClientId}),
ok.