Skip to content

Commit

Permalink
Merge pull request #184 from philomena-dev/limits
Browse files Browse the repository at this point in the history
Add some basic limits to anonymous tag changes
  • Loading branch information
liamwhite authored Jul 23, 2024
2 parents d1f4eb9 + 731e4d8 commit 392d920
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 1 deletion.
41 changes: 41 additions & 0 deletions lib/philomena/images.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule Philomena.Images do
alias Philomena.SourceChanges.SourceChange
alias Philomena.Notifications.Notification
alias Philomena.NotificationWorker
alias Philomena.TagChanges.Limits
alias Philomena.TagChanges.TagChange
alias Philomena.Tags
alias Philomena.UserStatistics
Expand Down Expand Up @@ -417,6 +418,9 @@ defmodule Philomena.Images do
error
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}} ->
tag_changes =
added_tags
Expand Down Expand Up @@ -460,6 +464,43 @@ defmodule Philomena.Images do
{:ok, count}
end)
|> 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

defp tag_change_attributes(attribution, image, tag, added, user) do
Expand Down
1 change: 1 addition & 0 deletions lib/philomena/images/image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ defmodule Philomena.Images.Image do
field :added_tags, {:array, :any}, default: [], virtual: true
field :removed_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 :removed_image, :string, virtual: true
Expand Down
22 changes: 21 additions & 1 deletion lib/philomena/images/tag_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ defmodule Philomena.Images.TagValidator do
def validate_tags(changeset) do
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

defp validate_tag_input(changeset, tags) do
Expand Down Expand Up @@ -108,6 +121,13 @@ defmodule Philomena.Images.TagValidator do
|> MapSet.new()
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 sexual_ratings, do: MapSet.new(["suggestive", "questionable", "explicit"])
defp horror_ratings, do: MapSet.new(["semi-grimdark", "grimdark"])
Expand Down
109 changes: 109 additions & 0 deletions lib/philomena/tag_changes/limits.ex
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions lib/philomena_web/controllers/image/tag_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule PhilomenaWeb.Image.TagController do
alias Philomena.Images
alias Philomena.Tags
alias Philomena.Repo
alias Plug.Conn
import Ecto.Query

plug PhilomenaWeb.LimitPlug,
Expand Down Expand Up @@ -88,6 +89,18 @@ defmodule PhilomenaWeb.Image.TagController do
image: image,
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

0 comments on commit 392d920

Please sign in to comment.