Search code examples
elixirphoenix-frameworkphoenix-channels

How do I use Phoenix Presence to track the existence of topics/subtopics?


I'm using subtopics as a one-user channel that others can use to send messages to specific other people.

For example: - I'm user 1, and I want to send a message to user 2. - I send a message with payload { to: 2, message: 'hi' } and handle_in does App.Endpoint.broadcast("user:2", "hi")

Here is a snippet from my user_channel.ex:

def handle_in("chat", incoming_payload, socket) do
  from_uid = socket.assigns.uid
  uid = incoming_payload["to"]
  message = incoming_payload["message"]
  topic = "user:#{uid}"
  payload = %{uid: from_uid, message: message}

  # Send to the topic based of the incoming_payload's 
  # 'to' param.
  App.Endpoint.broadcast(topic, "chat", payload)

  {:reply, :ok, socket}
end

For anyone who's curious: the code is open source

https://github.com/NullVoxPopuli/mesh-relay-phoenix/tree/feature/presence-tracking


Solution

  • Thanks to some wonderful folks in the elixir-lang slack channel, I was able to arrive at this solution (which works):

    defmodule MeshRelay.UserChannel do
      use Phoenix.Channel, :channel
      alias MeshRelay.Presence
      require Logger
    
      defp uids_present(to_uid, from_uid) do
        to_uid && from_uid
      end
    
      defp has_valid_payload(payload) do
        uid = payload["to"]
        message = payload["message"]
    
        uid && message
      end
    
      # uid is the member's channel that
      # he/she receives their messages on.
      # no messages not intended to be received by
      # this member should be sent on this channel / subtopic
      #
      # socket.assigns.uid is the uid from the connect
      def join("user:" <> uid, _params, socket) do
        has_uids = uids_present(uid, socket.assigns.uid)
    
        if has_uids do
          send(self(), :after_join)
          # Logger.debug Presence.list(socket)
          {:ok, socket}
        else
          # kick him out he is not allowed here
          {:error,
            %{reason: "in order to receive messages, you must join a channel using your own uid"},
            socket
          }
        end
      end
    
      def handle_in("chat", incoming_payload, socket) do
        if has_valid_payload(incoming_payload) do
          from_uid = socket.assigns.uid
          uid = incoming_payload["to"]
          message = incoming_payload["message"]
          topic = "user:#{uid}"
          payload = %{uid: from_uid, message: message}
    
          if is_member_online?(uid) do
            MeshRelay.Endpoint.broadcast(topic, "chat", payload)
            # broadcast! socket, "chat", payload
            {:reply, :ok, socket}
          else
            reply_with_error_message(socket, %{
              reason: "member not found",
              to_uid: uid,
              from_uid: from_uid
            })
          end
        else
          reply_with_error_message(socket, %{
            reason: "please format your message: { \"to\": \"uidstring\", \"message\": \"encrypted message\" }"
          })
        end
      end
    
      def reply_with_error_message(socket, error) do
        {:reply, {:error, error}, socket }
      end
    
      def handle_info(:after_join, socket) do
        Presence.track(socket.channel_pid, "connected_members", socket.assigns.uid, %{
          online_at: inspect(System.system_time(:milli_seconds))
        })
    
        {:noreply, socket}
      end
    
      def is_member_online?(uid) do
        Presence.list("connected_members")
        |> Map.keys
        |> Enum.any?(fn key -> key == uid end)
      end
    
    
    end