lib/nostr_basics/event.ex

defmodule NostrBasics.Event do
  @moduledoc """
  Represents the basic structure of anything that's being sent to/from relays
  """

  require Logger

  defstruct [:id, :pubkey, :created_at, :kind, :tags, :content, :sig]

  alias NostrBasics.Event
  alias NostrBasics.Event.Parser
  alias NostrBasics.Crypto

  @type t :: %Event{}

  # This thing is needed so that the Jason library knows how to serialize the events
  defimpl Jason.Encoder do
    def encode(
          %Event{
            id: id,
            pubkey: pubkey,
            created_at: created_at,
            kind: kind,
            sig: sig,
            tags: tags,
            content: content
          },
          opts
        ) do
      hex_pubkey = Binary.to_hex(pubkey)
      hex_sig = Binary.to_hex(sig)
      timestamp = DateTime.to_unix(created_at)

      Jason.Encode.map(
        %{
          "id" => id,
          "pubkey" => hex_pubkey,
          "created_at" => timestamp,
          "kind" => kind,
          "tags" => tags,
          "content" => content,
          "sig" => hex_sig
        },
        opts
      )
    end
  end

  @doc """
  Simplifies the creation of an event, adding the created_at and tags fields and
  requiring the bare minimum to do so

  ## Examples
      iex> pubkey = <<0xEFC83F01C8FB309DF2C8866B8C7924CC8B6F0580AFDDE1D6E16E2B6107C2862C::256>>
      ...> NostrBasics.Event.create(1, "this is the content", pubkey)
      %NostrBasics.Event{
        pubkey: <<0xefc83f01c8fb309df2c8866b8c7924cc8b6f0580afdde1d6e16e2b6107c2862c::256>>,
        kind: 1,
        content: "this is the content",
        tags: []
      }
  """
  @spec create(integer(), String.t() | nil, <<_::256>>) :: Event.t()
  def create(kind, content, pubkey) do
    %Event{
      kind: kind,
      pubkey: pubkey,
      content: content,
      tags: []
    }
  end

  @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.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(body) do
    Parser.parse(body)
  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.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"
      }
  """
  def decode(raw_event) when is_map(raw_event) do
    Parser.decode(raw_event)
  end

  @doc """
  Adds an ID to an event that doesn't have one

  ## Examples
      iex> %NostrBasics.Event{
      ...>   pubkey: <<0x5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2::256>>,
      ...>   created_at: ~U[2023-02-07 18:24:32Z],
      ...>   kind: 1,
      ...>   tags: [],
      ...>   content: "this is the content"
      ...> }
      ...> |> NostrBasics.Event.add_id
      %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 add_id(Event.t()) :: Event.t()
  def add_id(event) do
    id = create_id(event)

    %{event | id: id}
  end

  @doc """
  Creates an ID for an event that doesn't have one

  ## Examples
      iex> %NostrBasics.Event{
      ...>   pubkey: <<0x5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2::256>>,
      ...>   created_at: ~U[2023-02-07 18:24:32Z],
      ...>   kind: 1,
      ...>   tags: [],
      ...>   content: "this is the content"
      ...> }
      ...> |> NostrBasics.Event.create_id
      "0f017fc299f6351efe9d5bfbfb36c0c7a1399627f9bec02c49b00d0ec98a5f34"
  """
  @spec create_id(Event.t()) :: String.t()
  def create_id(%Event{} = event) do
    event
    |> json_for_id()
    |> Crypto.sha256()
    |> Binary.to_hex()
  end

  @doc """
  A structure an event has to be converted to prior to being SHA256'd, mainly for ID creation

  ## Examples
      iex> %NostrBasics.Event{
      ...>   pubkey: <<0x5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2::256>>,
      ...>   created_at: ~U[2023-02-07 18:24:32Z],
      ...>   kind: 1,
      ...>   tags: [],
      ...>   content: "this is the content"
      ...> }
      ...> |> NostrBasics.Event.json_for_id
      ~s([0,"5ab9f2efb1fda6bc32696f6f3fd715e156346175b93b6382099d23627693c3f2",1675794272,1,[],"this is the content"])
  """
  @spec json_for_id(Event.t()) :: String.t()
  def json_for_id(%Event{
        pubkey: pubkey,
        created_at: created_at,
        kind: kind,
        tags: tags,
        content: content
      }) do
    hex_pubkey = Binary.to_hex(pubkey)
    timestamp = DateTime.to_unix(created_at)

    [
      0,
      hex_pubkey,
      timestamp,
      kind,
      tags,
      content
    ]
    |> Jason.encode!()
  end
end