From 731e4d8869d9d2652122b9a94b13ef30ede50851 Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Wed, 10 May 2023 22:43:20 -0400 Subject: [PATCH 1/6] Add some basic limits to anonymous tag changes --- lib/philomena/images.ex | 41 +++++++ lib/philomena/images/image.ex | 1 + lib/philomena/images/tag_validator.ex | 22 +++- lib/philomena/tag_changes/limits.ex | 109 ++++++++++++++++++ .../controllers/image/tag_controller.ex | 13 +++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 lib/philomena/tag_changes/limits.ex diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index db95b09ab..09705d164 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -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 @@ -419,6 +420,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 @@ -462,6 +466,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 diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index 83ce9409b..3e9c889aa 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -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 diff --git a/lib/philomena/images/tag_validator.ex b/lib/philomena/images/tag_validator.ex index 887b5daa8..8ffe3bdbc 100644 --- a/lib/philomena/images/tag_validator.ex +++ b/lib/philomena/images/tag_validator.ex @@ -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"]) diff --git a/lib/philomena/tag_changes/limits.ex b/lib/philomena/tag_changes/limits.ex new file mode 100644 index 000000000..496ef2c25 --- /dev/null +++ b/lib/philomena/tag_changes/limits.ex @@ -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 diff --git a/lib/philomena_web/controllers/image/tag_controller.ex b/lib/philomena_web/controllers/image/tag_controller.ex index 2bae9731c..468864c97 100644 --- a/lib/philomena_web/controllers/image/tag_controller.ex +++ b/lib/philomena_web/controllers/image/tag_controller.ex @@ -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 From 5def18e5f0bb8921310de8acb908f11d3209abac Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sat, 20 Jul 2024 22:04:14 -0700 Subject: [PATCH 2/6] fix(Philomena.DuplicateReports.SearchQuery): validate_required in image_changeset --- lib/philomena/duplicate_reports/search_query.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/philomena/duplicate_reports/search_query.ex b/lib/philomena/duplicate_reports/search_query.ex index bc9220773..23525c721 100644 --- a/lib/philomena/duplicate_reports/search_query.ex +++ b/lib/philomena/duplicate_reports/search_query.ex @@ -37,6 +37,16 @@ defmodule Philomena.DuplicateReports.SearchQuery do :image_aspect_ratio, :uploaded_image ]) + |> validate_required([ + :image_width, + :image_height, + :image_format, + :image_duration, + :image_mime_type, + :image_is_animated, + :image_aspect_ratio, + :uploaded_image + ]) |> validate_number(:image_width, greater_than: 0) |> validate_number(:image_height, greater_than: 0) |> validate_inclusion( From f6c511ce486a815bc192207e4c3901a5ba7ca60d Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 21 Jul 2024 19:12:41 -0400 Subject: [PATCH 3/6] Order detected duplicates based on L2 distance from query point --- lib/philomena/duplicate_reports.ex | 10 ++++++++++ lib/philomena/duplicate_reports/power.ex | 9 +++++++++ 2 files changed, 19 insertions(+) create mode 100644 lib/philomena/duplicate_reports/power.ex diff --git a/lib/philomena/duplicate_reports.ex b/lib/philomena/duplicate_reports.ex index 5f84c9daf..c6cb2c55d 100644 --- a/lib/philomena/duplicate_reports.ex +++ b/lib/philomena/duplicate_reports.ex @@ -3,7 +3,9 @@ defmodule Philomena.DuplicateReports do The DuplicateReports context. """ + import Philomena.DuplicateReports.Power import Ecto.Query, warn: false + alias Ecto.Multi alias Philomena.Repo @@ -46,6 +48,14 @@ defmodule Philomena.DuplicateReports do where: i.image_aspect_ratio >= ^(aspect_ratio - aspect_dist) and i.image_aspect_ratio <= ^(aspect_ratio + aspect_dist), + order_by: [ + asc: + power(it.nw - ^intensities.nw, 2) + + power(it.ne - ^intensities.ne, 2) + + power(it.sw - ^intensities.sw, 2) + + power(it.se - ^intensities.se, 2) + + power(i.image_aspect_ratio - ^aspect_ratio, 2) + ], limit: ^limit end diff --git a/lib/philomena/duplicate_reports/power.ex b/lib/philomena/duplicate_reports/power.ex new file mode 100644 index 000000000..32f1bc1c5 --- /dev/null +++ b/lib/philomena/duplicate_reports/power.ex @@ -0,0 +1,9 @@ +defmodule Philomena.DuplicateReports.Power do + @moduledoc false + + defmacro power(left, right) do + quote do + fragment("power(?, ?)", unquote(left), unquote(right)) + end + end +end From a9077f4bf47a6e98c40db41311dd2e5aebe3f4c0 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sun, 21 Jul 2024 18:54:01 -0700 Subject: [PATCH 4/6] fix: :uploaded_image instead of :image SearchQuery doesn't have :image so I don't think this error_tag would ever be reached --- lib/philomena_web/templates/search/reverse/index.html.slime | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/philomena_web/templates/search/reverse/index.html.slime b/lib/philomena_web/templates/search/reverse/index.html.slime index 83dd72a73..97d886864 100644 --- a/lib/philomena_web/templates/search/reverse/index.html.slime +++ b/lib/philomena_web/templates/search/reverse/index.html.slime @@ -13,7 +13,7 @@ h1 Reverse Search p Upload a file from your computer, or provide a link to the page containing the image and click Fetch. .field = file_input f, :image, class: "input js-scraper" - = error_tag f, :image + = error_tag f, :uploaded_image = error_tag f, :image_width = error_tag f, :image_height = error_tag f, :image_mime_type @@ -40,7 +40,7 @@ h1 Reverse Search = cond do - is_nil(@images) -> - + - Enum.any?(@images) -> h2 Results @@ -49,7 +49,7 @@ h1 Reverse Search th   th Image th   - + = for match <- @images do tr th From 9ca1a50a0af6667e9aaf1dc90a6a5353c0cc8508 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sun, 21 Jul 2024 19:05:35 -0700 Subject: [PATCH 5/6] feat: use keyCode instead of key --- assets/js/shortcuts.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts index 682863ef1..3de21c89d 100644 --- a/assets/js/shortcuts.ts +++ b/assets/js/shortcuts.ts @@ -4,7 +4,7 @@ import { $ } from './utils/dom'; -type ShortcutKeyMap = Record void>; +type ShortcutKeyMap = Record void>; function getHover(): string | null { const thumbBoxHover = $('.media-box:hover'); @@ -50,19 +50,19 @@ function isOK(event: KeyboardEvent): boolean { /* eslint-disable prettier/prettier */ const keyCodes: ShortcutKeyMap = { - j() { click('.js-prev'); }, // J - go to previous image - i() { click('.js-up'); }, // I - go to index page - k() { click('.js-next'); }, // K - go to next image - r() { click('.js-rand'); }, // R - go to random image - s() { click('.js-source-link'); }, // S - go to image source - l() { click('.js-tag-sauce-toggle'); }, // L - edit tags - o() { openFullView(); }, // O - open original - v() { openFullViewNewTab(); }, // V - open original in a new tab - f() { + 74() { click('.js-prev'); }, // J - go to previous image + 73() { click('.js-up'); }, // I - go to index page + 75() { click('.js-next'); }, // K - go to next image + 82() { click('.js-rand'); }, // R - go to random image + 83() { click('.js-source-link'); }, // S - go to image source + 76() { click('.js-tag-sauce-toggle'); }, // L - edit tags + 79() { openFullView(); }, // O - open original + 86() { openFullViewNewTab(); }, // V - open original in a new tab + 70() { // F - favourite image click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]` : '.block__header a.interaction--fave'); }, - u() { + 85() { // U - upvote image click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]` : '.block__header a.interaction--upvote'); }, @@ -72,8 +72,8 @@ const keyCodes: ShortcutKeyMap = { export function listenForKeys() { document.addEventListener('keydown', (event: KeyboardEvent) => { - if (isOK(event) && keyCodes[event.key]) { - keyCodes[event.key](); + if (isOK(event) && keyCodes[event.keyCode]) { + keyCodes[event.keyCode](); event.preventDefault(); } }); From b442d983b05f8afe7e57148240477d37e99b562d Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sun, 21 Jul 2024 19:08:38 -0700 Subject: [PATCH 6/6] feat: use keyCode for markdown shortcuts --- assets/js/markdowntoolbar.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/assets/js/markdowntoolbar.js b/assets/js/markdowntoolbar.js index 534388f68..05c8eb8e9 100644 --- a/assets/js/markdowntoolbar.js +++ b/assets/js/markdowntoolbar.js @@ -7,19 +7,19 @@ import { $, $$ } from './utils/dom'; const markdownSyntax = { bold: { action: wrapSelection, - options: { prefix: '**', shortcutKey: 'b' }, + options: { prefix: '**', shortcutKeyCode: 66 }, }, italics: { action: wrapSelection, - options: { prefix: '*', shortcutKey: 'i' }, + options: { prefix: '*', shortcutKeyCode: 73 }, }, under: { action: wrapSelection, - options: { prefix: '__', shortcutKey: 'u' }, + options: { prefix: '__', shortcutKeyCode: 85 }, }, spoiler: { action: wrapSelection, - options: { prefix: '||', shortcutKey: 's' }, + options: { prefix: '||', shortcutKeyCode: 83 }, }, code: { action: wrapSelectionOrLines, @@ -29,7 +29,7 @@ const markdownSyntax = { prefixMultiline: '```\n', suffixMultiline: '\n```', singleWrap: true, - shortcutKey: 'e', + shortcutKeyCode: 69, }, }, strike: { @@ -50,11 +50,11 @@ const markdownSyntax = { }, link: { action: insertLink, - options: { shortcutKey: 'l' }, + options: { shortcutKeyCode: 76 }, }, image: { action: insertLink, - options: { image: true, shortcutKey: 'k' }, + options: { image: true, shortcutKeyCode: 75 }, }, escape: { action: escapeSelection, @@ -257,10 +257,10 @@ function shortcutHandler(event) { } const textarea = event.target, - key = event.key.toLowerCase(); + keyCode = event.keyCode; for (const id in markdownSyntax) { - if (key === markdownSyntax[id].options.shortcutKey) { + if (keyCode === markdownSyntax[id].options.shortcutKeyCode) { markdownSyntax[id].action(textarea, markdownSyntax[id].options); event.preventDefault(); }