support for expires_in/expires_at in filters
This commit is contained in:
parent
250e202098
commit
875fbaae35
11 changed files with 709 additions and 234 deletions
|
|
@ -11,6 +11,9 @@ defmodule Pleroma.Filter do
|
|||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
@type t() :: %__MODULE__{}
|
||||
@type format() :: :postgres | :re
|
||||
|
||||
schema "filters" do
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
field(:filter_id, :integer)
|
||||
|
|
@ -18,15 +21,16 @@ defmodule Pleroma.Filter do
|
|||
field(:whole_word, :boolean, default: true)
|
||||
field(:phrase, :string)
|
||||
field(:context, {:array, :string})
|
||||
field(:expires_at, :utc_datetime)
|
||||
field(:expires_at, :naive_datetime)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@spec get(integer() | String.t(), User.t()) :: t() | nil
|
||||
def get(id, %{id: user_id} = _user) do
|
||||
query =
|
||||
from(
|
||||
f in Pleroma.Filter,
|
||||
f in __MODULE__,
|
||||
where: f.filter_id == ^id,
|
||||
where: f.user_id == ^user_id
|
||||
)
|
||||
|
|
@ -34,14 +38,17 @@ defmodule Pleroma.Filter do
|
|||
Repo.one(query)
|
||||
end
|
||||
|
||||
@spec get_active(Ecto.Query.t() | module()) :: Ecto.Query.t()
|
||||
def get_active(query) do
|
||||
from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
|
||||
end
|
||||
|
||||
@spec get_irreversible(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
def get_irreversible(query) do
|
||||
from(f in query, where: f.hide)
|
||||
end
|
||||
|
||||
@spec get_filters(Ecto.Query.t() | module(), User.t()) :: [t()]
|
||||
def get_filters(query \\ __MODULE__, %User{id: user_id}) do
|
||||
query =
|
||||
from(
|
||||
|
|
@ -53,7 +60,32 @@ defmodule Pleroma.Filter do
|
|||
Repo.all(query)
|
||||
end
|
||||
|
||||
def create(%Pleroma.Filter{user_id: user_id, filter_id: nil} = filter) do
|
||||
@spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(attrs \\ %{}) do
|
||||
Repo.transaction(fn -> create_with_expiration(attrs) end)
|
||||
end
|
||||
|
||||
defp create_with_expiration(attrs) do
|
||||
with {:ok, filter} <- do_create(attrs),
|
||||
{:ok, _} <- maybe_add_expiration_job(filter) do
|
||||
filter
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_create(attrs) do
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, [:phrase, :context, :hide, :expires_at, :whole_word, :user_id, :filter_id])
|
||||
|> maybe_add_filter_id()
|
||||
|> validate_required([:phrase, :context, :user_id, :filter_id])
|
||||
|> maybe_add_expires_at(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
defp maybe_add_filter_id(%{changes: %{filter_id: _}} = changeset), do: changeset
|
||||
|
||||
defp maybe_add_filter_id(%{changes: %{user_id: user_id}} = changeset) do
|
||||
# If filter_id wasn't given, use the max filter_id for this user plus 1.
|
||||
# XXX This could result in a race condition if a user tries to add two
|
||||
# different filters for their account from two different clients at the
|
||||
|
|
@ -61,7 +93,7 @@ defmodule Pleroma.Filter do
|
|||
|
||||
max_id_query =
|
||||
from(
|
||||
f in Pleroma.Filter,
|
||||
f in __MODULE__,
|
||||
where: f.user_id == ^user_id,
|
||||
select: max(f.filter_id)
|
||||
)
|
||||
|
|
@ -76,34 +108,92 @@ defmodule Pleroma.Filter do
|
|||
max_id + 1
|
||||
end
|
||||
|
||||
filter
|
||||
|> Map.put(:filter_id, filter_id)
|
||||
|> Repo.insert()
|
||||
change(changeset, filter_id: filter_id)
|
||||
end
|
||||
|
||||
def create(%Pleroma.Filter{} = filter) do
|
||||
Repo.insert(filter)
|
||||
# don't override expires_at, if passed expires_at and expires_in
|
||||
defp maybe_add_expires_at(%{changes: %{expires_at: %NaiveDateTime{} = _}} = changeset, _) do
|
||||
changeset
|
||||
end
|
||||
|
||||
def delete(%Pleroma.Filter{id: filter_key} = filter) when is_number(filter_key) do
|
||||
Repo.delete(filter)
|
||||
defp maybe_add_expires_at(changeset, %{expires_in: expires_in})
|
||||
when is_integer(expires_in) and expires_in > 0 do
|
||||
expires_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(expires_in)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
change(changeset, expires_at: expires_at)
|
||||
end
|
||||
|
||||
def delete(%Pleroma.Filter{id: filter_key} = filter) when is_nil(filter_key) do
|
||||
%Pleroma.Filter{id: id} = get(filter.filter_id, %{id: filter.user_id})
|
||||
|
||||
filter
|
||||
|> Map.put(:id, id)
|
||||
|> Repo.delete()
|
||||
defp maybe_add_expires_at(changeset, %{expires_in: nil}) do
|
||||
change(changeset, expires_at: nil)
|
||||
end
|
||||
|
||||
def update(%Pleroma.Filter{} = filter, params) do
|
||||
defp maybe_add_expires_at(changeset, _), do: changeset
|
||||
|
||||
defp maybe_add_expiration_job(%{expires_at: %NaiveDateTime{} = expires_at} = filter) do
|
||||
Pleroma.Workers.PurgeExpiredFilter.enqueue(%{
|
||||
filter_id: filter.id,
|
||||
expires_at: DateTime.from_naive!(expires_at, "Etc/UTC")
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_add_expiration_job(_), do: {:ok, nil}
|
||||
|
||||
@spec delete(t()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(%__MODULE__{} = filter) do
|
||||
Repo.transaction(fn -> delete_with_expiration(filter) end)
|
||||
end
|
||||
|
||||
defp delete_with_expiration(filter) do
|
||||
with {:ok, _} <- maybe_delete_old_expiration_job(filter, nil),
|
||||
{:ok, filter} <- Repo.delete(filter) do
|
||||
filter
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
@spec update(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%__MODULE__{} = filter, params) do
|
||||
Repo.transaction(fn -> update_with_expiration(filter, params) end)
|
||||
end
|
||||
|
||||
defp update_with_expiration(filter, params) do
|
||||
with {:ok, updated} <- do_update(filter, params),
|
||||
{:ok, _} <- maybe_delete_old_expiration_job(filter, updated),
|
||||
{:ok, _} <-
|
||||
maybe_add_expiration_job(updated) do
|
||||
updated
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(filter, params) do
|
||||
filter
|
||||
|> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
|
||||
|> validate_required([:phrase, :context])
|
||||
|> maybe_add_expires_at(params)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp maybe_delete_old_expiration_job(%{expires_at: nil}, _), do: {:ok, nil}
|
||||
|
||||
defp maybe_delete_old_expiration_job(%{expires_at: expires_at}, %{expires_at: expires_at}) do
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
defp maybe_delete_old_expiration_job(%{id: id}, _) do
|
||||
with %Oban.Job{} = job <- Pleroma.Workers.PurgeExpiredFilter.get_expiration(id) do
|
||||
Repo.delete(job)
|
||||
else
|
||||
nil -> {:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@spec compose_regex(User.t() | [t()], format()) :: String.t() | Regex.t() | nil
|
||||
def compose_regex(user_or_filters, format \\ :postgres)
|
||||
|
||||
def compose_regex(%User{} = user, format) do
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
|
||||
def open_api_operation(action) do
|
||||
|
|
@ -20,7 +21,8 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
operationId: "FilterController.index",
|
||||
security: [%{"oAuth" => ["read:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filters", "application/json", array_of_filters())
|
||||
200 => Operation.response("Filters", "application/json", array_of_filters()),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -32,7 +34,10 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
operationId: "FilterController.create",
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{200 => Operation.response("Filter", "application/json", filter())}
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter()),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -44,7 +49,9 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
operationId: "FilterController.show",
|
||||
security: [%{"oAuth" => ["read:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter())
|
||||
200 => Operation.response("Filter", "application/json", filter()),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -58,7 +65,8 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter())
|
||||
200 => Operation.response("Filter", "application/json", filter()),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -75,7 +83,8 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
Operation.response("Filter", "application/json", %Schema{
|
||||
type: :object,
|
||||
description: "Empty object"
|
||||
})
|
||||
}),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -210,15 +219,13 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
nullable: true,
|
||||
description: "Consider word boundaries?",
|
||||
default: true
|
||||
},
|
||||
expires_in: %Schema{
|
||||
nullable: true,
|
||||
type: :integer,
|
||||
description:
|
||||
"Number of seconds from now the filter should expire. Otherwise, null for a filter that doesn't expire."
|
||||
}
|
||||
# TODO: probably should implement filter expiration
|
||||
# expires_in: %Schema{
|
||||
# type: :string,
|
||||
# format: :"date-time",
|
||||
# description:
|
||||
# "ISO 8601 Datetime for when the filter expires. Otherwise,
|
||||
# null for a filter that doesn't expire."
|
||||
# }
|
||||
},
|
||||
required: [:phrase, :context],
|
||||
example: %{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
|
|||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
@doc "GET /api/v1/filters"
|
||||
def index(%{assigns: %{user: user}} = conn, _) do
|
||||
filters = Filter.get_filters(user)
|
||||
|
|
@ -29,25 +31,23 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
|
|||
|
||||
@doc "POST /api/v1/filters"
|
||||
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||
query = %Filter{
|
||||
user_id: user.id,
|
||||
phrase: params.phrase,
|
||||
context: params.context,
|
||||
hide: params.irreversible,
|
||||
whole_word: params.whole_word
|
||||
# TODO: support `expires_in` parameter (as in Mastodon API)
|
||||
}
|
||||
|
||||
{:ok, response} = Filter.create(query)
|
||||
|
||||
render(conn, "show.json", filter: response)
|
||||
with {:ok, response} <-
|
||||
params
|
||||
|> Map.put(:user_id, user.id)
|
||||
|> Map.put(:hide, params[:irreversible])
|
||||
|> Map.delete(:irreversible)
|
||||
|> Filter.create() do
|
||||
render(conn, "show.json", filter: response)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/filters/:id"
|
||||
def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
|
||||
filter = Filter.get(filter_id, user)
|
||||
|
||||
render(conn, "show.json", filter: filter)
|
||||
with %Filter{} = filter <- Filter.get(filter_id, user) do
|
||||
render(conn, "show.json", filter: filter)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "PUT /api/v1/filters/:id"
|
||||
|
|
@ -56,28 +56,31 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
|
|||
%{id: filter_id}
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Map.delete(:irreversible)
|
||||
|> Map.put(:hide, params[:irreversible])
|
||||
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||
|> Map.new()
|
||||
|
||||
# TODO: support `expires_in` parameter (as in Mastodon API)
|
||||
if is_boolean(params[:irreversible]) do
|
||||
params
|
||||
|> Map.put(:hide, params[:irreversible])
|
||||
|> Map.delete(:irreversible)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
with %Filter{} = filter <- Filter.get(filter_id, user),
|
||||
{:ok, %Filter{} = filter} <- Filter.update(filter, params) do
|
||||
render(conn, "show.json", filter: filter)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@doc "DELETE /api/v1/filters/:id"
|
||||
def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
|
||||
query = %Filter{
|
||||
user_id: user.id,
|
||||
filter_id: filter_id
|
||||
}
|
||||
|
||||
{:ok, _} = Filter.delete(query)
|
||||
json(conn, %{})
|
||||
with %Filter{} = filter <- Filter.get(filter_id, user),
|
||||
{:ok, _} <- Filter.delete(filter) do
|
||||
json(conn, %{})
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
43
lib/pleroma/workers/purge_expired_filter.ex
Normal file
43
lib/pleroma/workers/purge_expired_filter.ex
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.PurgeExpiredFilter do
|
||||
@moduledoc """
|
||||
Worker which purges expired filters
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: :filter_expiration, max_attempts: 1, unique: [fields: [:args]]
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Oban.Job
|
||||
alias Pleroma.Repo
|
||||
|
||||
@spec enqueue(%{filter_id: integer(), expires_at: DateTime.t()}) ::
|
||||
{:ok, Job.t()} | {:error, Ecto.Changeset.t()}
|
||||
def enqueue(args) do
|
||||
{scheduled_at, args} = Map.pop(args, :expires_at)
|
||||
|
||||
args
|
||||
|> new(scheduled_at: scheduled_at)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def perform(%Job{args: %{"filter_id" => id}}) do
|
||||
Pleroma.Filter
|
||||
|> Repo.get(id)
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
@spec get_expiration(pos_integer()) :: Job.t() | nil
|
||||
def get_expiration(id) do
|
||||
from(j in Job,
|
||||
where: j.state == "scheduled",
|
||||
where: j.queue == "filter_expiration",
|
||||
where: fragment("?->'filter_id' = ?", j.args, ^id)
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue