How to create a channel that will allow users to exchange information privately?

I’m trying to create a channel that will allow users to exchange information privately. According to some of the other threads on the topic this can be done by creating individual channel topics for users and sending messages between them using socket.endpoint.broadcast!/3 . I’m trying to do this but I can’t get it to work.

This is the channel.ex in question.

defmodule ChatWeb.PrivateChannel do
use ChatWeb, :channel
require Logger

alias ChatWeb.Presence
alias Chat.Repo
alias Chat.Auth.User

def join("private:" <> user_id, _message, socket) do
    {:ok, %{channel: "private:#{user_id}"}, assign(socket, :user_id, user_id)}
end

#This one is mean to receive the message from the sender 
def handle_in("offer", %{"message" => message, "sender" => sender, "recipient" => recipient}, socket) do
  Logger.info "THIS" # I just use these to check if the function has fired or not
  broadcast!(socket, "private:#{recipient}:message", %{message: message, sender: sender})
  {:noreply, socket}
end

 #This is meant take the message from the client side and sends it to the recipient
def handle_in("message", %{"message" => message, "recipient" => recipient, "sender" => sender}, socket) do
  Logger.info "THAT"
  socket.endpoint.broadcast!("private:#{recipient}", "offer", %{message: message, sender: sender, recipient: recipient})
  {:noreply, socket}
end

def id(_socket), do: nil

end

I’m not getting any errors. It’s just not doing anything and I’m not sure why.

It’s been a long time since I wrote this code… it was Phoenix 1.3.

I do not understand some part of your code, for example

def id(_socket), do: nil

end

This code does not belong in the channel, but in the socket.

broadcast!(socket, "private:#{recipient}:message", %{message: message, sender: sender})

Why do You use this topic? Why do You add :message at the end? I would use a topic with “private:#{…}”

def join("private:" <> user_id, _message, socket) do
    {:ok, %{channel: "private:#{user_id}"}, assign(socket, :user_id, user_id)}
end

There is no check here, anyone connecting to this channel would be allowed. I authenticate in the socket, then when I am in the channel, I am sure of who’s in.

Some sample code (for Phoenix 1.3)

This is my socket, verify token is one of my helper, to check against … token

  def connect(%{"token" => token}, socket) do
    with {:ok, user_id} <- verify_token(token) do
      {:ok, assign(socket, :user_id, user_id)}
    else
      {:error, _reason} -> :error
    end
  end

Once I get connected, this is my join, in my user channel

  def join("user:" <> id, _params, socket) do
    if String.to_integer(id) === socket.assigns.user_id do
      {:ok, socket}
    else
      {:error, %{reason: "Not authorized"}}
    end
  end

And finally, this is how send to private channels

  defp broadcast_to_user(socket, id, message, payload) do
    socket.endpoint.broadcast!("user:#{id}", message, payload)
  end

I am not sure why it is not working for You, but I would say there are mistakes in your code.

1 Like
  1. Some of the code like the id is probably left over from an example I copied earlier, I forgot to remove it.

  2. I thought that this is how I send data from a channel call to my client side. I can see (now) how my code is kind of nonsensical x_x
    That is actually what I need to figure out how to do but I can’t seem to figure it out and I haven’t found any good examples of how to do this.

  3. At the present I’m not really concerned with authentication, the app I’m working on is just a proof of concept.

Thank you for the example but there are a couple of thing that I still don’t understand.

  1. What is payload?
  2. Do I need to add/change anything else to make this work?

The main error is to put message in the topic, payload is the map I send

1 Like

So this would be the correct version?

  def handle_in("message", %{"message" => message, "recipient" => recipient, "sender" => sender}, socket) do
      Logger.info "THAT"
      socket.endpoint.broadcast!("private:#{recipient}", message, %{sender: sender})
      {:noreply, socket}
  end

so let’s say this successfully sends it “private:#{recipient}”, then what. How do you receive the message on the other end?

My front end looks like this:

  let private_channel = socket.channel(`private:${userid}`)

  private_channel.join()
    .receive("ok", resp => { console.log("Joined successfully", resp) })
    .receive("error", resp => { console.log("Unable to join", resp) })

  private_channel.on(`message`, (message, recipient, sender) => {
    console.log("message", message)
    console.log("message", recipient)
    console.log("sender", sender)
  });

I figured out what was wrong. I needed to create a separate event for the private message.

   def handle_in("message", %{"message" => message, "recipient" => recipient, "sender" => sender}, socket) do
      Logger.info "THAT"
      socket.endpoint.broadcast!("private:#{recipient}" , "offer" , %{message: message, sender: sender, recipient: recipient})
      {:noreply, socket}
    end

On the front end, I didn’t need to add "private:#{userid}:offer" in order to receive the event. private_channel was already listening to "private:#{userid}:" I just needed to enter the specific even I wanted which was "offer". Which looks like this now.

  private_channel.join()
    .receive("ok", resp => { console.log("Joined successfully", resp) })
    .receive("error", resp => { console.log("Unable to join", resp) })

  private_channel.on(`offer`, (message) => {
    console.log('it works');
    console.log(message)
  });
2 Likes