Packages

A Nostr client library for Elixir. Uses `nostr_lib` and `mint` to connect to relays, send events, and subscribe to relays.

Current section

Files

Jump to
nostr_ex lib subscription.ex
Raw

lib/subscription.ex

defmodule NostrEx.Subscription do
@moduledoc """
Represents a Nostr subscription (REQ message).
A subscription consists of:
- `id` - Unique subscription identifier (generated automatically)
- `filters` - List of `Nostr.Filter` structs defining what events to receive
- `created_at` - Unix timestamp when the subscription was created
## Creating Subscriptions
# Single filter
{:ok, sub} = NostrEx.create_sub(authors: ["abc123"], kinds: [1])
# Multiple filters
{:ok, sub} = NostrEx.create_sub([
[authors: ["abc123"], kinds: [1]],
[authors: ["def456"], kinds: [0]]
])
## Sending Subscriptions
{:ok, sub} = NostrEx.create_sub(authors: [pubkey], kinds: [1])
:ok = NostrEx.send_sub(sub)
"""
alias Nostr.Filter
@type t :: %__MODULE__{
id: String.t(),
filters: [Filter.t()],
created_at: integer()
}
@enforce_keys [:id, :filters, :created_at]
defstruct [:id, :filters, :created_at]
@doc """
Create a new subscription with the given filters.
Generates a unique subscription ID automatically.
## Parameters
- `filters` - A keyword list for a single filter, or a list of keyword lists for multiple filters
## Returns
- `{:ok, %Subscription{}}` on success
- `{:error, reason}` if filters are invalid
"""
@spec new(keyword() | [keyword()] | [Filter.t()]) :: {:ok, t()} | {:error, String.t()}
def new(filters) when is_list(filters) do
with {:ok, filter_structs} <- normalize_filters(filters) do
sub = %__MODULE__{
id: generate_id(),
filters: filter_structs,
created_at: DateTime.utc_now() |> DateTime.to_unix()
}
{:ok, sub}
end
end
def new(_), do: {:error, "filters must be a keyword list or list of keyword lists"}
@spec normalize_filters(keyword() | [keyword()] | [Filter.t()]) ::
{:ok, [Filter.t()]} | {:error, String.t()}
defp normalize_filters([]), do: {:ok, []}
defp normalize_filters([%Filter{} | _] = filters), do: {:ok, filters}
defp normalize_filters([{_key, _value} | _] = filter) do
filter_struct = Map.merge(%Filter{}, Enum.into(filter, %{}))
{:ok, [filter_struct]}
end
defp normalize_filters([f | _] = filters) when is_list(f) do
if Enum.all?(filters, &Keyword.keyword?/1) do
filter_structs = Enum.map(filters, &Map.merge(%Filter{}, Enum.into(&1, %{})))
{:ok, filter_structs}
else
{:error, "all filter elements must be keyword lists"}
end
end
defp normalize_filters(_), do: {:error, "invalid filter format"}
@spec generate_id() :: String.t()
defp generate_id do
:crypto.strong_rand_bytes(32) |> Base.encode16(case: :lower)
end
end