Browse Source

Add filter endpoints

master
buttercat1791 2 months ago
parent
commit
01d7dcb82f
  1. 21
      lib/gc_index_relay/nostr.ex
  2. 7
      lib/gc_index_relay_web/controllers/fallback_controller.ex
  3. 133
      lib/gc_index_relay_web/controllers/filter_controller.ex
  4. 22
      lib/gc_index_relay_web/controllers/filter_json.ex
  5. 2
      lib/gc_index_relay_web/router.ex

21
lib/gc_index_relay/nostr.ex

@ -12,6 +12,7 @@ defmodule GcIndexRelay.Nostr do @@ -12,6 +12,7 @@ defmodule GcIndexRelay.Nostr do
import Ecto.Query, warn: false
alias GcIndexRelay.Nostr.Validator
alias GcIndexRelay.Nostr.PubEvent
alias GcIndexRelay.Nostr.Filter
alias GcIndexRelay.Repo
alias GcIndexRelay.Nostr.Event
@ -28,6 +29,26 @@ defmodule GcIndexRelay.Nostr do @@ -28,6 +29,26 @@ defmodule GcIndexRelay.Nostr do
|> PubEvent.from_db()
end
@doc """
Queries Nostr events using a NIP-01 filter.
Returns a list of `GcIndexRelay.Nostr.PubEvent` structs matching the filter criteria.
"""
@spec query_events(map()) :: {:ok, [PubEvent.t()]} | {:error, String.t()}
def query_events(filter_map) when is_map(filter_map) do
with {:ok, filter} <- Filter.from_map(filter_map),
events <-
from(e in Event)
|> Filter.apply(filter),
pub_events <-
Enum.map(events, fn event ->
{:ok, pub_event} = PubEvent.from_db(event)
pub_event
end) do
{:ok, pub_events}
end
end
@doc """
Writes a `GcIndexRelay.Nostr.PubEvent` to the database.
"""

7
lib/gc_index_relay_web/controllers/fallback_controller.ex

@ -14,6 +14,13 @@ defmodule GcIndexRelayWeb.FallbackController do @@ -14,6 +14,13 @@ defmodule GcIndexRelayWeb.FallbackController do
|> render(:error, changeset: changeset)
end
# This clause handles string error messages (e.g., from filter validation).
def call(conn, {:error, message}) when is_binary(message) do
conn
|> put_status(:bad_request)
|> json(%{errors: %{detail: message}})
end
# This clause is an example of how to handle resources that cannot be found.
def call(conn, {:error, :not_found}) do
conn

