socketswebsocketelixirphoenix-frameworkphoenix-channels

Connect to Phoenix Socket with Token and Presence


I'm trying to tie together the Phoenix Channel, Token, and Presence modules to add chat functionality to my Phoenix 1.3 application. I haven't been able to get all 3 modules working together. The last error was connection to websocket closed before handshake. Now, I'm not getting any errors but it's also not connecting to the socket.

I believe the issue is the "connect" function in the player_socket.ex. ( I have a player resource ). Here is the function:

  def connect(%{"token" => token}, socket) do
      case Phoenix.Token.verify(socket, "player auth", token, max_age: @max_age) do
        {:ok, player_id} ->
          player = Repo.get!(Player, player_id)
          {:ok, assign(socket, :current_player, player)}
          {:error, _reason} ->
           :error
      end
  end

I'm signing the token in a meta tag in app.html.eex. <%= tag :meta, name: "channel_token", content: Phoenix.Token.sign(@conn, "player auth", :player_id) %>

Then in the lobby_channel.ex I'm trying to join the channel:

  def join("lobby:lobby", _params, socket) do
    send(self(), :after_join)
    {:ok, assign(socket, :player_id, :current_player)}
  end

  def handle_info(:after_join, socket) do
    push socket, "presence_state", Presence.list(socket)
    {:ok, _} = Presence.track(socket, socket.assigns.current_player, %{
      online_at: inspect(System.system_time(:seconds))
    })
    {:noreply, socket}
  end

I read the docs but can't seem to figure out why I'm unable to connect to the websocket with the "current_player" so that I can use Presence to display who is online and the player's names to associate with their chat messages. Any insight is greatly appreciated! I have the repo here: https://github.com/EssenceOfChaos/gofish

UPDATE

I am using a "current_player" plug to store the player struct in the conn as "current_player.

%Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...},
 assigns: %{current_player: %Gofish.Accounts.Player{__meta__: #Ecto.Schema.Metadata<:loaded, "players">,
    email: "example@aol.com", id: 6,

Here is my updated lobby_channel.ex:

  def join("lobby:lobby", _params, socket) do
    send(self(), :after_join)
    {:ok, socket}
  end

  def handle_info(:after_join, socket) do
    push socket, "presence_state", Presence.list(socket)
    {:ok, _} = Presence.track(socket, socket.assigns.current_player.id, %{
      username: socket.assigns.current_player.username,
      online_at: inspect(System.system_time(:seconds))
    })
    {:noreply, socket}
  end

Solution

  • Your player_socket.ex is fine. You do have a few issues though:

    In your layout/app.eex template:

    Phoenix.Token.sign(@conn, "player auth", :player_id) is literally writing an atom :player_id instead of the ID of the player. In order to write the ID of the player, you should use @player_id and add a plug that assigns the value globally to your router.ex like so:

    pipeline :browser do
      [...]
      plug :fetch_current_user
    end
    
    ...
    
    def fetch_current_user(conn, _) do
      assigns(conn, :current_player, get_session(conn, :current_player)
    end
    

    This will make @current_player available in all your templates, which you can then use in app.eex:

    <%= tag :meta, name: "channel_token", content: Phoenix.Token.sign(@conn, "player auth", @current_player) %>
    

    (you should write this conditionally if @current_player isn't nil and stop your JS client from attempting websocket connections if it is, btw)

    This change will immediately fix your inability to connect to the websocket as long as you've signed in, but you still have one more issue: {:ok, assign(socket, :player_id, :current_player)} in your loby_channel.ex is assigning the atom :current_player literally instead of using the actual value of the current player's ID, but you don't need this line at all. Instead, in your :after_join, you should do

    {:ok, _} = Presence.track(socket, socket.assigns.current_player.username, %{
      online_at: inspect(System.system_time(:seconds))
    })
    

    Notice I changed socket.assigns.current_player to socket.assigns.current_player.username. This is because you cannot assign a struct as a Presence key.

    Alternatively you could do

    {:ok, _} = Presence.track(socket, socket.assigns.current_player.id, %{
      username: socket.assigns.current_player.username,
      online_at: inspect(System.system_time(:seconds))
    })
    

    and in your socket.js you'd use first.username instead of id inside renderOnlineUsers