I would like to implement a banned
feature in a chat and I think the easiest way would be to somehow put %{banned_in: ["chat:1", "chat:4", ...]}
in the banned socket’s assigns.
Is there an easy way to do this?
I would like to implement a banned
feature in a chat and I think the easiest way would be to somehow put %{banned_in: ["chat:1", "chat:4", ...]}
in the banned socket’s assigns.
Is there an easy way to do this?
Yep, send a message from that other process to the process that you want to change the asisgn in and let it change it itself.
That is the Actor way to change any data in a process from elsewhere. ^.^
Thanks, but how do I get the pid
of the process that keeps the socket which I want to ban?
Oh, stupid me, I can just send it a message via MyApp.Endpoint.push/3
Oops, there is no MyApp.Endpoint.push/3
…
Register it somewhere is the traditional way, however it sounds like you are using Phoenix channels, in which case it has a system for this for you already!
Look in the Channels
section (that has no hotlink sadly) in Phoenix.Endpoint — Phoenix v1.7.10 for details on the calls, but basically:
Have each user socket that you want to listen (the one where you want to add the banned part) register itself to a unique user topic, something like MyEndpoint.subscribe(user_id)
then you can just MyEndpoint.broadcast(user_id, :add_to_ban_list, ["blah", "blorp"])
or something like that.
So do I understand correctly that every user’s socket (since every user can be banned somewhere) would subscribe itself to user_id
topic in the PubSub so that sometime later it can receive a message that it has been banned?
It might sound like a premature optimization, but how much overhead would that add (if any)?
Yep yep, try to make sure the topic names would be unique of course, so you’d want to prepend something like "UserChannel:"<>user_id
or so just to make sure.
A direct subscribe like that links the current existing process by storing its pid in the global table, so no real overhead for that. Broadcasting just does a lookup in the global table for the PID(s) and then just sends a message like normal, so trivial there either. In other words, this already is the optimized path short of really low-level stuff that you honestly should not ever do.
Did you check out: https://hexdocs.pm/phoenix/Phoenix.Socket.html#c:id/1
Thanks, that’s exactly what I need. But where do I handle this message? In MyApp.UserSocket
or in MyApp.UserChannel
? Can’t find an example on github.
IIRC, you would receive it in your channels and intercept it (see “Intercepting Outgoing Events” in the channel doc) to change the state instead of actually broadcasting to frontends.
Can’t make it work …
channel/user_socket.ex
defmodule Test.Web.UserSocket do
use Phoenix.Socket
## Channels
channel "user:*", Test.Web.UserChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
def connect(_params, socket) do
id = :crypto.strong_rand_bytes(9) |> Base.encode64
{:ok, assign(socket, :id, id)}
end
def id(%{assigns: %{id: id}}), do: IO.inspect("user_socket:#{id}")
end
channel/user_chanel.ex
defmodule Test.Web.UserChannel do
use Test.Web, :channel
alias Phoenix.Socket.Broadcast
intercept ["test"]
def join("user:" <> _user_id, _params, %{assigns: %{id: _id}} = socket) do
{:ok, socket}
end
def handle_in(event, msg, socket) do
IO.inspect([event, msg])
{:reply, :ok, socket}
end
def handle_out(event, msg, socket) do
IO.inspect([event, msg, socket])
{:noreply, socket}
end
def handle_info(%Broadcast{} = broadcast, socket) do
IO.inspect(broadcast)
{:noreply, socket}
end
end
And I broadcast something like Test.Web.Endpoint.broadcast "user_socket:YL9ewkRK/Fg8", "test", %{}
. No IO.inspect
in handle_*
gets called.
[info] GET /
[debug] Processing with Test.Web.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 252µs
"user_socket:YL9ewkRK/Fg8"
[info] JOIN "user:1" to Test.Web.UserChannel
Transport: Phoenix.Transports.WebSocket
Parameters: %{}
[info] Replied user:1 :ok
iex(7)> Test.Web.Endpoint.broadcast "user_socket:YL9ewkRK/Fg8", "test", %{}
:ok
Also, I’m a little bit afraid to use intercept
because of this note in the docs
Note: intercepting events can introduce significantly more overhead if a large number of subscribers must customize a message since the broadcast will be encoded N times instead of a single shared encoding across all subscribers.
That is because you are broadcasting to the id
topic, which is good for pretty much only killing the channel entirely and all topics built on it, this is not what you want.
Instead you need to put something like MyServer.Endpoint.subscribe("UserChannelBlah:#{socket.assigns.user_id}")
in your UserChannel module, say in the join
function as an example, and you need to listen for that message as your handle_info
is already doing fine (though matching on the message event is useful).
Thanks.
I still don’t understand how Test.Web.Endpoint.broadcast <socket id>, event, msg
works though. Is there only a predefined set of events that I can send to it, like "disconnect"
?
Here by socket id
I mean the value returned by https://hexdocs.pm/phoenix/Phoenix.Socket.html#c:id/1
Nope, can send anything and everything you want. The event
just gets put into the %Broadcast{:event}
field, nothing special there at all.
But no handle_*
function ever got the message. Where should have I handled it?
If you are broadcasting to the topic set in your id
method on the UserSocket, then you’d handle it in the UserSocket module itself if it allows you to override handle_info (I’m not sure it does for safety reasons, but I’ve not tried).
Why is it not good for modifying the socket’s assigns?
You can sure, it just won’t do anything unless other broadcast messages use them. Remember that the channels get a ‘copy’ of the assigns, so changing it in the channel will do absolutely nothing to theirs.
If you are broadcasting to the topic set in your id method on the UserSocket, then you’d handle it in the UserSocket module itself if it allows you to override handle_info (I’m not sure it does for safety reasons, but I’ve not tried).
I’ve tried adding
def handle_info(event, msg, socket) do
IO.inspect([event, msg, socket])
{:noreply, socket}
end
def handle_info(event, socket) do
IO.inspect([event, socket])
{:noreply, socket}
end
to UserSocket
module but still no IO.inspect
got called.
And it doesn’t seem to not conform to the genserver behaviour anywhere in the code https://github.com/phoenixframework/phoenix/blob/master/lib/phoenix/socket.ex