133
lib/gc_index_relay_web/controllers/filter_controller.ex

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
defmodule GcIndexRelayWeb.FilterController do
use GcIndexRelayWeb, :controller
alias GcIndexRelay.Nostr
action_fallback GcIndexRelayWeb.FallbackController
@doc """
GET /api/events - Query events by specifying NIP-01 filter parameters in the URL query string.
# Required Parameters
The `since`, `until`, and `limit` parameters are required. This ensures every query generates a
unique, repeatable response. Queries that do not specify `since`, `until`, or `limit` should be
made against POST /api/events/filter.
"""
def index(conn, params) do
with {:ok, filter_map} <- parse_query_params(params),
{:ok, events} <- Nostr.query_events(filter_map) do
render(conn, :index, events: events)
end
end
@doc """
POST /api/events/filter - Query events using a JSON filter in the request body.
"""
def query(conn, filter_params) do
with {:ok, events} <- Nostr.query_events(filter_params) do
render(conn, :index, events: events)
end
end
# Parse query parameters into a NIP-01 filter map
@spec parse_query_params(map()) :: {:ok, map()} | {:error, String.t()}
defp parse_query_params(params) do
# Require since, until, and limit for client-side caching and predictable pagination
with :ok <- require_param(params, "since"),
:ok <- require_param(params, "until"),
:ok <- require_param(params, "limit"),
{:ok, recognized_params} <- validate_known_params(params) do
parse_params(recognized_params)
end
end
# Ensure a required parameter is present
@spec require_param(map(), String.t()) :: :ok | {:error, String.t()}
defp require_param(params, key) do
if Map.has_key?(params, key) do
:ok
else
{:error, "Missing required parameter: '#{key}'"}
end
end
# Validate that only known NIP-01 filter keys are present
@spec validate_known_params(map()) :: {:ok, map()} | {:error, String.t()}
defp validate_known_params(params) do
known_keys = ["ids", "authors", "kinds", "since", "until", "limit"]
unknown_keys =
params
|> Map.keys()
|> Enum.reject(fn key ->
key in known_keys or String.starts_with?(key, "#")
end)
case unknown_keys do
[] -> {:ok, params}
[key | _] -> {:error, "Unknown query parameter: '#{key}'"}
end
end
# Parse individual parameters from strings to proper types
@spec parse_params(map()) :: {:ok, map()} | {:error, String.t()}
defp parse_params(params) do
result =
params
|> Enum.reduce_while({:ok, %{}}, fn {key, value}, {:ok, acc} ->
case parse_param(key, value) do
{:ok, parsed_value} -> {:cont, {:ok, Map.put(acc, key, parsed_value)}}
{:error, _} = error -> {:halt, error}
end
end)
result
end
# Parse individual parameter based on its key
@spec parse_param(String.t(), String.t()) :: {:ok, any()} | {:error, String.t()}
defp parse_param("ids", value), do: {:ok, String.split(value, ",")}
defp parse_param("authors", value), do: {:ok, String.split(value, ",")}
defp parse_param("kinds", value) do
value
|> String.split(",")
|> Enum.reduce_while({:ok, []}, fn kind_str, {:ok, acc} ->
case Integer.parse(kind_str) do
{kind, ""} -> {:cont, {:ok, [kind | acc]}}
_ -> {:halt, {:error, "Invalid kind value: '#{kind_str}' must be an integer"}}
end
end)
|> case do
{:ok, kinds} -> {:ok, Enum.reverse(kinds)}
error -> error
end
end
defp parse_param("since", value) do
case Integer.parse(value) do
{int, ""} -> {:ok, int}
_ -> {:error, "Invalid since value: '#{value}' must be an integer"}
end
end
defp parse_param("until", value) do
case Integer.parse(value) do
{int, ""} -> {:ok, int}
_ -> {:error, "Invalid until value: '#{value}' must be an integer"}
end
end
defp parse_param("limit", value) do
case Integer.parse(value) do
{int, ""} -> {:ok, int}
_ -> {:error, "Invalid limit value: '#{value}' must be an integer"}
end
end
# Handle tag parameters (keys starting with "#")
defp parse_param("#" <> _tag_name, value) do
{:ok, String.split(value, ",")}
end
end

22
lib/gc_index_relay_web/controllers/filter_json.ex

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
defmodule GcIndexRelayWeb.FilterJSON do
alias GcIndexRelay.Nostr.PubEvent
@doc """
Renders a list of events.
"""
def index(%{events: events}) do
%{data: Enum.map(events, &data/1)}
end
defp data(%PubEvent{} = event) do
%{
id: event.id,
pubkey: event.pubkey,
created_at: event.created_at,
kind: event.kind,
content: event.content,
sig: event.sig,
tags: event.tags
}
end
end

2
lib/gc_index_relay_web/router.ex

@ -23,6 +23,8 @@ defmodule GcIndexRelayWeb.Router do @@ -23,6 +23,8 @@ defmodule GcIndexRelayWeb.Router do
scope "/api", GcIndexRelayWeb do
pipe_through :api
get "/events", FilterController, :index
post "/events/filter", FilterController, :query
resources "/events", EventController, only: [:show, :create, :delete]
end

Loading…
Cancel
Save