227 lines
6 KiB
Elixir
227 lines
6 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Filter do
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
import Ecto.Query
|
|
|
|
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)
|
|
field(:hide, :boolean, default: false)
|
|
field(:whole_word, :boolean, default: true)
|
|
field(:phrase, :string)
|
|
field(:context, {:array, :string})
|
|
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 __MODULE__,
|
|
where: f.filter_id == ^id,
|
|
where: f.user_id == ^user_id
|
|
)
|
|
|
|
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(
|
|
f in query,
|
|
where: f.user_id == ^user_id,
|
|
order_by: [desc: :id]
|
|
)
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@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
|
|
# same time, but that should be unlikely.
|
|
|
|
max_id_query =
|
|
from(
|
|
f in __MODULE__,
|
|
where: f.user_id == ^user_id,
|
|
select: max(f.filter_id)
|
|
)
|
|
|
|
filter_id =
|
|
case Repo.one(max_id_query) do
|
|
# Start allocating from 1
|
|
nil ->
|
|
1
|
|
|
|
max_id ->
|
|
max_id + 1
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
|
|
defp maybe_add_expires_at(changeset, %{expires_in: nil}) do
|
|
change(changeset, expires_at: nil)
|
|
end
|
|
|
|
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
|
|
__MODULE__
|
|
|> get_active()
|
|
|> get_irreversible()
|
|
|> get_filters(user)
|
|
|> compose_regex(format)
|
|
end
|
|
|
|
def compose_regex([_ | _] = filters, format) do
|
|
phrases =
|
|
filters
|
|
|> Enum.map(& &1.phrase)
|
|
|> Enum.join("|")
|
|
|
|
case format do
|
|
:postgres ->
|
|
"\\y(#{phrases})\\y"
|
|
|
|
:re ->
|
|
~r/\b#{phrases}\b/i
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
def compose_regex(_, _), do: nil
|
|
end
|