226 lines
		
	
	
	
		
			6 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			226 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
 |