Current section
Files
Jump to
Current section
Files
lib/nostr/filter.ex
defmodule Nostr.Filter do
@moduledoc """
Nostr filter
"""
defstruct [
:ids,
:authors,
:kinds,
:"#e",
:"#p",
:"#a",
:"#d",
:since,
:until,
:limit,
:search,
:tags
]
@type t() :: %__MODULE__{
ids: nil | [<<_::32, _::_*8>>],
authors: nil | [<<_::32, _::_*8>>],
kinds: nil | [non_neg_integer()],
"#e": nil | [<<_::32, _::_*8>>],
"#p": nil | [<<_::32, _::_*8>>],
# award definition link
"#a": nil | [<<_::32, _::_*8>>],
# badge name
"#d": nil | [binary()],
since: nil | DateTime.t(),
until: nil | DateTime.t(),
limit: nil | non_neg_integer(),
search: nil | String.t(),
# Arbitrary single-letter tag filters (NIP-01)
tags: nil | %{String.t() => [binary()]}
}
# Known keys that map to struct fields
@known_keys %{
"ids" => :ids,
"authors" => :authors,
"kinds" => :kinds,
"#e" => :"#e",
"#p" => :"#p",
"#a" => :"#a",
"#d" => :"#d",
"since" => :since,
"until" => :until,
"limit" => :limit,
"search" => :search
}
# Single-letter tag pattern (NIP-01: #<single-letter (a-zA-Z)>)
@tag_pattern ~r/^#[a-zA-Z]$/
@doc """
Parse filter from raw list to `Nostr.Filter` struct
"""
@spec parse(map) :: __MODULE__.t()
def parse(filter) when is_map(filter) do
{known, extra_tags} = Enum.reduce(filter, {%{}, %{}}, &classify_key/2)
known
|> maybe_add_tags(extra_tags)
|> maybe_parse_timestamp(:since)
|> maybe_parse_timestamp(:until)
|> then(&struct(__MODULE__, &1))
end
defp classify_key({key, value}, {known_acc, tags_acc}) do
str_key = if is_atom(key), do: Atom.to_string(key), else: key
case Map.fetch(@known_keys, str_key) do
{:ok, atom_key} ->
{Map.put(known_acc, atom_key, value), tags_acc}
:error ->
if Regex.match?(@tag_pattern, str_key) do
{known_acc, Map.put(tags_acc, str_key, value)}
else
{known_acc, tags_acc}
end
end
end
defp maybe_add_tags(known, extra_tags) when map_size(extra_tags) > 0 do
Map.put(known, :tags, extra_tags)
end
defp maybe_add_tags(known, _extra_tags), do: known
defp maybe_parse_timestamp(known, field) do
case Map.fetch(known, field) do
{:ok, value} when is_integer(value) ->
Map.put(known, field, DateTime.from_unix!(value))
_not_found ->
known
end
end
end
defimpl JSON.Encoder, for: Nostr.Filter do
def encode(%Nostr.Filter{} = filter, encoder) do
# Extract extra tags before converting
extra_tags = filter.tags || %{}
filter
|> Map.update!(:since, &encode_unix/1)
|> Map.update!(:until, &encode_unix/1)
|> Map.from_struct()
|> Map.delete(:tags)
|> Enum.reject(fn {_key, val} -> is_nil(val) end)
|> Enum.into(%{})
|> Map.merge(extra_tags)
|> :elixir_json.encode_map(encoder)
end
defp encode_unix(nil), do: nil
defp encode_unix(date_time), do: DateTime.to_unix(date_time)
end