lib/nostr_basics/event/parser.ex

defmodule NostrBasics.Event.Parser do
  @moduledoc """
  Turns raw events from JSON websocket messages into elixir structs
  """

  alias NostrBasics.Event

  @doc """
  Converts a NIP-01 JSON string into a %Event{}

  ## Examples
      iex> ~s({"id":"0f017fc299f6351efe9d5bfbfb36c0c7a1399627f9bec02c49b00d0ec98a5f34","pubkey":"5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2","created_at":1675794272,"kind":1,"tags":[],"content":"this is the content"})
      ...> |> NostrBasics.Event.Parser.parse()
      {
        :ok,
        %NostrBasics.Event{
          id: "0f017fc299f6351efe9d5bfbfb36c0c7a1399627f9bec02c49b00d0ec98a5f34",
          pubkey: <<0x5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2::256>>,
          created_at: ~U[2023-02-07 18:24:32Z],
          kind: 1,
          tags: [],
          content: "this is the content"
        }
      }
  """
  @spec parse(String.t()) :: {:ok, Event.t()} | {:error, String.t()}
  def parse(json_event) when is_binary(json_event) do
    case Jason.decode(json_event) do
      {:ok, event} ->
        {:ok, decode(event)}

      {:error, %Jason.DecodeError{position: position, token: token}} ->
        {:error, "error decoding JSON at position #{position}: #{token}"}
    end
  end

  @doc """
  Converts a NIP-01 JSON string decoded as a map by Jason into an %Event{}

  ## Examples
      iex> %{
      ...>    "content" => "this is the content",
      ...>    "created_at" => 1675794272,
      ...>    "id" => "0f017fc299f6351efe9d5bfbfb36c0c7a1399627f9bec02c49b00d0ec98a5f34",
      ...>    "kind" => 1,
      ...>    "pubkey" => "5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2",
      ...>    "tags" => []
      ...>  }
      ...> |> NostrBasics.Event.Parser.decode()
      %NostrBasics.Event{
        id: "0f017fc299f6351efe9d5bfbfb36c0c7a1399627f9bec02c49b00d0ec98a5f34",
        pubkey: <<0x5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2::256>>,
        created_at: ~U[2023-02-07 18:24:32Z],
        kind: 1,
        tags: [],
        content: "this is the content"
      }
  """
  @spec decode(map()) :: Event.t()
  def decode(raw_event) when is_map(raw_event) do
    pubkey = raw_event |> Map.get("pubkey") |> to_binary
    sig = raw_event |> Map.get("sig") |> to_binary

    created_at = raw_event |> Map.get("created_at") |> parse_created_at()
    content = raw_event |> Map.get("content")

    %Event{
      id: Map.get(raw_event, "id"),
      content: content,
      created_at: created_at,
      kind: Map.get(raw_event, "kind"),
      pubkey: pubkey,
      sig: sig,
      tags: Map.get(raw_event, "tags")
    }
  end

  defp parse_created_at(unix_created_at) do
    case DateTime.from_unix(unix_created_at) do
      {:ok, created_at} -> created_at
      {:error, _message} -> nil
    end
  end

  defp to_binary(nil), do: nil
  defp to_binary(value), do: Binary.from_hex(value)
end