Current section

Files

Jump to
nostr_lib lib nostr message.ex
Raw

lib/nostr/message.ex

defmodule Nostr.Message do
@moduledoc """
Nostr message is represented as tuple in Elixir. This module provides functions to generate
messages, serialize or parse them.
"""
require Logger
@type t() ::
{:event, Nostr.Event.t()}
| {:event, binary(), Nostr.Event.t()}
| {:req, binary(), [Nostr.Filter.t()]}
| {:close, binary()}
| {:eose, binary()}
| {:notice, String.t()}
| {:ok, binary(), boolean(), String.t()}
| {:auth, Nostr.Event.t() | binary()}
| {:count, String.t(), Nostr.Filter.t() | [Nostr.Filter.t()] | %{count: integer()}}
| {:closed, String.t(), String.t()}
@doc """
Generate post new event message
"""
@doc sender: :client
@spec create_event(Nostr.Event.t() | %{event: Nostr.Event.t()}) :: {:event, Nostr.Event.t()}
def create_event(%{event: %Nostr.Event{} = event}), do: {:event, event}
def create_event(%Nostr.Event{} = event), do: {:event, event}
@doc """
Generate request message
"""
@doc sender: :client
@spec request(Nostr.Filter.t() | [Nostr.Filter.t()], binary()) ::
{:req, binary(), [Nostr.Filter.t()]}
def request(%Nostr.Filter{} = filter, sub_id), do: {:req, sub_id, [filter]}
def request(filters, sub_id) when is_list(filters), do: {:req, sub_id, filters}
@doc """
Generate close message
"""
@doc sender: :client
@spec close(binary()) :: {:close, binary()}
def close(sub_id), do: {:close, sub_id}
@doc """
Generate count message (NIP-45).
Can be used to request counts from relay (with filters) or respond with counts (with integer).
"""
@spec count(pos_integer() | Nostr.Filter.t() | [Nostr.Filter.t()], binary()) ::
{:count, binary(), %{count: pos_integer()} | Nostr.Filter.t() | [Nostr.Filter.t()]}
def count(count, sub_id) when is_integer(count), do: {:count, sub_id, %{count: count}}
def count(%Nostr.Filter{} = filter, sub_id), do: {:count, sub_id, filter}
def count(filters, sub_id) when is_list(filters), do: {:count, sub_id, filters}
@doc """
Generate event message
"""
@doc sender: :relay
@spec event(Nostr.Event.t() | %{event: Nostr.Event.t()}, binary()) ::
{:event, binary(), Nostr.Event.t()}
def event(%{event: %Nostr.Event{} = event}, sub_id), do: {:event, sub_id, event}
def event(%Nostr.Event{} = event, sub_id), do: {:event, sub_id, event}
@doc """
Generate notice message
"""
@doc sender: :relay
@spec notice(String.t()) :: {:notice, String.t()}
def notice(message), do: {:notice, message}
@doc """
Generate eose message
"""
@doc sender: :relay
@spec eose(binary()) :: {:eose, binary()}
def eose(sub_id), do: {:eose, sub_id}
@doc """
Generate OK message
"""
@doc sender: :relay
@spec ok(binary(), boolean(), String.t()) :: {:ok, binary(), boolean(), String.t()}
def ok(event_id, success?, message), do: {:ok, event_id, success?, message}
@doc """
Generate CLOSED message (NIP-01)
"""
@doc sender: :relay
@spec closed(String.t(), String.t()) :: {:closed, String.t(), String.t()}
def closed(sub_id, message), do: {:closed, sub_id, message}
@doc """
Generate AUTH message (NIP-42).
Can be used by relay (with challenge string) or by client (with signed event).
"""
@spec auth(Nostr.Event.t() | %{event: Nostr.Event.t()} | binary()) ::
{:auth, Nostr.Event.t() | binary()}
def auth(%{event: %Nostr.Event{} = event}), do: {:auth, event}
def auth(%Nostr.Event{} = event), do: {:auth, event}
def auth(challenge), do: {:auth, challenge}
@doc """
Serialize Elixir tuple message to on-the-wire binary
"""
@spec serialize(tuple()) :: binary()
def serialize(message) when is_tuple(message) do
message
|> Tuple.to_list()
|> List.flatten()
|> then(fn [name | rest] -> [name |> Atom.to_string() |> String.upcase() | rest] end)
|> JSON.encode!()
end
@doc """
Parse binary message to Elixir tuple, if message contains event it will be returned as general
`Nostr.Event.t()` struct
"""
@spec parse(msg :: String.t()) :: t()
def parse(msg) when is_binary(msg), do: msg |> JSON.decode!() |> do_parse(:general)
@doc """
Parse binary message to Elixir tuple, if message contains event it will be returned as specific
`Nostr.Event.t()` struct dependent of type of Event
"""
@spec parse_specific(String.t()) :: t() | struct()
def parse_specific(msg) when is_binary(msg),
do: msg |> JSON.decode!() |> do_parse(:specific)
# Client to relay
defp do_parse(["EVENT", event], :general) when is_map(event) do
{:event, Nostr.Event.parse(event)}
end
defp do_parse(["EVENT", event], :specific) when is_map(event) do
{:event, Nostr.Event.parse_specific(event)}
end
defp do_parse(["REQ", sub_id | filters], _type)
when is_binary(sub_id) and filters != [] do
parsed = Enum.map(filters, &Nostr.Filter.parse/1)
{:req, sub_id, parsed}
end
defp do_parse(["CLOSE", sub_id], _type) when is_binary(sub_id) do
{:close, sub_id}
end
defp do_parse(["AUTH", event], :general) when is_map(event) do
{:auth, Nostr.Event.parse(event)}
end
defp do_parse(["AUTH", event], :specific) when is_map(event) do
{:auth, Nostr.Event.parse_specific(event)}
end
# Relay to client
defp do_parse(["EVENT", sub_id, event], :general) when is_binary(sub_id) and is_map(event) do
{:event, sub_id, Nostr.Event.parse(event)}
end
defp do_parse(["EVENT", sub_id, event], :specific) when is_binary(sub_id) and is_map(event) do
{:event, sub_id, Nostr.Event.parse_specific(event)}
end
defp do_parse(["NOTICE", message], _type) when is_binary(message) do
{:notice, message}
end
defp do_parse(["EOSE", sub_id], _type) when is_binary(sub_id) do
{:eose, sub_id}
end
defp do_parse(["OK", event_id, success?, message], _type)
when is_binary(event_id) and is_boolean(success?) and is_binary(message) do
{:ok, event_id, success?, message}
end
defp do_parse(["AUTH", sub_id], _type) when is_binary(sub_id) do
{:auth, sub_id}
end
defp do_parse(["CLOSED", sub_id, message], _type) when is_binary(sub_id) do
{:closed, sub_id, message}
end
defp do_parse(["COUNT", sub_id, %{"count" => count}], _type)
when is_binary(sub_id) and is_integer(count) do
{:count, sub_id, %{count: count}}
end
defp do_parse(message, _type) do
Logger.warning("Parsing unknown message: #{inspect(message)}")
:error
end
end