mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Merge pull request #184 from philomena-dev/limits
Add some basic limits to anonymous tag changes
This commit is contained in:
commit
392d920657
5 changed files with 185 additions and 1 deletions
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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"])
|
||||
|
|
109
lib/philomena/tag_changes/limits.ex
Normal file
109
lib/philomena/tag_changes/limits.ex
Normal 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
|
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue