5 changed files with 185 additions and 0 deletions
@ -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 |
||||
@ -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 |
||||
Loading…
Reference in new issue