Skip to content

Instantly share code, notes, and snippets.

@joaodubas
Last active August 15, 2024 15:30
Show Gist options
  • Save joaodubas/6c3b06a25a29b55baadf9a9e3b0e22ed to your computer and use it in GitHub Desktop.
Save joaodubas/6c3b06a25a29b55baadf9a9e3b0e22ed to your computer and use it in GitHub Desktop.
websocket-server-client
erlang 27.0.1
elixir 1.17.2-otp-27

Messaging server client

This is a sample server and client using websockets to exchange messages.

To execute the project locally, it's recommended that asdf or mise be installed. They are used to install the correct versions of erlang and elixir.

Server

Uses bandit to create a websocket connection, based on the example provided in plug repository.

The poison library is used to encode and decode messages.

Client

Uses the fresh library to communicate with the server. Decoding/encoding messages is done with the poison library.

Executing the services

To start the server, use:

elixir server.ex

To start the client, use:

elixir client.ex
Mix.install([
{:fresh, "== 0.4.4"},
{:poison, "== 6.0.0"}
])
defmodule EchoClient do
use Fresh
@impl Fresh
def handle_connect(status, headers, state) do
IO.inspect(status, label: :status)
IO.inspect(headers, label: :headers)
IO.inspect(state, label: :state)
message = build(:authentication_request, state)
{:reply, {:text, message}, state}
end
@impl Fresh
def handle_in({:text, message}, state) do
reply_message = process(message, state)
IO.inspect(message, label: :received)
IO.inspect(reply_message, label: :sent)
{:reply, {:text, reply_message}, state}
end
defp build(category, %{provider: provider, phone_number: phone_number}) do
base =
%{
provider: provider,
identity: phone_number
}
category
|> message()
|> Map.merge(base)
|> Poison.encode!()
end
defp message(:authentication_request) do
%{type: "AUTHENTICATION_REQUEST"}
end
defp message(:chat_list) do
%{type: "CHAT_LIST"}
end
defp message(:message_list) do
%{type: "MESSAGE_LIST"}
end
defp message(:unknown) do
%{type: "UNKNOWN"}
end
defp process(raw_message, state) do
raw_message
|> Poison.decode()
|> case do
{:ok, %{"type" => "CONNECT"}} ->
build(:authentication_request, state)
{:ok, %{"type" => "AUTHENTICATION_CHALLENGE"}} ->
build(:chat_list, state)
{:ok, %{"type" => "CHAT_LIST"}} ->
build(:message_list, state)
{:ok, _} ->
build(:unknown, state)
error ->
error
end
end
end
EchoClient.start_link(
uri: "ws://localhost:4000/whatsapp/55119765337396",
state: %{
provider: "whatsapp",
phone_number: "5511976533796",
last_message_id: nil
},
opts: [name: {:local, EchoClient.Connection}]
)
Process.sleep(60_000)
send(EchoClient.Connection, :stop)
Mix.install([
{:bandit, "== 1.5.7"},
{:poison, "== 6.0.0"},
{:websock, "== 0.5.3"},
{:websock_adapter, "== 0.5.7"}
])
defmodule EchoServer do
@behaviour WebSock
@impl WebSock
def init(state) do
IO.inspect(state, label: :state)
{:reply, :ok, {:text, build(:connect, state)}, state}
end
@impl WebSock
def handle_in({message, [opcode: :text]}, state) do
reply_message = process(message, state)
IO.inspect(message, label: :received)
IO.inspect(reply_message, label: :sent)
{:reply, :ok, {:text, reply_message}, state}
end
@impl WebSock
def terminate(:timeout, state) do
{:ok, state}
end
@impl WebSock
def terminate({:error, :closed}, state) do
{:ok, state}
end
defp build(category, %{provider: provider, identity: identity}) do
base =
%{
provider: provider,
phone_number: identity
}
category
|> message()
|> Map.merge(base)
|> Poison.encode!()
end
defp message(:connect) do
%{
type: "CONNECT",
message: %{
encode: "text",
value: "SUCCESS"
}
}
end
defp message(:authentication_challenge) do
%{
type: "AUTHENTICATION_CHALLENGE",
message: %{
encode: "json",
value: %{
auth: "segredo",
expire_at: NaiveDateTime.utc_now() |> NaiveDateTime.to_iso8601()
}
}
}
end
defp message(:chat_list) do
%{
type: "CHAT_LIST",
message: %{
encode: "json",
value: [
%{
full_name: "João Paulo Dubas",
phone_number: "5511999999999"
},
%{
full_name: "João Paulo Dubas",
phone_number: "5511999999998"
}
]
}
}
end
defp message(:message_list) do
%{
type: "MESSAGE_LIST",
message: %{
encode: "json",
value: %{
full_name: "João Paulo Dubas",
phone_number: "5511999999999",
messages: [
%{
type: "text",
value: "Olá"
}
]
}
}
}
end
defp message(:unknown) do
%{
type: "UNKNOWN",
message: %{
encode: "text",
value: "error"
}
}
end
defp process(raw_message, state) do
Process.sleep(Enum.random(500..5_000))
raw_message
|> Poison.decode()
|> case do
{:ok, %{"type" => "AUTHENTICATION_REQUEST"}} ->
build(:authentication_challenge, state)
{:ok, %{"type" => "CHAT_LIST"}} ->
build(:chat_list, state)
{:ok, %{"type" => "MESSAGE_LIST"}} ->
build(:message_list, state)
{:ok, _} ->
build(:unknown, state)
error ->
error
end
end
end
defmodule Router do
use Plug.Router
plug(Plug.Logger)
plug(:match)
plug(:dispatch)
get "/" do
send_resp(conn, 200, """
Use the JavaScript console to interact using websockets
sock = new WebSocket("ws://localhost:4000/websocket")
sock.addEventListener("message", console.log)
sock.addEventListener("open", () => sock.send("ping"))
""")
end
get "/:provider/:identity" do
state = Enum.into(conn.path_params, %{}, fn {key, value} -> {String.to_atom(key), value} end)
conn
|> WebSockAdapter.upgrade(EchoServer, state, timeout: 60_000)
|> halt()
end
match _ do
send_resp(conn, 404, "not found")
end
end
require Logger
webserver = {Bandit, plug: Router, scheme: :http, port: 4000}
{:ok, _} = Supervisor.start_link([webserver], strategy: :one_for_one)
Logger.info("Plug now running on localhost:4000")
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment