Add some basic limits to anonymous tag changes

This commit is contained in:
byte[] 2023-05-10 22:43:20 -04:00 committed by Liam
parent 844e0a3535
commit 731e4d8869
5 changed files with 185 additions and 1 deletions

View file

@ -24,6 +24,7 @@ defmodule Philomena.Images do
alias Philomena.SourceChanges.SourceChange alias Philomena.SourceChanges.SourceChange
alias Philomena.Notifications.Notification alias Philomena.Notifications.Notification
alias Philomena.NotificationWorker alias Philomena.NotificationWorker
alias Philomena.TagChanges.Limits
alias Philomena.TagChanges.TagChange alias Philomena.TagChanges.TagChange
alias Philomena.Tags alias Philomena.Tags
alias Philomena.UserStatistics alias Philomena.UserStatistics
@ -419,6 +420,9 @@ defmodule Philomena.Images do
error error
end end
end) end)
|> Multi.run(:check_limits, fn _repo, %{image: {image, _added, _removed}} ->
check_tag_change_limits_before_commit(image, attribution)
end)
|> Multi.run(:added_tag_changes, fn repo, %{image: {image, added_tags, _removed}} -> |> Multi.run(:added_tag_changes, fn repo, %{image: {image, added_tags, _removed}} ->
tag_changes = tag_changes =
added_tags added_tags
@ -462,6 +466,43 @@ defmodule Philomena.Images do
{:ok, count} {:ok, count}
end) end)
|> Repo.transaction() |> Repo.transaction()
|> case do
{:ok, %{image: {image, _added, _removed}}} = res ->
update_tag_change_limits_after_commit(image, attribution)
res
err ->
err
end
end
defp check_tag_change_limits_before_commit(image, attribution) do
tag_changed_count = length(image.added_tags) + length(image.removed_tags)
rating_changed = image.ratings_changed
user = attribution[:user]
ip = attribution[:ip]
cond do
Limits.limited_for_tag_count?(user, ip, tag_changed_count) ->
{:error, :limit_exceeded}
rating_changed and Limits.limited_for_rating_count?(user, ip) ->
{:error, :limit_exceeded}
true ->
{:ok, 0}
end
end
def update_tag_change_limits_after_commit(image, attribution) do
rating_changed_count = if(image.ratings_changed, do: 1, else: 0)
tag_changed_count = length(image.added_tags) + length(image.removed_tags)
user = attribution[:user]
ip = attribution[:ip]
Limits.update_tag_count_after_update(user, ip, tag_changed_count)
Limits.update_rating_count_after_update(user, ip, rating_changed_count)
end end
defp tag_change_attributes(attribution, image, tag, added, user) do defp tag_change_attributes(attribution, image, tag, added, user) do

View file

@ -96,6 +96,7 @@ defmodule Philomena.Images.Image do
field :added_tags, {:array, :any}, default: [], virtual: true field :added_tags, {:array, :any}, default: [], virtual: true
field :removed_sources, {:array, :any}, default: [], virtual: true field :removed_sources, {:array, :any}, default: [], virtual: true
field :added_sources, {:array, :any}, default: [], virtual: true field :added_sources, {:array, :any}, default: [], virtual: true
field :ratings_changed, :boolean, default: false, virtual: true
field :uploaded_image, :string, virtual: true field :uploaded_image, :string, virtual: true
field :removed_image, :string, virtual: true field :removed_image, :string, virtual: true

View file

@ -5,7 +5,20 @@ defmodule Philomena.Images.TagValidator do
def validate_tags(changeset) do def validate_tags(changeset) do
tags = changeset |> get_field(:tags) tags = changeset |> get_field(:tags)
validate_tag_input(changeset, tags) changeset
|> validate_tag_input(tags)
|> set_rating_changed()
end
defp set_rating_changed(changeset) do
added_tags = changeset |> get_field(:added_tags) |> extract_names()
removed_tags = changeset |> get_field(:removed_tags) |> extract_names()
ratings = all_ratings()
added_ratings = MapSet.intersection(ratings, added_tags) |> MapSet.size()
removed_ratings = MapSet.intersection(ratings, removed_tags) |> MapSet.size()
put_change(changeset, :ratings_changed, added_ratings + removed_ratings > 0)
end end
defp validate_tag_input(changeset, tags) do defp validate_tag_input(changeset, tags) do
@ -108,6 +121,13 @@ defmodule Philomena.Images.TagValidator do
|> MapSet.new() |> MapSet.new()
end end
defp all_ratings do
safe_rating()
|> MapSet.union(sexual_ratings())
|> MapSet.union(horror_ratings())
|> MapSet.union(gross_rating())
end
defp safe_rating, do: MapSet.new(["safe"]) defp safe_rating, do: MapSet.new(["safe"])
defp sexual_ratings, do: MapSet.new(["suggestive", "questionable", "explicit"]) defp sexual_ratings, do: MapSet.new(["suggestive", "questionable", "explicit"])
defp horror_ratings, do: MapSet.new(["semi-grimdark", "grimdark"]) defp horror_ratings, do: MapSet.new(["semi-grimdark", "grimdark"])

View file

@ -0,0 +1,109 @@
defmodule Philomena.TagChanges.Limits do
@moduledoc """
Tag change limits for anonymous users.
"""
@tag_changes_per_ten_minutes 50
@rating_changes_per_ten_minutes 1
@ten_minutes_in_seconds 10 * 60
@doc """
Determine if the current user and IP can make any tag changes at all.
The user may be limited due to making more than 50 tag changes in the past 10 minutes.
Should be used in tandem with `update_tag_count_after_update/3`.
## Examples
iex> limited_for_tag_count?(%User{}, %Postgrex.INET{})
false
iex> limited_for_tag_count?(%User{}, %Postgrex.INET{}, 72)
true
"""
def limited_for_tag_count?(user, ip, additional \\ 0) do
check_limit(user, tag_count_key_for_ip(ip), @tag_changes_per_ten_minutes, additional)
end
@doc """
Determine if the current user and IP can make rating tag changes.
The user may be limited due to making more than one rating tag change in the past 10 minutes.
Should be used in tandem with `update_rating_count_after_update/3`.
## Examples
iex> limited_for_rating_count?(%User{}, %Postgrex.INET{})
false
iex> limited_for_rating_count?(%User{}, %Postgrex.INET{}, 2)
true
"""
def limited_for_rating_count?(user, ip) do
check_limit(user, rating_count_key_for_ip(ip), @rating_changes_per_ten_minutes, 0)
end
@doc """
Post-transaction update for successful tag changes.
Should be used in tandem with `limited_for_tag_count?/2`.
## Examples
iex> update_tag_count_after_update(%User{}, %Postgrex.INET{}, 25)
:ok
"""
def update_tag_count_after_update(user, ip, amount) do
increment_counter(user, tag_count_key_for_ip(ip), amount, @ten_minutes_in_seconds)
end
@doc """
Post-transaction update for successful rating tag changes.
Should be used in tandem with `limited_for_rating_count?/2`.
## Examples
iex> update_rating_count_after_update(%User{}, %Postgrex.INET{}, 1)
:ok
"""
def update_rating_count_after_update(user, ip, amount) do
increment_counter(user, rating_count_key_for_ip(ip), amount, @ten_minutes_in_seconds)
end
defp check_limit(user, key, limit, additional) do
if considered_for_limit?(user) do
amt = Redix.command!(:redix, ["GET", key]) || 0
amt + additional >= limit
else
false
end
end
defp increment_counter(user, key, amount, expiration) do
if considered_for_limit?(user) do
Redix.pipeline!(:redix, [
["INCRBY", key, amount],
["EXPIRE", key, expiration]
])
end
:ok
end
defp considered_for_limit?(user) do
is_nil(user) or not user.verified
end
defp tag_count_key_for_ip(ip) do
"rltcn:#{ip}"
end
defp rating_count_key_for_ip(ip) do
"rltcr:#{ip}"
end
end

View file

@ -8,6 +8,7 @@ defmodule PhilomenaWeb.Image.TagController do
alias Philomena.Images alias Philomena.Images
alias Philomena.Tags alias Philomena.Tags
alias Philomena.Repo alias Philomena.Repo
alias Plug.Conn
import Ecto.Query import Ecto.Query
plug PhilomenaWeb.LimitPlug, plug PhilomenaWeb.LimitPlug,
@ -88,6 +89,18 @@ defmodule PhilomenaWeb.Image.TagController do
image: image, image: image,
changeset: changeset changeset: changeset
) )
{:error, :check_limits, _error, _} ->
conn
|> put_flash(:error, "Too many tags changed. Change fewer tags or try again later.")
|> Conn.send_resp(:multiple_choices, "")
|> Conn.halt()
_err ->
conn
|> put_flash(:error, "Failed to update tags!")
|> Conn.send_resp(:multiple_choices, "")
|> Conn.halt()
end end
end end
end end