aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAlexander Strizhakov <alex.strizhakov@gmail.com>2021-01-25 15:34:59 +0300
committerAlexander Strizhakov <alex.strizhakov@gmail.com>2021-01-26 08:27:45 +0300
commit875fbaae35fe73f01d7fd55c255043dda929e365 (patch)
tree97df4e86816201611afe5dbe1c4eee772b8f913b /lib
parent250e2020987b1fc65251ba9564e41b38ba060391 (diff)
downloadpleroma-875fbaae35fe73f01d7fd55c255043dda929e365.tar.gz
support for expires_in/expires_at in filters
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/filter.ex124
-rw-r--r--lib/pleroma/web/api_spec/operations/filter_operation.ex33
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex61
-rw-r--r--lib/pleroma/workers/purge_expired_filter.ex43
4 files changed, 202 insertions, 59 deletions
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index fc531f7fc..82b9caf9b 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -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
+
+ # 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 create(%Pleroma.Filter{} = filter) do
- Repo.insert(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_number(filter_key) do
- Repo.delete(filter)
+ defp maybe_add_expires_at(changeset, %{expires_in: nil}) do
+ change(changeset, expires_at: nil)
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})
+ defp maybe_add_expires_at(changeset, _), do: changeset
- filter
- |> Map.put(:id, id)
- |> Repo.delete()
+ 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
- def update(%Pleroma.Filter{} = filter, params) do
+ 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
diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
index c5b0c035b..9374a7868 100644
--- a/lib/pleroma/web/api_spec/operations/filter_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -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: %{
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index c8b4a3095..9b1ae809d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -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
diff --git a/lib/pleroma/workers/purge_expired_filter.ex b/lib/pleroma/workers/purge_expired_filter.ex
new file mode 100644
index 000000000..9c3db8af2
--- /dev/null
+++ b/lib/pleroma/workers/purge_expired_filter.ex
@@ -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