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.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
|
||||||
|
@ -417,6 +418,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
|
||||||
|
@ -460,6 +464,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
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.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
|
||||||
|
|
Loading…
Reference in a new issue