|
|
|
|
@ -1,11 +1,42 @@
@@ -1,11 +1,42 @@
|
|
|
|
|
defmodule GcIndexRelay.Nostr.Filter do |
|
|
|
|
alias GcIndexRelay.Nostr.Event |
|
|
|
|
alias GcIndexRelay.Repo |
|
|
|
|
alias GcIndexRelay.Nostr.Tag |
|
|
|
|
import Ecto.Query |
|
|
|
|
|
|
|
|
|
@derive [Jason.Encoder] |
|
|
|
|
defstruct [:ids, :authors, :kinds, :tags, :since, :until, :limit] |
|
|
|
|
|
|
|
|
|
# TODO: Implement tag filtering |
|
|
|
|
@type t :: %__MODULE__{ |
|
|
|
|
ids: [String.t()] | nil, |
|
|
|
|
authors: [String.t()] | nil, |
|
|
|
|
kinds: [integer()] | nil, |
|
|
|
|
tags: %{String.t() => [String.t()]} | nil, |
|
|
|
|
since: integer() | nil, |
|
|
|
|
until: integer() | nil, |
|
|
|
|
limit: pos_integer() | nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@spec from_map(map()) :: t() |
|
|
|
|
def from_map(map) when is_map(map) do |
|
|
|
|
tags = |
|
|
|
|
for {"#" <> k, v} <- map, |
|
|
|
|
do: {k, v}, |
|
|
|
|
into: %{} |
|
|
|
|
|
|
|
|
|
%__MODULE__{ |
|
|
|
|
ids: map["ids"] || nil, |
|
|
|
|
authors: map["authors"] || nil, |
|
|
|
|
kinds: map["kinds"] || nil, |
|
|
|
|
tags: |
|
|
|
|
case tags do |
|
|
|
|
tags when map_size(tags) == 0 -> nil |
|
|
|
|
tags -> tags |
|
|
|
|
end, |
|
|
|
|
since: map["since"] || nil, |
|
|
|
|
until: map["until"] || nil, |
|
|
|
|
limit: map["limit"] || nil |
|
|
|
|
} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@doc """ |
|
|
|
|
Applies a filter to an Ecto query of Nostr events. |
|
|
|
|
@ -19,7 +50,8 @@ defmodule GcIndexRelay.Nostr.Filter do
@@ -19,7 +50,8 @@ defmodule GcIndexRelay.Nostr.Filter do
|
|
|
|
|
|
|
|
|
|
A filtered list of events in descending order of creation time. |
|
|
|
|
""" |
|
|
|
|
def apply(%Ecto.Query{} = query, %__MODULE__{} = filter) do |
|
|
|
|
@spec apply(Ecto.Query.t(), t()) :: [struct()] |
|
|
|
|
def apply(%Ecto.Query{from: %{source: {_table, Event}}} = query, %__MODULE__{} = filter) do |
|
|
|
|
query |
|
|
|
|
|> apply_ids(filter.ids) |
|
|
|
|
|> apply_authors(filter.authors) |
|
|
|
|
@ -27,30 +59,81 @@ defmodule GcIndexRelay.Nostr.Filter do
@@ -27,30 +59,81 @@ defmodule GcIndexRelay.Nostr.Filter do
|
|
|
|
|
|> apply_since(filter.since) |
|
|
|
|
|> apply_until(filter.until) |
|
|
|
|
|> preload(:tags) |
|
|
|
|
|> apply_tags(filter.tags) |
|
|
|
|
# Always sort in descending order of creation time |
|
|
|
|
|> order_by([e], desc: e.created_at) |
|
|
|
|
|> apply_limit(filter.limit) |
|
|
|
|
|> Repo.all() |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@spec apply_ids(Ecto.Query.t(), [String.t()] | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_ids(query, nil), do: query |
|
|
|
|
defp apply_ids(query, []), do: query |
|
|
|
|
defp apply_ids(query, ids), do: where(query, [e], e.id in ^ids) |
|
|
|
|
|
|
|
|
|
@spec apply_authors(Ecto.Query.t(), [String.t()] | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_authors(query, nil), do: query |
|
|
|
|
defp apply_authors(query, []), do: query |
|
|
|
|
defp apply_authors(query, authors), do: where(query, [e], e.author in ^authors) |
|
|
|
|
|
|
|
|
|
@spec apply_kinds(Ecto.Query.t(), [integer()] | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_kinds(query, nil), do: query |
|
|
|
|
defp apply_kinds(query, []), do: query |
|
|
|
|
defp apply_kinds(query, kinds), do: where(query, [e], e.kind in ^kinds) |
|
|
|
|
|
|
|
|
|
@spec apply_since(Ecto.Query.t(), integer() | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_since(query, nil), do: query |
|
|
|
|
defp apply_since(query, since), do: where(query, [e], e.created_at >= ^since) |
|
|
|
|
|
|
|
|
|
@spec apply_until(Ecto.Query.t(), integer() | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_until(query, nil), do: query |
|
|
|
|
defp apply_until(query, until), do: where(query, [e], e.created_at <= ^until) |
|
|
|
|
|
|
|
|
|
@spec apply_tags(Ecto.Query.t(), map() | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_tags(query, nil), do: query |
|
|
|
|
defp apply_tags(query, tags) when map_size(tags) == 0, do: query |
|
|
|
|
|
|
|
|
|
defp apply_tags(query, tags) do |
|
|
|
|
query = from(e in query, as: :event_query) |
|
|
|
|
|
|
|
|
|
Enum.reduce(tags, query, fn {tag_name, tag_values}, acc_query -> |
|
|
|
|
where( |
|
|
|
|
acc_query, |
|
|
|
|
[e], |
|
|
|
|
exists( |
|
|
|
|
from t in Tag, |
|
|
|
|
where: t.event_id == parent_as(:event_query).id, |
|
|
|
|
where: t.name == ^tag_name, |
|
|
|
|
where: t.value in ^tag_values |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
end) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@spec apply_limit(Ecto.Query.t(), pos_integer() | nil) :: Ecto.Query.t() |
|
|
|
|
defp apply_limit(query, nil), do: query |
|
|
|
|
defp apply_limit(query, limit), do: limit(query, ^limit) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defimpl Jason.Encoder, for: GcIndexRelay.Nostr.Filter do |
|
|
|
|
alias GcIndexRelay.Nostr.Filter |
|
|
|
|
|
|
|
|
|
def encode(%Filter{} = filter, opts) do |
|
|
|
|
# Prefix single-letter tags with '#' |
|
|
|
|
tags_map = |
|
|
|
|
case filter.tags do |
|
|
|
|
nil -> %{} |
|
|
|
|
tags -> Enum.map(tags, fn {k, v} -> {"#" <> k, v} end) |> Map.new() |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Produce a map from the remaining filter fields |
|
|
|
|
rest_map = |
|
|
|
|
filter |
|
|
|
|
|> Map.from_struct() |
|
|
|
|
|> Map.delete(:tags) |
|
|
|
|
|
|
|
|
|
# Merge the tags into the remaining fields and encode with Jason |
|
|
|
|
Map.merge(rest_map, tags_map) |
|
|
|
|
|> Jason.Encode.map(opts) |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|