From 43fd14f0a86fb3383a385deefe5ec6ab4f126ba0 Mon Sep 17 00:00:00 2001 From: Luna D Date: Thu, 6 Jun 2024 20:32:36 -0400 Subject: [PATCH 01/44] Convert staffhider to TypeScript --- assets/js/staffhider.js | 17 ----------------- assets/js/staffhider.ts | 13 +++++++++++++ assets/types/booru-object.d.ts | 4 ++++ 3 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 assets/js/staffhider.js create mode 100644 assets/js/staffhider.ts diff --git a/assets/js/staffhider.js b/assets/js/staffhider.js deleted file mode 100644 index 68b64447..00000000 --- a/assets/js/staffhider.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * StaffHider - * - * Hide staff elements if enabled in the settings. - */ - -import { $$ } from './utils/dom'; - -function hideStaffTools() { - if (window.booru.hideStaffTools === 'true') { - $$('.js-staff-action').forEach(el => { - el.classList.add('hidden'); - }); - } -} - -export { hideStaffTools }; diff --git a/assets/js/staffhider.ts b/assets/js/staffhider.ts new file mode 100644 index 00000000..86741d78 --- /dev/null +++ b/assets/js/staffhider.ts @@ -0,0 +1,13 @@ +/** + * StaffHider + * + * Hide staff elements if enabled in the settings. + */ + +import { $$, hideEl } from './utils/dom'; + +export function hideStaffTools() { + if (window.booru.hideStaffTools === 'true') { + $$('.js-staff-action').forEach(el => hideEl(el)); + } +} diff --git a/assets/types/booru-object.d.ts b/assets/types/booru-object.d.ts index 22d1aa08..2a5949c0 100644 --- a/assets/types/booru-object.d.ts +++ b/assets/types/booru-object.d.ts @@ -65,6 +65,10 @@ interface BooruObject { spoileredFilter: AstMatcher; tagsVersion: number; interactions: Interaction[]; + /** + * Indicates whether sensitive staff-only info should be hidden or not. + */ + hideStaffTools: string; } declare global { From b42c438dd06987e9057b150a979d971b6542d68a Mon Sep 17 00:00:00 2001 From: Luna D Date: Thu, 6 Jun 2024 20:37:11 -0400 Subject: [PATCH 02/44] Remove long-obsolete owner options scripts --- assets/js/comment.js | 5 ----- assets/js/communications/comment.js | 10 ---------- assets/js/communications/post.js | 10 ---------- assets/js/when-ready.js | 5 ----- 4 files changed, 30 deletions(-) delete mode 100644 assets/js/communications/comment.js delete mode 100644 assets/js/communications/post.js diff --git a/assets/js/comment.js b/assets/js/comment.js index de245fa8..5dbdfac6 100644 --- a/assets/js/comment.js +++ b/assets/js/comment.js @@ -3,7 +3,6 @@ */ import { $ } from './utils/dom'; -import { showOwnedComments } from './communications/comment'; import { filterNode } from './imagesclientside'; import { fetchHtml } from './utils/requests'; import { timeAgo } from './timeago'; @@ -131,9 +130,6 @@ function displayComments(container, commentsHtml) { // Filter images in the comments filterNode(container); - // Show options on own comments - showOwnedComments(); - } function loadComments(event) { @@ -175,7 +171,6 @@ function setupComments() { } else { filterNode(comments); - showOwnedComments(); } } diff --git a/assets/js/communications/comment.js b/assets/js/communications/comment.js deleted file mode 100644 index a4661c61..00000000 --- a/assets/js/communications/comment.js +++ /dev/null @@ -1,10 +0,0 @@ -import { $ } from '../utils/dom'; - -function showOwnedComments() { - const editableComments = $('.js-editable-comments'); - const editableCommentIds = editableComments && JSON.parse(editableComments.dataset.editable); - - if (editableCommentIds) editableCommentIds.forEach(id => $(`#comment_${id} .owner-options`).classList.remove('hidden')); -} - -export { showOwnedComments }; diff --git a/assets/js/communications/post.js b/assets/js/communications/post.js deleted file mode 100644 index d2172220..00000000 --- a/assets/js/communications/post.js +++ /dev/null @@ -1,10 +0,0 @@ -import { $ } from '../utils/dom'; - -function showOwnedPosts() { - const editablePost = $('.js-editable-posts'); - const editablePostIds = editablePost && JSON.parse(editablePost.dataset.editable); - - if (editablePostIds) editablePostIds.forEach(id => $(`#post_${id} .owner-options`).classList.remove('hidden')); -} - -export { showOwnedPosts }; diff --git a/assets/js/when-ready.js b/assets/js/when-ready.js index b17e247b..3bd3e049 100644 --- a/assets/js/when-ready.js +++ b/assets/js/when-ready.js @@ -4,9 +4,6 @@ import { whenReady } from './utils/dom'; -import { showOwnedComments } from './communications/comment'; -import { showOwnedPosts } from './communications/post'; - import { listenAutocomplete } from './autocomplete'; import { loadBooruData } from './booru'; import { registerEvents } from './boorujs'; @@ -39,8 +36,6 @@ import { imageSourcesCreator } from './sources'; whenReady(() => { - showOwnedComments(); - showOwnedPosts(); loadBooruData(); listenAutocomplete(); registerEvents(); From 7a6015b5498930240fefef3409257888049431f6 Mon Sep 17 00:00:00 2001 From: Luna D Date: Thu, 6 Jun 2024 20:42:04 -0400 Subject: [PATCH 03/44] Convert duplicate report comparison scripts to TypeScript --- assets/js/duplicate_reports.js | 42 ---------------------------------- assets/js/duplicate_reports.ts | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 42 deletions(-) delete mode 100644 assets/js/duplicate_reports.js create mode 100644 assets/js/duplicate_reports.ts diff --git a/assets/js/duplicate_reports.js b/assets/js/duplicate_reports.js deleted file mode 100644 index a6794ec4..00000000 --- a/assets/js/duplicate_reports.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Interactive behavior for duplicate reports. - */ - -import { $, $$ } from './utils/dom'; - -function setupDupeReports() { - const [ onion, slider ] = $$('.onion-skin__image, .onion-skin__slider'); - const swipe = $('.swipe__image'); - - if (swipe) setupSwipe(swipe); - if (onion) setupOnionSkin(onion, slider); -} - -function setupSwipe(swipe) { - const [ clip, divider ] = $$('#clip rect, #divider', swipe); - const { width } = swipe.viewBox.baseVal; - - function moveDivider({ clientX }) { - // Move center to cursor - const rect = swipe.getBoundingClientRect(); - const newX = (clientX - rect.left) * (width / rect.width); - - divider.setAttribute('x', newX); - clip.setAttribute('width', newX); - } - - swipe.addEventListener('mousemove', moveDivider); -} - -function setupOnionSkin(onion, slider) { - const target = $('#target', onion); - - function setOpacity() { - target.setAttribute('opacity', slider.value); - } - - setOpacity(); - slider.addEventListener('input', setOpacity); -} - -export { setupDupeReports }; diff --git a/assets/js/duplicate_reports.ts b/assets/js/duplicate_reports.ts new file mode 100644 index 00000000..55cdfeb1 --- /dev/null +++ b/assets/js/duplicate_reports.ts @@ -0,0 +1,42 @@ +/** + * Interactive behavior for duplicate reports. + */ + +import { assertNotNull } from './utils/assert'; +import { $, $$ } from './utils/dom'; + +export function setupDupeReports() { + const onion = $('.onion-skin__image'); + const slider = $('.onion-skin__slider'); + const swipe = $('.swipe__image'); + + if (swipe) setupSwipe(swipe); + if (onion && slider) setupOnionSkin(onion, slider); +} + +function setupSwipe(swipe: SVGSVGElement) { + const [ clip, divider ] = $$('#clip rect, #divider', swipe); + const { width } = swipe.viewBox.baseVal; + + function moveDivider({ clientX }: MouseEvent) { + // Move center to cursor + const rect = swipe.getBoundingClientRect(); + const newX = (clientX - rect.left) * (width / rect.width); + + divider.setAttribute('x', newX.toString()); + clip.setAttribute('width', newX.toString()); + } + + swipe.addEventListener('mousemove', moveDivider); +} + +function setupOnionSkin(onion: SVGSVGElement, slider: HTMLInputElement) { + const target = assertNotNull($('#target', onion)); + + function setOpacity() { + target.setAttribute('opacity', slider.value); + } + + setOpacity(); + slider.addEventListener('input', setOpacity); +} From 924334de4966df185cb9ee6ff3f24c41f86847cf Mon Sep 17 00:00:00 2001 From: Luna D Date: Thu, 6 Jun 2024 20:50:13 -0400 Subject: [PATCH 04/44] Convert keyboard shortcut scripts to TypeScript --- assets/js/shortcuts.js | 62 ----------------------------------- assets/js/shortcuts.ts | 74 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 62 deletions(-) delete mode 100644 assets/js/shortcuts.js create mode 100644 assets/js/shortcuts.ts diff --git a/assets/js/shortcuts.js b/assets/js/shortcuts.js deleted file mode 100644 index 67d1acbd..00000000 --- a/assets/js/shortcuts.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Keyboard shortcuts - */ - -import { $ } from './utils/dom'; - -function getHover() { - const thumbBoxHover = $('.media-box:hover'); - if (thumbBoxHover) return thumbBoxHover.dataset.imageId; -} - -function openFullView() { - const imageHover = $('[data-uris]:hover'); - if (!imageHover) return; - - window.location = JSON.parse(imageHover.dataset.uris).full; -} - -function openFullViewNewTab() { - const imageHover = $('[data-uris]:hover'); - if (!imageHover) return; - - window.open(JSON.parse(imageHover.dataset.uris).full); -} - -function click(selector) { - const el = $(selector); - if (el) el.click(); -} - -function isOK(event) { - return !event.altKey && !event.ctrlKey && !event.metaKey && - document.activeElement.tagName !== 'INPUT' && - document.activeElement.tagName !== 'TEXTAREA'; -} - -const keyCodes = { - 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 - getHover() ? click(`a.interaction--fave[data-image-id="${getHover()}"]`) - : click('.block__header a.interaction--fave'); - }, - 85() { // U - upvote image - getHover() ? click(`a.interaction--upvote[data-image-id="${getHover()}"]`) - : click('.block__header a.interaction--upvote'); - }, -}; - -function listenForKeys() { - document.addEventListener('keydown', event => { - if (isOK(event) && keyCodes[event.keyCode]) { keyCodes[event.keyCode](); event.preventDefault(); } - }); -} - -export { listenForKeys }; diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts new file mode 100644 index 00000000..a3254c16 --- /dev/null +++ b/assets/js/shortcuts.ts @@ -0,0 +1,74 @@ +/** + * Keyboard shortcuts + */ + +import { $ } from './utils/dom'; + +interface ShortcutKeycodes { + [key: string]: () => void +} + +function getHover(): string | null { + const thumbBoxHover = $('.media-box:hover'); + + return thumbBoxHover && (thumbBoxHover.dataset.imageId || null); +} + +function openFullView() { + const imageHover = $('[data-uris]:hover'); + + if (!imageHover || !imageHover.dataset.uris) return; + + window.location = JSON.parse(imageHover.dataset.uris).full; +} + +function openFullViewNewTab() { + const imageHover = $('[data-uris]:hover'); + + if (!imageHover || !imageHover.dataset.uris) return; + + window.open(JSON.parse(imageHover.dataset.uris).full); +} + +function click(selector: string) { + const el = $(selector); + + if (el) { + el.click(); + } +} + +function isOK(event: KeyboardEvent): boolean { + return !event.altKey && !event.ctrlKey && !event.metaKey && + document.activeElement !== null && + document.activeElement.tagName !== 'INPUT' && + document.activeElement.tagName !== 'TEXTAREA'; +} + +const keyCodes: ShortcutKeycodes = { + KeyJ() { click('.js-prev'); }, // J - go to previous image + KeyI() { click('.js-up'); }, // I - go to index page + KeyK() { click('.js-next'); }, // K - go to next image + KeyR() { click('.js-rand'); }, // R - go to random image + KeyS() { click('.js-source-link'); }, // S - go to image source + KeyL() { click('.js-tag-sauce-toggle'); }, // L - edit tags + KeyO() { openFullView(); }, // O - open original + KeyV() { openFullViewNewTab(); }, // V - open original in a new tab + KeyF() { // F - favourite image + getHover() ? click(`a.interaction--fave[data-image-id="${getHover()}"]`) + : click('.block__header a.interaction--fave'); + }, + KeyU() { // U - upvote image + getHover() ? click(`a.interaction--upvote[data-image-id="${getHover()}"]`) + : click('.block__header a.interaction--upvote'); + }, +}; + +export function listenForKeys() { + document.addEventListener('keydown', (event: KeyboardEvent) => { + if (isOK(event) && keyCodes[event.code]) { + keyCodes[event.code](); + event.preventDefault(); + } + }); +} From 0bdbc2899e0860d86f305000ab0e8e571a467395 Mon Sep 17 00:00:00 2001 From: Luna D Date: Sat, 8 Jun 2024 11:53:44 -0400 Subject: [PATCH 05/44] Convert miscellaneous scripts to TypeScript --- assets/js/{app.js => app.ts} | 0 assets/js/{captcha.js => captcha.ts} | 0 assets/js/{pmwarning.js => pmwarning.ts} | 14 ++++++-------- assets/js/{poll.js => poll.ts} | 4 +--- assets/js/{settings.js => settings.ts} | 9 +++++---- assets/js/{when-ready.js => when-ready.ts} | 0 6 files changed, 12 insertions(+), 15 deletions(-) rename assets/js/{app.js => app.ts} (100%) rename assets/js/{captcha.js => captcha.ts} (100%) rename assets/js/{pmwarning.js => pmwarning.ts} (57%) rename assets/js/{poll.js => poll.ts} (81%) rename assets/js/{settings.js => settings.ts} (51%) rename assets/js/{when-ready.js => when-ready.ts} (100%) diff --git a/assets/js/app.js b/assets/js/app.ts similarity index 100% rename from assets/js/app.js rename to assets/js/app.ts diff --git a/assets/js/captcha.js b/assets/js/captcha.ts similarity index 100% rename from assets/js/captcha.js rename to assets/js/captcha.ts diff --git a/assets/js/pmwarning.js b/assets/js/pmwarning.ts similarity index 57% rename from assets/js/pmwarning.js rename to assets/js/pmwarning.ts index 104bd09c..23772dff 100644 --- a/assets/js/pmwarning.js +++ b/assets/js/pmwarning.ts @@ -4,11 +4,11 @@ * Warn users that their PM will be reviewed. */ -import { $ } from './utils/dom'; +import { $, hideEl, showEl } from './utils/dom'; -function warnAboutPMs() { - const textarea = $('.js-toolbar-input'); - const warning = $('.js-hidden-warning'); +export function warnAboutPMs() { + const textarea = $('.js-toolbar-input'); + const warning = $('.js-hidden-warning'); const imageEmbedRegex = /!+\[/g; if (!warning || !textarea) return; @@ -17,12 +17,10 @@ function warnAboutPMs() { const value = textarea.value; if (value.match(imageEmbedRegex)) { - warning.classList.remove('hidden'); + showEl(warning); } else if (!warning.classList.contains('hidden')) { - warning.classList.add('hidden'); + hideEl(warning); } }); } - -export { warnAboutPMs }; diff --git a/assets/js/poll.js b/assets/js/poll.ts similarity index 81% rename from assets/js/poll.js rename to assets/js/poll.ts index 68debf5d..38c74907 100644 --- a/assets/js/poll.js +++ b/assets/js/poll.ts @@ -1,6 +1,6 @@ import { inputDuplicatorCreator } from './input-duplicator'; -function pollOptionCreator() { +export function pollOptionCreator() { inputDuplicatorCreator({ addButtonSelector: '.js-poll-add-option', fieldSelector: '.js-poll-option', @@ -8,5 +8,3 @@ function pollOptionCreator() { removeButtonSelector: '.js-option-remove', }); } - -export { pollOptionCreator }; diff --git a/assets/js/settings.js b/assets/js/settings.ts similarity index 51% rename from assets/js/settings.js rename to assets/js/settings.ts index 360228ee..1faf28d0 100644 --- a/assets/js/settings.js +++ b/assets/js/settings.ts @@ -2,6 +2,7 @@ * Settings. */ +import { assertNotNull, assertNotUndefined } from './utils/assert'; import { $, $$ } from './utils/dom'; import store from './utils/store'; @@ -9,9 +10,9 @@ export function setupSettings() { if (!$('#js-setting-table')) return; - const localCheckboxes = $$('[data-tab="local"] input[type="checkbox"]'); - const themeSelect = $('#user_theme'); - const styleSheet = $('head link[rel="stylesheet"]'); + const localCheckboxes = $$('[data-tab="local"] input[type="checkbox"]'); + const themeSelect = assertNotNull($('#user_theme')); + const styleSheet = assertNotNull($('head link[rel="stylesheet"]')); // Local settings localCheckboxes.forEach(checkbox => { @@ -22,7 +23,7 @@ export function setupSettings() { // Theme preview themeSelect && themeSelect.addEventListener('change', () => { - styleSheet.href = themeSelect.options[themeSelect.selectedIndex].dataset.themePath; + styleSheet.href = assertNotUndefined(themeSelect.options[themeSelect.selectedIndex].dataset.themePath); }); } diff --git a/assets/js/when-ready.js b/assets/js/when-ready.ts similarity index 100% rename from assets/js/when-ready.js rename to assets/js/when-ready.ts From b15a17f6faa9e5145a864348c090374c92599fc4 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Jun 2024 10:06:41 -0400 Subject: [PATCH 06/44] Fix vite build --- assets/test/vitest-setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/test/vitest-setup.ts b/assets/test/vitest-setup.ts index d889b27f..a1f3c626 100644 --- a/assets/test/vitest-setup.ts +++ b/assets/test/vitest-setup.ts @@ -10,6 +10,7 @@ window.booru = { csrfToken: 'mockCsrfToken', hiddenTag: '/mock-tagblocked.svg', hiddenTagList: [], + hideStaffTools: 'true', ignoredTagList: [], imagesWithDownvotingDisabled: [], spoilerType: 'off', From 42499ae2b0813abe421e68f4632260dcd1c9d742 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 6 Jun 2024 15:31:29 -0400 Subject: [PATCH 07/44] Add netmask support to ip_profile tag and source changes --- lib/philomena_query/ip_mask.ex | 109 ++++++++++++++++++ .../ip_profile/source_change_controller.ex | 8 +- .../ip_profile/tag_change_controller.ex | 6 +- .../templates/ip_profile/show.html.slime | 13 ++- lib/philomena_web/views/ip_profile_view.ex | 5 + 5 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 lib/philomena_query/ip_mask.ex diff --git a/lib/philomena_query/ip_mask.ex b/lib/philomena_query/ip_mask.ex new file mode 100644 index 00000000..6bf7c192 --- /dev/null +++ b/lib/philomena_query/ip_mask.ex @@ -0,0 +1,109 @@ +defmodule PhilomenaQuery.IpMask do + @moduledoc """ + Postgres IP masks. + """ + + @doc """ + Parse a netmask from a string parameter, producing an `m:Postgrex.INET` type suitable for use in + a containment (<<=, <<, >>, >>=) query. Ignores invalid strings and passes the IP through on + error. [Postgres documentation](https://www.postgresql.org/docs/current/functions-net.html) + has more information on `inet` operations. + + > #### Info {: .info} + > + > Netmasks lower than /8 are clamped to a minimum of /8. Such low masks are unlikely to be + > useful and this avoids producing very expensive masks to evaluate. + + ## Examples + + iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "12"}) + %Postgrex.INET{address: {192, 160, 0, 0}, netmask: 12} + + iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "4"}) + %Postgrex.INET{address: {192, 0, 0, 0}, netmask: 8} + + iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "64"}) + %Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32} + + iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "e"}) + %Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32} + + iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{}) + %Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32} + + iex> parse_mask(%Postgrex.INET{ + ...> address: {0x2001, 0xab0, 0x33a8, 0xd6e2, 0x10e9, 0xac1b, 0x9b0f, 0x67bc}, + ...> netmask: 128 + ...> }, %{"mask" => "64"}) + %Postgrex.INET{address: {8193, 2736, 13224, 55010, 0, 0, 0, 0}, netmask: 64} + + """ + @spec parse_mask(Postgrex.INET.t(), map()) :: Postgrex.INET.t() + def parse_mask(ip, params) + + def parse_mask(ip, %{"mask" => mask}) when is_binary(mask) do + case Integer.parse(mask) do + {mask, _rest} -> + mask = clamp_mask(ip.address, mask) + address = apply_mask(ip.address, mask) + + %Postgrex.INET{address: address, netmask: mask} + + _ -> + ip + end + end + + def parse_mask(ip, _params), do: ip + + defp clamp(n, min, _max) when n < min, do: min + defp clamp(n, _min, max) when n > max, do: max + defp clamp(n, _min, _max), do: n + + defp clamp_mask(ip, mask) do + # Clamp mask length: + # - low end 8 (too taxing to evaluate) + # - high end address_bits (limit of address) + case tuple_size(ip) do + 4 -> + clamp(mask, 8, 32) + + 8 -> + clamp(mask, 8, 128) + end + end + + defp unit_length(ip) when tuple_size(ip) == 4, do: 8 + defp unit_length(ip) when tuple_size(ip) == 8, do: 16 + + defp apply_mask(ip, mask) when is_tuple(ip) do + # Determine whether elements are octets or hexadectets + length = unit_length(ip) + + # 1. Convert tuple to list of octets/hexadectets + # 2. Convert list to bitstring + # 3. Perform truncation operation on bitstring + # 4. Convert bitstring back to list of octets/hexadectets + # 5. Convert list to tuple + + ip + |> Tuple.to_list() + |> list_to_bits(length) + |> apply_mask(mask) + |> bits_to_list(length) + |> List.to_tuple() + end + + defp apply_mask(ip, mask) when is_binary(ip) do + # Truncate bit size of ip to mask length and zero-fill the remainder + <> + end + + defp list_to_bits(list, unit_length) do + for u <- list, into: <<>>, do: <> + end + + defp bits_to_list(bits, unit_length) do + for <>, do: u + end +end diff --git a/lib/philomena_web/controllers/ip_profile/source_change_controller.ex b/lib/philomena_web/controllers/ip_profile/source_change_controller.ex index f5bf868c..d82359e2 100644 --- a/lib/philomena_web/controllers/ip_profile/source_change_controller.ex +++ b/lib/philomena_web/controllers/ip_profile/source_change_controller.ex @@ -1,25 +1,27 @@ defmodule PhilomenaWeb.IpProfile.SourceChangeController do use PhilomenaWeb, :controller + alias PhilomenaQuery.IpMask alias Philomena.SourceChanges.SourceChange alias Philomena.Repo import Ecto.Query plug :verify_authorized - def index(conn, %{"ip_profile_id" => ip}) do + def index(conn, %{"ip_profile_id" => ip} = params) do {:ok, ip} = EctoNetwork.INET.cast(ip) + range = IpMask.parse_mask(ip, params) source_changes = SourceChange - |> where(ip: ^ip) + |> where(fragment("? >>= ip", ^range)) |> order_by(desc: :id) |> preload([:user, image: [:user, :sources, tags: :aliases]]) |> Repo.paginate(conn.assigns.scrivener) render(conn, "index.html", title: "Source Changes for IP `#{ip}'", - ip: ip, + ip: range, source_changes: source_changes ) end diff --git a/lib/philomena_web/controllers/ip_profile/tag_change_controller.ex b/lib/philomena_web/controllers/ip_profile/tag_change_controller.ex index b9779913..bdfebc29 100644 --- a/lib/philomena_web/controllers/ip_profile/tag_change_controller.ex +++ b/lib/philomena_web/controllers/ip_profile/tag_change_controller.ex @@ -1,6 +1,7 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do use PhilomenaWeb, :controller + alias PhilomenaQuery.IpMask alias Philomena.TagChanges.TagChange alias Philomena.Repo import Ecto.Query @@ -9,10 +10,11 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do def index(conn, %{"ip_profile_id" => ip} = params) do {:ok, ip} = EctoNetwork.INET.cast(ip) + range = IpMask.parse_mask(ip, params) tag_changes = TagChange - |> where(ip: ^ip) + |> where(fragment("? >>= ip", ^range)) |> added_filter(params) |> preload([:tag, :user, image: [:user, :sources, tags: :aliases]]) |> order_by(desc: :id) @@ -20,7 +22,7 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do render(conn, "index.html", title: "Tag Changes for IP `#{ip}'", - ip: ip, + ip: range, tag_changes: tag_changes ) end diff --git a/lib/philomena_web/templates/ip_profile/show.html.slime b/lib/philomena_web/templates/ip_profile/show.html.slime index eb3ac25c..da8acd3d 100644 --- a/lib/philomena_web/templates/ip_profile/show.html.slime +++ b/lib/philomena_web/templates/ip_profile/show.html.slime @@ -11,8 +11,17 @@ ul h2 Administration Options ul - li = link "View tag changes", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes" - li = link "View source URL history", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes" + li + => link "View tag changes", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes" + = if ipv6?(@ip) do + ' … + = link "(/64)", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes?mask=64" + li + => link "View source URL history", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes" + = if ipv6?(@ip) do + ' … + = link "(/64)", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes?mask=64" + li = link "View reports this IP has made", to: ~p"/admin/reports?#{[rq: "ip:#{@ip}"]}" li = link "View IP ban history", to: ~p"/admin/subnet_bans?#{[ip: to_string(@ip)]}" li = link "Ban this sucker", to: ~p"/admin/subnet_bans/new?#{[specification: to_string(@ip)]}" diff --git a/lib/philomena_web/views/ip_profile_view.ex b/lib/philomena_web/views/ip_profile_view.ex index 9aef6c29..a9f99f20 100644 --- a/lib/philomena_web/views/ip_profile_view.ex +++ b/lib/philomena_web/views/ip_profile_view.ex @@ -1,3 +1,8 @@ defmodule PhilomenaWeb.IpProfileView do use PhilomenaWeb, :view + + @spec ipv6?(Postgrex.INET.t()) :: boolean() + def ipv6?(ip) do + tuple_size(ip.address) == 8 + end end From 363e27f063674c327e58f129eaea244b4c89d0c3 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Jun 2024 12:40:26 -0400 Subject: [PATCH 08/44] mix format --- lib/philomena_query/ip_mask.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/philomena_query/ip_mask.ex b/lib/philomena_query/ip_mask.ex index 6bf7c192..bbcd160b 100644 --- a/lib/philomena_query/ip_mask.ex +++ b/lib/philomena_query/ip_mask.ex @@ -96,7 +96,7 @@ defmodule PhilomenaQuery.IpMask do defp apply_mask(ip, mask) when is_binary(ip) do # Truncate bit size of ip to mask length and zero-fill the remainder - <> + <> end defp list_to_bits(list, unit_length) do From 1eed44aa9502af47d2807acbb09eaeade9f573f5 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Jun 2024 12:40:44 -0400 Subject: [PATCH 09/44] Fix scraper error with invalid hostname --- lib/philomena_proxy/scrapers.ex | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/philomena_proxy/scrapers.ex b/lib/philomena_proxy/scrapers.ex index 9a166887..67711045 100644 --- a/lib/philomena_proxy/scrapers.ex +++ b/lib/philomena_proxy/scrapers.ex @@ -56,16 +56,17 @@ defmodule PhilomenaProxy.Scrapers do def scrape!(url) do uri = URI.parse(url) - @scrapers - |> Enum.find(& &1.can_handle?(uri, url)) - |> wrap() - |> Enum.map(& &1.scrape(uri, url)) - |> unwrap() + cond do + is_nil(uri.host) -> + # Scraping without a hostname doesn't make sense because the proxy cannot fetch it, and + # some scrapers may test properties of the hostname. + nil + + true -> + # Find the first scraper which can handle the URL and process, or return nil + Enum.find_value(@scrapers, nil, fn scraper -> + scraper.can_handle?(uri, url) && scraper.scrape(uri, url) + end) + end end - - defp wrap(nil), do: [] - defp wrap(res), do: [res] - - defp unwrap([result]), do: result - defp unwrap(_result), do: nil end From 0b0412786fde9ea030b7cd7ac6f920b04626fcd9 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Jun 2024 16:30:21 -0400 Subject: [PATCH 10/44] Fix mismerge --- assets/js/captcha.ts | 13 +++++++------ assets/vite.config.ts | 2 +- lib/philomena_web/templates/layout/app.html.slime | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/assets/js/captcha.ts b/assets/js/captcha.ts index ec0b4f32..44d0d9b6 100644 --- a/assets/js/captcha.ts +++ b/assets/js/captcha.ts @@ -1,18 +1,19 @@ +import { assertNotNull } from './utils/assert'; import { delegate, leftClick } from './utils/events'; import { clearEl, makeEl } from './utils/dom'; -function insertCaptcha(_event, target) { - const { parentNode, dataset: { sitekey } } = target; +function insertCaptcha(_event: Event, target: HTMLInputElement) { + const parentElement = assertNotNull(target.parentElement); const script = makeEl('script', {src: 'https://hcaptcha.com/1/api.js', async: true, defer: true}); const frame = makeEl('div', {className: 'h-captcha'}); - frame.dataset.sitekey = sitekey; + frame.dataset.sitekey = target.dataset.sitekey; - clearEl(parentNode); + clearEl(parentElement); - parentNode.insertAdjacentElement('beforeend', frame); - parentNode.insertAdjacentElement('beforeend', script); + parentElement.insertAdjacentElement('beforeend', frame); + parentElement.insertAdjacentElement('beforeend', script); } export function bindCaptchaLinks() { diff --git a/assets/vite.config.ts b/assets/vite.config.ts index 5b2a49c0..3e1d3cd9 100644 --- a/assets/vite.config.ts +++ b/assets/vite.config.ts @@ -39,7 +39,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { cssCodeSplit: true, rollupOptions: { input: { - 'js/app': './js/app.js', + 'js/app': './js/app.ts', ...Object.fromEntries(themes) }, output: { diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime index 227432e7..cfa82d19 100644 --- a/lib/philomena_web/templates/layout/app.html.slime +++ b/lib/philomena_web/templates/layout/app.html.slime @@ -23,7 +23,7 @@ html lang="en" = vite_hmr? do script type="module" src="http://localhost:5173/@vite/client" - script type="module" src="http://localhost:5173/js/app.js" + script type="module" src="http://localhost:5173/js/app.ts" - else script type="text/javascript" src=~p"/js/app.js" async="async" = render PhilomenaWeb.LayoutView, "_opengraph.html", assigns From 9ea6fdfb39facdb9b02f3359367873e4ba9d521d Mon Sep 17 00:00:00 2001 From: Luna D Date: Sat, 8 Jun 2024 11:47:09 -0400 Subject: [PATCH 11/44] Convert tagsmisc and sources scripts to TypeScript --- assets/js/{sources.js => sources.ts} | 12 +++-- assets/js/tagsmisc.js | 56 ---------------------- assets/js/tagsmisc.ts | 69 ++++++++++++++++++++++++++++ assets/types/ujs.ts | 11 +++++ 4 files changed, 87 insertions(+), 61 deletions(-) rename assets/js/{sources.js => sources.ts} (71%) delete mode 100644 assets/js/tagsmisc.js create mode 100644 assets/js/tagsmisc.ts create mode 100644 assets/types/ujs.ts diff --git a/assets/js/sources.js b/assets/js/sources.ts similarity index 71% rename from assets/js/sources.js rename to assets/js/sources.ts index bb4ae3ad..b4bc51ec 100644 --- a/assets/js/sources.js +++ b/assets/js/sources.ts @@ -1,4 +1,7 @@ +import { assertNotNull } from './utils/assert'; +import { $ } from './utils/dom'; import { inputDuplicatorCreator } from './input-duplicator'; +import '../types/ujs'; function setupInputs() { inputDuplicatorCreator({ @@ -9,12 +12,13 @@ function setupInputs() { }); } -function imageSourcesCreator() { +export function imageSourcesCreator() { setupInputs(); - document.addEventListener('fetchcomplete', ({ target, detail }) => { - const sourceSauce = document.querySelector('.js-sourcesauce'); + document.addEventListener('fetchcomplete', ({ target, detail }) => { if (target.matches('#source-form')) { + const sourceSauce = assertNotNull($('.js-sourcesauce')); + detail.text().then(text => { sourceSauce.outerHTML = text; setupInputs(); @@ -22,5 +26,3 @@ function imageSourcesCreator() { } }); } - -export { imageSourcesCreator }; diff --git a/assets/js/tagsmisc.js b/assets/js/tagsmisc.js deleted file mode 100644 index 29dc9dbe..00000000 --- a/assets/js/tagsmisc.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Tags Misc - */ - -import { $$ } from './utils/dom'; -import store from './utils/store'; -import { initTagDropdown } from './tags'; -import { setupTagsInput, reloadTagsInput } from './tagsinput'; - -function tagInputButtons({target}) { - const actions = { - save(tagInput) { - store.set('tag_input', tagInput.value); - }, - load(tagInput) { - // If entry 'tag_input' does not exist, try to use the current list - tagInput.value = store.get('tag_input') || tagInput.value; - reloadTagsInput(tagInput); - }, - clear(tagInput) { - tagInput.value = ''; - reloadTagsInput(tagInput); - }, - }; - - for (const action in actions) { - if (target.matches(`#tagsinput-${action}`)) actions[action](document.getElementById('image_tag_input')); - } -} - -function setupTags() { - $$('.js-tag-block').forEach(el => { - setupTagsInput(el); - el.classList.remove('js-tag-block'); - }); -} - -function updateTagSauce({target, detail}) { - const tagSauce = document.querySelector('.js-tagsauce'); - - if (target.matches('#tags-form')) { - detail.text().then(text => { - tagSauce.outerHTML = text; - setupTags(); - initTagDropdown(); - }); - } -} - -function setupTagEvents() { - setupTags(); - document.addEventListener('fetchcomplete', updateTagSauce); - document.addEventListener('click', tagInputButtons); -} - -export { setupTagEvents }; diff --git a/assets/js/tagsmisc.ts b/assets/js/tagsmisc.ts new file mode 100644 index 00000000..5ae6874a --- /dev/null +++ b/assets/js/tagsmisc.ts @@ -0,0 +1,69 @@ +/** + * Tags Misc + */ + +import { assertType, assertNotNull } from './utils/assert'; +import { $, $$ } from './utils/dom'; +import store from './utils/store'; +import { initTagDropdown } from './tags'; +import { setupTagsInput, reloadTagsInput } from './tagsinput'; +import '../types/ujs'; + +type TagInputActionFunction = (tagInput: HTMLTextAreaElement | null) => void; +type TagInputActionList = Record; + +function tagInputButtons(event: MouseEvent) { + const target = assertType(event.target, HTMLElement); + + const actions: TagInputActionList = { + save(tagInput: HTMLTextAreaElement | null) { + tagInput && store.set('tag_input', tagInput.value); + }, + load(tagInput: HTMLTextAreaElement | null) { + if (!tagInput) { return; } + + // If entry 'tag_input' does not exist, try to use the current list + tagInput.value = store.get('tag_input') || tagInput.value; + reloadTagsInput(tagInput); + }, + clear(tagInput: HTMLTextAreaElement | null) { + if (!tagInput) { return; } + + tagInput.value = ''; + reloadTagsInput(tagInput); + }, + }; + + for (const [ name, action ] of Object.entries(actions)) { + if (target && target.matches(`#tagsinput-${name}`)) { + action($('image_tag_input')); + } + } +} + +function setupTags() { + $$('.js-tag-block').forEach(el => { + setupTagsInput(el); + el.classList.remove('js-tag-block'); + }); +} + +function updateTagSauce({ target, detail }: FetchcompleteEvent) { + if (target.matches('#tags-form')) { + const tagSauce = assertNotNull($('.js-tagsauce')); + + detail.text().then(text => { + tagSauce.outerHTML = text; + setupTags(); + initTagDropdown(); + }); + } +} + +function setupTagEvents() { + setupTags(); + document.addEventListener('fetchcomplete', updateTagSauce); + document.addEventListener('click', tagInputButtons); +} + +export { setupTagEvents }; diff --git a/assets/types/ujs.ts b/assets/types/ujs.ts new file mode 100644 index 00000000..f9cb88f5 --- /dev/null +++ b/assets/types/ujs.ts @@ -0,0 +1,11 @@ +export {}; + +declare global { + interface FetchcompleteEvent extends CustomEvent { + target: HTMLElement, + } + + interface GlobalEventHandlersEventMap { + fetchcomplete: FetchcompleteEvent; + } +} From fe4698aedc9087c8fc779dc918f38a4f73f8285f Mon Sep 17 00:00:00 2001 From: Luna D Date: Sat, 8 Jun 2024 11:58:11 -0400 Subject: [PATCH 12/44] Convert gallery rearrangement scripts to TypeScript --- assets/js/{galleries.js => galleries.ts} | 21 +++++++++++++-------- assets/types/booru-object.d.ts | 4 ++++ 2 files changed, 17 insertions(+), 8 deletions(-) rename assets/js/{galleries.js => galleries.ts} (52%) diff --git a/assets/js/galleries.js b/assets/js/galleries.ts similarity index 52% rename from assets/js/galleries.js rename to assets/js/galleries.ts index 6cf97865..1556d8a0 100644 --- a/assets/js/galleries.js +++ b/assets/js/galleries.ts @@ -3,6 +3,7 @@ */ import { arraysEqual } from './utils/array'; +import { assertNotNull, assertNotUndefined } from './utils/assert'; import { $, $$ } from './utils/dom'; import { initDraggables } from './utils/draggable'; import { fetchJson } from './utils/requests'; @@ -10,17 +11,18 @@ import { fetchJson } from './utils/requests'; export function setupGalleryEditing() { if (!$('.rearrange-button')) return; - const [ rearrangeEl, saveEl ] = $$('.rearrange-button'); - const sortableEl = $('#sortable'); - const containerEl = $('.js-resizable-media-container'); + const [ rearrangeEl, saveEl ] = $$('.rearrange-button'); + const sortableEl = assertNotNull($('#sortable')); + const containerEl = assertNotNull($('.js-resizable-media-container')); // Copy array - let oldImages = window.booru.galleryImages.slice(); - let newImages = window.booru.galleryImages.slice(); + const galleryImages = assertNotUndefined(window.booru.galleryImages); + let oldImages = galleryImages.slice(); + let newImages = galleryImages.slice(); initDraggables(); - $$('.media-box', containerEl).forEach(i => i.draggable = true); + $$('.media-box', containerEl).forEach(i => i.draggable = true); rearrangeEl.addEventListener('click', () => { sortableEl.classList.add('editing'); @@ -31,12 +33,15 @@ export function setupGalleryEditing() { sortableEl.classList.remove('editing'); containerEl.classList.remove('drag-container'); - newImages = $$('.image-container', containerEl).map(i => parseInt(i.dataset.imageId, 10)); + newImages = $$('.image-container', containerEl) + .map(i => parseInt(assertNotUndefined(i.dataset.imageId), 10)); // If nothing changed, don't bother. if (arraysEqual(newImages, oldImages)) return; - fetchJson('PATCH', saveEl.dataset.reorderPath, { + const reorderPath = assertNotUndefined(saveEl.dataset.reorderPath); + + fetchJson('PATCH', reorderPath, { image_ids: newImages, // copy the array again so that we have the newly updated set diff --git a/assets/types/booru-object.d.ts b/assets/types/booru-object.d.ts index 2a5949c0..4154385c 100644 --- a/assets/types/booru-object.d.ts +++ b/assets/types/booru-object.d.ts @@ -69,6 +69,10 @@ interface BooruObject { * Indicates whether sensitive staff-only info should be hidden or not. */ hideStaffTools: string; + /** + * List of image IDs in the current gallery. + */ + galleryImages?: number[]; } declare global { From 922f2db583dc301642dabe0f139540b1535c82dc Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 10 Jun 2024 08:08:46 -0400 Subject: [PATCH 13/44] Fix selector --- assets/js/tagsmisc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/tagsmisc.ts b/assets/js/tagsmisc.ts index 5ae6874a..7e80dce5 100644 --- a/assets/js/tagsmisc.ts +++ b/assets/js/tagsmisc.ts @@ -36,7 +36,7 @@ function tagInputButtons(event: MouseEvent) { for (const [ name, action ] of Object.entries(actions)) { if (target && target.matches(`#tagsinput-${name}`)) { - action($('image_tag_input')); + action($('#image_tag_input')); } } } From 62a2f13bb6917faf42dfe72c0e9d322ab8fcf658 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Mon, 10 Jun 2024 17:30:51 +0300 Subject: [PATCH 14/44] feat: bump max source count to 15 --- lib/philomena/images.ex | 2 +- lib/philomena_web/templates/image/_source.html.slime | 2 +- lib/philomena_web/templates/image/new.html.slime | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index 95788ad5..d68595fb 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -937,7 +937,7 @@ defmodule Philomena.Images do (source.sources ++ target.sources) |> Enum.map(fn s -> %Source{image_id: target.id, source: s.source} end) |> Enum.uniq() - |> Enum.take(10) + |> Enum.take(15) target |> Image.sources_changeset(sources) diff --git a/lib/philomena_web/templates/image/_source.html.slime b/lib/philomena_web/templates/image/_source.html.slime index 8fa32872..48e3c6de 100644 --- a/lib/philomena_web/templates/image/_source.html.slime +++ b/lib/philomena_web/templates/image/_source.html.slime @@ -8,7 +8,7 @@ p 'The page(s) you found this image on. Images may have a maximum of - span.js-max-source-count> 10 + span.js-max-source-count> 15 ' source URLs. Leave any sources you don't want to use blank. = inputs_for f, :sources, [as: "image[old_sources]", skip_hidden: true], fn fs -> diff --git a/lib/philomena_web/templates/image/new.html.slime b/lib/philomena_web/templates/image/new.html.slime index ad596f17..dfb664d9 100644 --- a/lib/philomena_web/templates/image/new.html.slime +++ b/lib/philomena_web/templates/image/new.html.slime @@ -40,7 +40,7 @@ h4 About this image p 'The page(s) you found this image on. Images may have a maximum of - span.js-max-source-count> 10 + span.js-max-source-count> 15 ' source URLs. Leave any sources you don't want to use blank. = inputs_for f, :sources, fn fs -> From 567eaac69725144d4f5b8d1eac759b8ef1572298 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Mon, 10 Jun 2024 17:48:29 +0300 Subject: [PATCH 15/44] image: validate sources length --- lib/philomena/images/image.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index d8e791c8..7b808eaa 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -212,11 +212,13 @@ defmodule Philomena.Images.Image do image |> cast(attrs, []) |> SourceDiffer.diff_input(old_sources, new_sources) + |> validate_length(:sources, max: 15) end def sources_changeset(image, new_sources) do change(image) |> put_assoc(:sources, new_sources) + |> validate_length(:sources, max: 15) end def tag_changeset(image, attrs, old_tags, new_tags, excluded_tags \\ []) do From 6ffa24b4b86a921d9554aa23dde597cced955b24 Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Mon, 10 Jun 2024 21:17:06 +0200 Subject: [PATCH 16/44] fix vite on external devices --- assets/vite.config.ts | 4 ++++ lib/philomena_web/templates/layout/app.html.slime | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/assets/vite.config.ts b/assets/vite.config.ts index 3e1d3cd9..3ec68f35 100644 --- a/assets/vite.config.ts +++ b/assets/vite.config.ts @@ -24,6 +24,10 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { return { publicDir: 'static', plugins: [], + server: { + host: '0.0.0.0', + port: 5173, + }, resolve: { alias: { common: path.resolve(__dirname, 'css/common/'), diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime index cfa82d19..8fc29580 100644 --- a/lib/philomena_web/templates/layout/app.html.slime +++ b/lib/philomena_web/templates/layout/app.html.slime @@ -22,8 +22,8 @@ html lang="en" = csrf_meta_tag() = vite_hmr? do - script type="module" src="http://localhost:5173/@vite/client" - script type="module" src="http://localhost:5173/js/app.ts" + script type="module" src="http://#{@conn.host}:5173/@vite/client" + script type="module" src="http://#{@conn.host}:5173/js/app.ts" - else script type="text/javascript" src=~p"/js/app.js" async="async" = render PhilomenaWeb.LayoutView, "_opengraph.html", assigns From 4896857bc30171e58b7256d2933894f0bdecf14f Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 10 Jun 2024 20:37:34 -0400 Subject: [PATCH 17/44] Convert tag dropdown scripts to TypeScript --- assets/js/{tags.js => tags.ts} | 34 ++++++++++++------- .../templates/tag/_tag.html.slime | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) rename assets/js/{tags.js => tags.ts} (65%) diff --git a/assets/js/tags.js b/assets/js/tags.ts similarity index 65% rename from assets/js/tags.js rename to assets/js/tags.ts index 1a97c196..c3547f27 100644 --- a/assets/js/tags.js +++ b/assets/js/tags.ts @@ -2,23 +2,28 @@ * Tags Dropdown */ -import { showEl, hideEl } from './utils/dom'; +import { $$, showEl, hideEl } from './utils/dom'; +import { assertNotUndefined } from './utils/assert'; +import '../types/ujs'; -function addTag(tagId, list) { +type TagDropdownActionFunction = () => void; +type TagDropdownActionList = Record; + +function addTag(tagId: number, list: number[]) { list.push(tagId); } -function removeTag(tagId, list) { +function removeTag(tagId: number, list: number[]) { list.splice(list.indexOf(tagId), 1); } -function createTagDropdown(tag) { +function createTagDropdown(tag: HTMLSpanElement) { const { userIsSignedIn, userCanEditFilter, watchedTagList, spoileredTagList, hiddenTagList } = window.booru; - const [ unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter ] = [].slice.call(tag.querySelectorAll('.tag__dropdown__link')); - const [ unwatched, watched, spoilered, hidden ] = [].slice.call(tag.querySelectorAll('.tag__state')); - const tagId = parseInt(tag.dataset.tagId, 10); + const [ unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter ] = $$('.tag__dropdown__link', tag); + const [ unwatched, watched, spoilered, hidden ] = $$('.tag__state', tag); + const tagId = parseInt(assertNotUndefined(tag.dataset.tagId), 10); - const actions = { + const actions: TagDropdownActionList = { unwatch() { hideEl(unwatch, watched); showEl(watch, unwatched); removeTag(tagId, watchedTagList); }, watch() { hideEl(watch, unwatched); showEl(unwatch, watched); addTag(tagId, watchedTagList); }, @@ -51,11 +56,14 @@ function createTagDropdown(tag) { if (userIsSignedIn && !userCanEditFilter) showEl(filter); - tag.addEventListener('fetchcomplete', event => actions[event.target.dataset.tagAction]()); + tag.addEventListener('fetchcomplete', event => { + const act = assertNotUndefined(event.target.dataset.tagAction); + actions[act](); + }); } -function initTagDropdown() { - [].forEach.call(document.querySelectorAll('.tag.dropdown'), createTagDropdown); +export function initTagDropdown() { + for (const tagSpan of $$('.tag.dropdown')) { + createTagDropdown(tagSpan); + } } - -export { initTagDropdown }; diff --git a/lib/philomena_web/templates/tag/_tag.html.slime b/lib/philomena_web/templates/tag/_tag.html.slime index 0de59b7e..9fa32b92 100644 --- a/lib/philomena_web/templates/tag/_tag.html.slime +++ b/lib/philomena_web/templates/tag/_tag.html.slime @@ -1,5 +1,5 @@ span.tag.dropdown data-tag-category="#{@tag.category}" data-tag-id="#{@tag.id}" data-tag-name="#{@tag.name}" data-tag-slug="#{@tag.slug}" - / The order of tag states and dropdown links is important for tags.js + / The order of tag states and dropdown links is important for tags.ts span span.tag__state.hidden title="Unwatched" | + From 45df8a878e14d4ee5631cfd288ba4028600acd60 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 10 Jun 2024 20:41:24 -0400 Subject: [PATCH 18/44] Use Record type for shortcut map too --- assets/js/shortcuts.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts index a3254c16..d47da985 100644 --- a/assets/js/shortcuts.ts +++ b/assets/js/shortcuts.ts @@ -4,9 +4,7 @@ import { $ } from './utils/dom'; -interface ShortcutKeycodes { - [key: string]: () => void -} +type ShortcutKeyMap = Record void>; function getHover(): string | null { const thumbBoxHover = $('.media-box:hover'); @@ -45,7 +43,7 @@ function isOK(event: KeyboardEvent): boolean { document.activeElement.tagName !== 'TEXTAREA'; } -const keyCodes: ShortcutKeycodes = { +const keyCodes: ShortcutKeyMap = { KeyJ() { click('.js-prev'); }, // J - go to previous image KeyI() { click('.js-up'); }, // I - go to index page KeyK() { click('.js-next'); }, // K - go to next image From b748383ab14d6c1b4cd6b2f9274facb0b91ebfc4 Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Wed, 12 Jun 2024 23:06:41 +0200 Subject: [PATCH 19/44] npm update --- assets/package-lock.json | 1571 +++++++++++++++++++------------------- assets/package.json | 17 +- 2 files changed, 809 insertions(+), 779 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index bdd7c28a..ea901f29 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -6,34 +6,33 @@ "": { "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", - "@types/web": "^0.0.143", - "@typescript-eslint/eslint-plugin": "^7.8.0", - "@typescript-eslint/parser": "^7.8.0", + "@types/web": "^0.0.148", "autoprefixer": "^10.4.19", "cross-env": "^7.0.3", - "eslint": "^8.34.0", + "eslint": "^9.4.0", "jest-environment-jsdom": "^29.7.0", "normalize-scss": "^8.0.0", "sass": "^1.75.0", "typescript": "^5.4", + "typescript-eslint": "8.0.0-alpha.30", "vite": "^5.2" }, "devDependencies": { "@testing-library/dom": "^10.1.0", - "@testing-library/jest-dom": "^6.4.2", + "@testing-library/jest-dom": "^6.4.6", "@types/chai-dom": "^1.11.3", - "@vitest/coverage-v8": "^1.5.3", + "@vitest/coverage-v8": "^1.6.0", "chai": "^5", "eslint-plugin-vitest": "^0.5.4", - "jsdom": "^24.0.0", - "vitest": "^1.5.3", + "jsdom": "^24.1.0", + "vitest": "^1.6.0", "vitest-fetch-mock": "^0.2.2" } }, "node_modules/@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "node_modules/@ampproject/remapping": { @@ -50,11 +49,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -62,28 +61,28 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -157,9 +156,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -169,9 +168,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -181,13 +180,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -559,29 +558,10 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -589,32 +569,63 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@eslint/config-array": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.15.1.tgz", + "integrity": "sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ==", "dependencies": { - "brace-expansion": "^1.1.7" + "@eslint/object-schema": "^2.1.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz", + "integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fortawesome/fontawesome-free": { @@ -626,39 +637,6 @@ "node": ">=6" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -671,10 +649,17 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", @@ -823,9 +808,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.1.tgz", - "integrity": "sha512-P6Wg856Ou/DLpR+O0ZLneNmrv7QpqBg+hK4wE05ijbC/t349BRfMfx+UFj5Ha3fCFopIa6iSZlpdaB4agkWp2Q==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", "cpu": [ "arm" ], @@ -835,9 +820,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.1.tgz", - "integrity": "sha512-piwZDjuW2WiHr05djVdUkrG5JbjnGbtx8BXQchYCMfib/nhjzWoiScelZ+s5IJI7lecrwSxHCzW026MWBL+oJQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", "cpu": [ "arm64" ], @@ -847,9 +832,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.1.tgz", - "integrity": "sha512-LsZXXIsN5Q460cKDT4Y+bzoPDhBmO5DTr7wP80d+2EnYlxSgkwdPfE3hbE+Fk8dtya+8092N9srjBTJ0di8RIA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", "cpu": [ "arm64" ], @@ -859,9 +844,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.1.tgz", - "integrity": "sha512-S7TYNQpWXB9APkxu/SLmYHezWwCoZRA9QLgrDeml+SR2A1LLPD2DBUdUlvmCF7FUpRMKvbeeWky+iizQj65Etw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", "cpu": [ "x64" ], @@ -871,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.1.tgz", - "integrity": "sha512-Lq2JR5a5jsA5um2ZoLiXXEaOagnVyCpCW7xvlcqHC7y46tLwTEgUSTM3a2TfmmTMmdqv+jknUioWXlmxYxE9Yw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", "cpu": [ "arm" ], @@ -883,9 +868,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.1.tgz", - "integrity": "sha512-9BfzwyPNV0IizQoR+5HTNBGkh1KXE8BqU0DBkqMngmyFW7BfuIZyMjQ0s6igJEiPSBvT3ZcnIFohZ19OqjhDPg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", "cpu": [ "arm" ], @@ -895,9 +880,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.1.tgz", - "integrity": "sha512-e2uWaoxo/rtzA52OifrTSXTvJhAXb0XeRkz4CdHBK2KtxrFmuU/uNd544Ogkpu938BzEfvmWs8NZ8Axhw33FDw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", "cpu": [ "arm64" ], @@ -907,9 +892,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.1.tgz", - "integrity": "sha512-ekggix/Bc/d/60H1Mi4YeYb/7dbal1kEDZ6sIFVAE8pUSx7PiWeEh+NWbL7bGu0X68BBIkgF3ibRJe1oFTksQQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", "cpu": [ "arm64" ], @@ -919,9 +904,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.1.tgz", - "integrity": "sha512-UGV0dUo/xCv4pkr/C8KY7XLFwBNnvladt8q+VmdKrw/3RUd3rD0TptwjisvE2TTnnlENtuY4/PZuoOYRiGp8Gw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", "cpu": [ "ppc64" ], @@ -931,9 +916,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.1.tgz", - "integrity": "sha512-gEYmYYHaehdvX46mwXrU49vD6Euf1Bxhq9pPb82cbUU9UT2NV+RSckQ5tKWOnNXZixKsy8/cPGtiUWqzPuAcXQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", "cpu": [ "riscv64" ], @@ -943,9 +928,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.1.tgz", - "integrity": "sha512-xeae5pMAxHFp6yX5vajInG2toST5lsCTrckSRUFwNgzYqnUjNBcQyqk1bXUxX5yhjWFl2Mnz3F8vQjl+2FRIcw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", "cpu": [ "s390x" ], @@ -955,9 +940,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.1.tgz", - "integrity": "sha512-AsdnINQoDWfKpBzCPqQWxSPdAWzSgnYbrJYtn6W0H2E9It5bZss99PiLA8CgmDRfvKygt20UpZ3xkhFlIfX9zQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", "cpu": [ "x64" ], @@ -967,9 +952,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.1.tgz", - "integrity": "sha512-KoB4fyKXTR+wYENkIG3fFF+5G6N4GFvzYx8Jax8BR4vmddtuqSb5oQmYu2Uu067vT/Fod7gxeQYKupm8gAcMSQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", "cpu": [ "x64" ], @@ -979,9 +964,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.1.tgz", - "integrity": "sha512-J0d3NVNf7wBL9t4blCNat+d0PYqAx8wOoY+/9Q5cujnafbX7BmtYk3XvzkqLmFECaWvXGLuHmKj/wrILUinmQg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", "cpu": [ "arm64" ], @@ -991,9 +976,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.1.tgz", - "integrity": "sha512-xjgkWUwlq7IbgJSIxvl516FJ2iuC/7ttjsAxSPpC9kkI5iQQFHKyEN5BjbhvJ/IXIZ3yIBcW5QDlWAyrA+TFag==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", "cpu": [ "ia32" ], @@ -1003,9 +988,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.1.tgz", - "integrity": "sha512-0QbCkfk6cnnVKWqqlC0cUrrUMDMfu5ffvYMTUHf+qMN2uAb3MKP31LPcwiMXBNsvoFGs/kYdFOsuLmvppCopXA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", "cpu": [ "x64" ], @@ -1055,18 +1040,18 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", - "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", "dev": true, "dependencies": { - "@adobe/css-tools": "^4.3.2", + "@adobe/css-tools": "^4.4.0", "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "redent": "^3.0.0" }, "engines": { @@ -1133,9 +1118,9 @@ "dev": true }, "node_modules/@types/chai": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz", - "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==", + "version": "4.3.16", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", + "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", "dev": true }, "node_modules/@types/chai-dom": { @@ -1183,24 +1168,14 @@ "parse5": "^7.0.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "dependencies": { "undici-types": "~5.26.4" } }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1212,9 +1187,9 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" }, "node_modules/@types/web": { - "version": "0.0.143", - "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.143.tgz", - "integrity": "sha512-TazK16/OqeeqfQRB/Tv/NwzJagHbLi/w5g26FLbiFte/8LpPq6BuTyXHO/cpgwJpE6KGgFSNYb6Ap05Tz9XvCA==" + "version": "0.0.148", + "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.148.tgz", + "integrity": "sha512-HX2eARbn26tZuCOxZ25Ew6UUNhw8fgdGrOGcxX0/J6yTtlJm+nHlL9/h+2zgSzse13vlVe+c+W3LWqhnlAd5rg==" }, "node_modules/@types/yargs": { "version": "17.0.32", @@ -1230,32 +1205,30 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", - "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.30.tgz", + "integrity": "sha512-2CBUupdkfbE3eATph4QeZejvT+M+1bVur+zXlVx09WN31phap51ps/qemeclnCbGEz6kTgBDmScrr9XmmF8/Pg==", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/type-utils": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4", + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/type-utils": "8.0.0-alpha.30", + "@typescript-eslint/utils": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1263,76 +1236,26 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", - "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", - "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", - "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.0-alpha.30.tgz", + "integrity": "sha512-tAYgFmgXU1MlCK3nbblUvJlDSibBvxtAQXGrF3IG0KmnRza9FXILZifHWL0rrwacDn40K53K607Fk2QkMjiGgw==", "dependencies": { - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1341,49 +1264,33 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", - "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.30.tgz", + "integrity": "sha512-FGW/iPWGyPFamAVZ60oCAthMqQrqafUGebF8UKuq/ha+e9SVG6YhJoRzurlQXOVf8dHfOhJ0ADMXyFnMc53clg==", "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0" + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", - "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", - "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.30.tgz", + "integrity": "sha512-FrnhlCKEKZKRbpDviHkIU9tayIUGTOfa+SjvrRv6p/AJIUv6QT8oRboRjLH/cCuwUEbM0k5UtRWYug4albHUqQ==", "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30", + "@typescript-eslint/utils": "8.0.0-alpha.30", "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1395,31 +1302,119 @@ } } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", - "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", - "dependencies": { - "@typescript-eslint/types": "7.8.0", - "eslint-visitor-keys": "^3.4.3" - }, + "node_modules/@typescript-eslint/types": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.30.tgz", + "integrity": "sha512-4WzLlw27SO9pK9UFj/Hu7WGo8WveT0SEiIpFVsV2WwtQmLps6kouwtVCB8GJPZKJyurhZhcqCoQVQFmpv441Vg==", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.30.tgz", + "integrity": "sha512-WSXbc9ZcXI+7yC+6q95u77i8FXz6HOLsw3ST+vMUlFy1lFbXyFL/3e6HDKQCm2Clt0krnoCPiTGvIn+GkYPn4Q==", + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.30.tgz", + "integrity": "sha512-rfhqfLqFyXhHNDwMnHiVGxl/Z2q/3guQ1jLlGQ0hi9Rb7inmwz42crM+NnLPR+2vEnwyw1P/g7fnQgQ3qvFx4g==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.30.tgz", + "integrity": "sha512-XZuNurZxBqmr6ZIRIwWFq7j5RZd6ZlkId/HZEWyfciK+CWoyOxSF9Pv2VXH9Rlu2ZG2PfbhLz2Veszl4Pfn7yA==", + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/@vitest/coverage-v8": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.3.tgz", - "integrity": "sha512-DPyGSu/fPHOJuPxzFSQoT4N/Fu/2aJfZRtEpEp8GI7NHsXBGE94CQ+pbEGBUMFjatsHPDJw/+TAF9r4ens2CNw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", + "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -1440,17 +1435,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.5.3" + "vitest": "1.6.0" } }, "node_modules/@vitest/expect": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.3.tgz", - "integrity": "sha512-y+waPz31pOFr3rD7vWTbwiLe5+MgsMm40jTZbQE8p8/qXyBX3CQsIXRx9XK12IbY7q/t5a5aM/ckt33b4PxK2g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dev": true, "dependencies": { - "@vitest/spy": "1.5.3", - "@vitest/utils": "1.5.3", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "chai": "^4.3.10" }, "funding": { @@ -1497,9 +1492,9 @@ } }, "node_modules/@vitest/expect/node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -1508,6 +1503,15 @@ "node": ">=6" } }, + "node_modules/@vitest/expect/node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/@vitest/expect/node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -1518,12 +1522,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.3.tgz", - "integrity": "sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dev": true, "dependencies": { - "@vitest/utils": "1.5.3", + "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -1559,9 +1563,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.3.tgz", - "integrity": "sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -1605,9 +1609,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.3.tgz", - "integrity": "sha512-Llj7Jgs6lbnL55WoshJUUacdJfjU2honvGcAJBxhra5TPEzTJH8ZuhI3p/JwqqfnTr4PmP7nDmOXP53MS7GJlg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -1617,9 +1621,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.3.tgz", - "integrity": "sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -1643,6 +1647,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@vitest/utils/node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/@vitest/utils/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -1855,28 +1868,29 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "funding": [ { "type": "opencollective", @@ -1892,10 +1906,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -1922,9 +1936,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001614", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", - "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", + "version": "1.0.30001632", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", + "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", "funding": [ { "type": "opencollective", @@ -1941,13 +1955,13 @@ ] }, "node_modules/chai": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz", - "integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { "assertion-error": "^2.0.1", - "check-error": "^2.0.0", + "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" @@ -1956,15 +1970,6 @@ "node": ">=12" } }, - "node_modules/chai/node_modules/loupe": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", - "integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1981,9 +1986,9 @@ } }, "node_modules/check-error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", - "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "engines": { "node": ">= 16" @@ -2137,6 +2142,12 @@ "node": ">=18" } }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -2151,9 +2162,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -2172,9 +2183,9 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "node_modules/deep-eql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", - "integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "engines": { "node": ">=6" @@ -2222,17 +2233,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -2252,9 +2252,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.751", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", - "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" + "version": "1.4.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz", + "integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q==" }, "node_modules/entities": { "version": "4.5.0", @@ -2344,40 +2344,36 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.4.0.tgz", + "integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/config-array": "^0.15.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.4.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2391,7 +2387,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2421,19 +2417,74 @@ } } }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/scope-manager": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", + "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/visitor-keys": "7.13.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", + "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", + "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/visitor-keys": "7.13.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", - "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz", + "integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" + "@typescript-eslint/scope-manager": "7.13.0", + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/typescript-estree": "7.13.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2446,63 +2497,96 @@ "eslint": "^8.56.0" } }, + "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", + "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.13.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vitest/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.11.3", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2567,6 +2651,29 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2617,20 +2724,20 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2654,16 +2761,15 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { @@ -2699,7 +2805,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -2723,10 +2830,24 @@ "node": "*" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2753,35 +2874,12 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2868,6 +2966,15 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2888,9 +2995,9 @@ } }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -2928,6 +3035,8 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2936,7 +3045,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -2989,6 +3099,18 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3345,9 +3467,9 @@ } }, "node_modules/jsdom": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", - "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", "dev": true, "dependencies": { "cssstyle": "^4.0.1", @@ -3355,21 +3477,21 @@ "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", + "nwsapi": "^2.2.10", "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "rrweb-cssom": "^0.7.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.3", + "tough-cookie": "^4.1.4", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.16.0", + "ws": "^8.17.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -3461,9 +3583,9 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" @@ -3528,11 +3650,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -3558,6 +3680,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3568,29 +3702,26 @@ } }, "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/mlly": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", - "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", "dev": true, "dependencies": { "acorn": "^8.11.3", "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" } }, "node_modules/ms": { @@ -3688,19 +3819,62 @@ "resolved": "https://registry.npmjs.org/normalize-scss/-/normalize-scss-8.0.0.tgz", "integrity": "sha512-C6GXIxQ2LOYWrde27xWbONavmybobxp+V6TY8BiBJw5M+yMNEg2R0WjaeDtmP5JsunFYKvFOvgMAIC0/OxZuJQ==" }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nwsapi": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.9.tgz", - "integrity": "sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==" + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3779,6 +3953,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -3815,9 +3990,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3831,13 +4006,13 @@ } }, "node_modules/pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", + "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", "dev": true, "dependencies": { "confbox": "^0.1.7", - "mlly": "^1.6.1", + "mlly": "^1.7.0", "pathe": "^1.1.2" } }, @@ -4002,24 +4177,10 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.1.tgz", - "integrity": "sha512-0gG94inrUtg25sB2V/pApwiv1lUb0bQ25FPNuzO89Baa+B+c0ccaaBKM5zkZV/12pUUdH+lWCSm9wmHqyocuVQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "dependencies": { "@types/estree": "1.0.5" }, @@ -4031,29 +4192,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.1", - "@rollup/rollup-android-arm64": "4.17.1", - "@rollup/rollup-darwin-arm64": "4.17.1", - "@rollup/rollup-darwin-x64": "4.17.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.1", - "@rollup/rollup-linux-arm-musleabihf": "4.17.1", - "@rollup/rollup-linux-arm64-gnu": "4.17.1", - "@rollup/rollup-linux-arm64-musl": "4.17.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.1", - "@rollup/rollup-linux-riscv64-gnu": "4.17.1", - "@rollup/rollup-linux-s390x-gnu": "4.17.1", - "@rollup/rollup-linux-x64-gnu": "4.17.1", - "@rollup/rollup-linux-x64-musl": "4.17.1", - "@rollup/rollup-win32-arm64-msvc": "4.17.1", - "@rollup/rollup-win32-ia32-msvc": "4.17.1", - "@rollup/rollup-win32-x64-msvc": "4.17.1", + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", "fsevents": "~2.3.2" } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, "node_modules/run-parallel": { @@ -4084,9 +4245,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz", - "integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==", + "version": "1.77.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", + "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -4111,12 +4272,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -4124,22 +4282,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4165,6 +4307,18 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4232,6 +4386,18 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -4303,28 +4469,6 @@ "node": ">=8" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4430,17 +4574,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -4453,6 +4586,28 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.0-alpha.30.tgz", + "integrity": "sha512-/vGhBMsK1TpadQh1eQ02c5pyiPGmKR9cVzX5C9plZ+LC0HPLpWoJbbTVfQN7BkIK7tUxDt2BFr3pFL5hDDrx7g==", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.0.0-alpha.30", + "@typescript-eslint/parser": "8.0.0-alpha.30", + "@typescript-eslint/utils": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ufo": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", @@ -4473,9 +4628,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "funding": [ { "type": "opencollective", @@ -4491,8 +4646,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -4519,9 +4674,9 @@ } }, "node_modules/vite": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", - "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", + "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -4573,9 +4728,9 @@ } }, "node_modules/vite-node": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.3.tgz", - "integrity": "sha512-axFo00qiCpU/JLd8N1gu9iEYL3xTbMbMrbe5nDp9GL0nb6gurIdZLkkFogZXWnE8Oyy5kfSLwNVIcVsnhE7lgQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -4595,16 +4750,16 @@ } }, "node_modules/vitest": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.3.tgz", - "integrity": "sha512-2oM7nLXylw3mQlW6GXnRriw+7YvZFk/YNV8AxIC3Z3MfFbuziLGWP9GPxxu/7nRlXhqyxBikpamr+lEEj1sUEw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", "dev": true, "dependencies": { - "@vitest/expect": "1.5.3", - "@vitest/runner": "1.5.3", - "@vitest/snapshot": "1.5.3", - "@vitest/spy": "1.5.3", - "@vitest/utils": "1.5.3", + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -4618,7 +4773,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.5.3", + "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -4633,8 +4788,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.5.3", - "@vitest/ui": "1.5.3", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", "happy-dom": "*", "jsdom": "*" }, @@ -4714,9 +4869,9 @@ } }, "node_modules/vitest/node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -4725,114 +4880,13 @@ "node": ">=6" } }, - "node_modules/vitest/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "node_modules/vitest/node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/vitest/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/vitest/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "get-func-name": "^2.0.1" } }, "node_modules/vitest/node_modules/pathval": { @@ -4844,30 +4898,6 @@ "node": "*" } }, - "node_modules/vitest/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/vitest/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -4963,7 +4993,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/ws": { "version": "8.17.0", diff --git a/assets/package.json b/assets/package.json index eba4cbe7..6a5d31c3 100644 --- a/assets/package.json +++ b/assets/package.json @@ -2,7 +2,7 @@ "type": "module", "scripts": { "deploy": "cross-env NODE_ENV=production tsc && cross-env NODE_ENV=production vite build", - "lint": "eslint . --ext .js,.ts", + "lint": "eslint .", "test": "vitest run --coverage", "test:watch": "vitest watch --coverage", "dev": "vite", @@ -11,12 +11,11 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", - "@types/web": "^0.0.143", - "@typescript-eslint/eslint-plugin": "^7.8.0", - "@typescript-eslint/parser": "^7.8.0", + "@types/web": "^0.0.148", + "typescript-eslint": "8.0.0-alpha.30", "autoprefixer": "^10.4.19", "cross-env": "^7.0.3", - "eslint": "^8.34.0", + "eslint": "^9.4.0", "jest-environment-jsdom": "^29.7.0", "normalize-scss": "^8.0.0", "sass": "^1.75.0", @@ -25,13 +24,13 @@ }, "devDependencies": { "@testing-library/dom": "^10.1.0", - "@testing-library/jest-dom": "^6.4.2", + "@testing-library/jest-dom": "^6.4.6", "@types/chai-dom": "^1.11.3", - "@vitest/coverage-v8": "^1.5.3", + "@vitest/coverage-v8": "^1.6.0", "chai": "^5", "eslint-plugin-vitest": "^0.5.4", - "jsdom": "^24.0.0", - "vitest": "^1.5.3", + "jsdom": "^24.1.0", + "vitest": "^1.6.0", "vitest-fetch-mock": "^0.2.2" } } From 64492d86571d5e32a77196be923bf4b160aea60c Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Wed, 12 Jun 2024 22:56:58 +0200 Subject: [PATCH 20/44] eslint 9 --- assets/.browserslistrc | 2 - assets/.eslintignore | 2 - assets/.eslintrc.yml | 289 ----------------------------------- assets/eslint.config.js | 293 ++++++++++++++++++++++++++++++++++++ assets/js/booru.js | 2 +- assets/js/fingerprint.js | 2 +- assets/js/settings.ts | 9 +- assets/js/shortcuts.ts | 8 +- assets/js/tagsmisc.ts | 16 +- assets/js/utils/store.ts | 8 +- assets/test/vitest-setup.ts | 1 - 11 files changed, 314 insertions(+), 318 deletions(-) delete mode 100644 assets/.eslintignore delete mode 100644 assets/.eslintrc.yml create mode 100644 assets/eslint.config.js diff --git a/assets/.browserslistrc b/assets/.browserslistrc index 41e7f1c1..ee6762d0 100644 --- a/assets/.browserslistrc +++ b/assets/.browserslistrc @@ -2,8 +2,6 @@ last 2 Android versions last 2 Chrome versions last 2 ChromeAndroid versions last 2 Edge versions -last 1 Explorer version -last 1 ExplorerMobile versions last 2 Firefox versions last 2 FirefoxAndroid versions last 2 iOS versions diff --git a/assets/.eslintignore b/assets/.eslintignore deleted file mode 100644 index 53c47bd8..00000000 --- a/assets/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -js/vendor/* -vite.config.ts diff --git a/assets/.eslintrc.yml b/assets/.eslintrc.yml deleted file mode 100644 index f5983245..00000000 --- a/assets/.eslintrc.yml +++ /dev/null @@ -1,289 +0,0 @@ -env: - browser: true - es6: true - -parser: '@typescript-eslint/parser' - -parserOptions: - ecmaVersion: 6 - sourceType: module - -plugins: - - '@typescript-eslint' - - vitest - -globals: - ga: false - md5: false - Sortable: false - ActionCable: false - -extends: - - 'plugin:@typescript-eslint/recommended' - -rules: - accessor-pairs: 2 - array-bracket-spacing: 0 - array-callback-return: 2 - arrow-body-style: 0 - arrow-parens: [2, 'as-needed'] - arrow-spacing: 2 - block-scoped-var: 2 - block-spacing: 2 - brace-style: [2, 'stroustrup', {allowSingleLine: true}] - callback-return: 0 - camelcase: [2, {allow: ['camo_url', 'spoiler_image_uri','image_ids']}] - class-methods-use-this: 0 - comma-dangle: [2, 'only-multiline'] - comma-spacing: 2 - comma-style: 2 - complexity: 0 - computed-property-spacing: [2, 'never'] - consistent-return: 0 - consistent-this: [2, 'that'] - constructor-super: 2 - curly: [2, 'multi-line', 'consistent'] - default-case: 2 - dot-location: [2, 'property'] - dot-notation: [2, {allowKeywords: true}] - eol-last: 2 - eqeqeq: 2 - func-call-spacing: 0 - func-name-matching: 2 - func-names: 0 - func-style: 0 - generator-star-spacing: 2 - global-require: 2 - guard-for-in: 0 - handle-callback-err: 2 - id-blacklist: 0 - id-length: 0 - id-match: 2 - indent: [2, 2, {SwitchCase: 1, VariableDeclarator: {var: 2, let: 2, const: 3}}] - init-declarations: 0 - jsx-quotes: 0 - key-spacing: 0 - keyword-spacing: 2 - line-comment-position: 0 - linebreak-style: [2, 'unix'] - lines-around-comment: 0 - lines-around-directive: 2 - max-depth: 0 - max-len: 0 - max-lines: 0 - max-nested-callbacks: 0 - max-params: 0 - max-statements-per-line: 0 - max-statements: 0 - multiline-ternary: 0 - new-cap: 2 - new-parens: 2 - newline-after-var: 0 - newline-before-return: 0 - newline-per-chained-call: 0 - no-alert: 0 - no-array-constructor: 2 - no-caller: 2 - no-case-declarations: 2 - no-catch-shadow: 2 - no-class-assign: 2 - no-cond-assign: 2 - no-confusing-arrow: 2 - no-console: 0 - no-const-assign: 2 - no-constant-condition: 2 - no-control-regex: 2 - no-debugger: 2 - no-delete-var: 2 - no-div-regex: 2 - no-dupe-args: 2 - no-dupe-class-members: 2 - no-dupe-keys: 2 - no-duplicate-case: 2 - no-duplicate-imports: 2 - no-else-return: 2 - no-empty-character-class: 2 - no-empty-function: 0 - no-empty-pattern: 2 - no-empty: 2 - no-eq-null: 2 - no-eval: 2 - no-ex-assign: 2 - no-extend-native: 2 - no-extra-bind: 2 - no-extra-boolean-cast: 2 - no-extra-label: 2 - no-extra-parens: [2, 'all', {nestedBinaryExpressions: false}] - no-extra-semi: 2 - no-fallthrough: 2 - no-floating-decimal: 2 - no-func-assign: 2 - no-global-assign: 2 - no-implicit-coercion: 2 - no-implicit-globals: 2 - no-implied-eval: 2 - no-inline-comments: 0 - no-inner-declarations: [2, 'both'] - no-invalid-regexp: 2 - no-invalid-this: 2 - no-irregular-whitespace: 2 - no-iterator: 2 - no-label-var: 2 - no-labels: 2 - no-lone-blocks: 2 - no-lonely-if: 0 - no-loop-func: 2 - no-magic-numbers: 0 - no-mixed-operators: 0 - no-mixed-requires: 0 - no-mixed-spaces-and-tabs: 2 - no-multi-spaces: 0 - no-multi-str: 2 - no-multiple-empty-lines: [2, {max: 3, maxBOF: 0, maxEOF: 1}] - no-native-reassign: 2 - no-negated-condition: 0 - no-negated-in-lhs: 2 - no-nested-ternary: 2 - no-new-func: 2 - no-new-object: 2 - no-new-require: 2 - no-new-symbol: 2 - no-new-wrappers: 2 - no-new: 2 - no-obj-calls: 2 - no-octal-escape: 2 - no-octal: 2 - no-param-reassign: 2 - no-path-concat: 2 - no-plusplus: 0 - no-process-env: 2 - no-process-exit: 2 - no-proto: 2 - no-prototype-builtins: 0 - no-redeclare: 2 - no-regex-spaces: 2 - no-restricted-globals: [2, 'event'] - no-restricted-imports: 2 - no-restricted-modules: 2 - no-restricted-properties: 0 - no-restricted-syntax: 2 - no-return-assign: 0 - no-script-url: 2 - no-self-assign: 2 - no-self-compare: 2 - no-sequences: 2 - no-shadow-restricted-names: 2 - no-shadow: 2 - no-spaced-func: 2 - no-sparse-arrays: 2 - no-sync: 0 - no-tabs: 2 - no-template-curly-in-string: 2 - no-ternary: 0 - no-this-before-super: 2 - no-throw-literal: 2 - no-trailing-spaces: 2 - no-undef-init: 2 - no-undef: 2 - no-undefined: 2 - no-underscore-dangle: 0 - no-unexpected-multiline: 2 - no-unmodified-loop-condition: 2 - no-unneeded-ternary: 2 - no-unreachable: 2 - no-unsafe-finally: 2 - no-unsafe-negation: 2 - no-unused-expressions: [2, {allowShortCircuit: true, allowTernary: true}] - no-unused-labels: 2 - no-unused-vars: [2, {vars: 'all', args: 'after-used'}] - no-use-before-define: [2, 'nofunc'] - no-useless-call: 2 - no-useless-computed-key: 2 - no-useless-concat: 2 - no-useless-constructor: 2 - no-useless-escape: 2 - no-useless-rename: 2 - no-var: 2 - no-void: 2 - no-warning-comments: 0 - no-whitespace-before-property: 2 - no-with: 2 - object-curly-newline: 0 - object-curly-spacing: 0 - object-property-newline: 0 - object-shorthand: 2 - one-var-declaration-per-line: 0 - one-var: 0 - operator-assignment: [2, 'always'] - operator-linebreak: 0 - padded-blocks: 0 - prefer-arrow-callback: 2 - prefer-const: 2 - prefer-numeric-literals: 2 - prefer-reflect: 0 - prefer-rest-params: 2 - prefer-spread: 0 - prefer-template: 2 - quote-props: [2, 'as-needed'] - quotes: [2, 'single'] - radix: 2 - require-jsdoc: 0 - require-yield: 2 - rest-spread-spacing: 2 - semi-spacing: [2, {before: false, after: true}] - semi: 2 - sort-imports: 0 - sort-keys: 0 - sort-vars: 0 - space-before-blocks: [2, 'always'] - space-before-function-paren: [2, 'never'] - space-in-parens: [2, 'never'] - space-infix-ops: 2 - space-unary-ops: [2, {words: true, nonwords: false}] - spaced-comment: 0 - strict: [2, 'function'] - symbol-description: 2 - template-curly-spacing: [2, 'never'] - unicode-bom: 2 - use-isnan: 2 - valid-jsdoc: 0 - valid-typeof: 2 - vars-on-top: 2 - wrap-iife: 2 - wrap-regex: 0 - yield-star-spacing: 2 - yoda: [2, 'never'] - -overrides: - # JavaScript Files - # Disable rules which are impossible to satisfy (types require .ts extension) - - files: - - '*.js' - rules: - '@typescript-eslint/explicit-module-boundary-types': 0 - # TypeScript Files - # Some ESLint rules report false errors due to lacking type information. Replacement rules are provided for these, and the originals need to be disabled - - files: - - '*.ts' - rules: - # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors - no-undef: 0 - no-unused-vars: 0 - '@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'after-used'}] - no-redeclare: 0 - '@typescript-eslint/no-redeclare': 2 - no-extra-parens: 0 - '@typescript-eslint/no-extra-parens': 2 - no-shadow: 0 - '@typescript-eslint/no-shadow': 2 - # Unit Tests (also written in TypeScript) - # Disable rules that do not make sense in test files (e.g. testing for undefined input values should be allowed) - - files: - - '*.spec.ts' - - 'test/*.ts' - extends: - - 'plugin:vitest/legacy-recommended' - rules: - no-undefined: 0 - no-unused-expressions: 0 - vitest/valid-expect: 0 diff --git a/assets/eslint.config.js b/assets/eslint.config.js new file mode 100644 index 00000000..166da758 --- /dev/null +++ b/assets/eslint.config.js @@ -0,0 +1,293 @@ +import tsEslint from 'typescript-eslint'; +import vitestPlugin from 'eslint-plugin-vitest'; +import globals from 'globals'; + +export default tsEslint.config( + ...tsEslint.configs.recommended, + { + name: 'PhilomenaConfig', + files: ['**/*.js', '**/*.ts'], + languageOptions: { + ecmaVersion: 6, + sourceType: 'module', + parserOptions: { + ecmaVersion: 6, + sourceType: 'module' + }, + globals: { + ...globals.browser + } + }, + rules: { + 'accessor-pairs': 2, + 'array-bracket-spacing': 0, + 'array-callback-return': 2, + 'arrow-body-style': 0, + 'arrow-parens': [2, 'as-needed'], + 'arrow-spacing': 2, + 'block-scoped-var': 2, + 'block-spacing': 2, + 'brace-style': [2, 'stroustrup', {allowSingleLine: true}], + 'callback-return': 0, + camelcase: [2, {allow: ['camo_url', 'spoiler_image_uri', 'image_ids']}], + 'class-methods-use-this': 0, + 'comma-dangle': [2, 'only-multiline'], + 'comma-spacing': 2, + 'comma-style': 2, + complexity: 0, + 'computed-property-spacing': [2, 'never'], + 'consistent-return': 0, + 'consistent-this': [2, 'that'], + 'constructor-super': 2, + curly: [2, 'multi-line', 'consistent'], + 'default-case': 2, + 'dot-location': [2, 'property'], + 'dot-notation': [2, {allowKeywords: true}], + 'eol-last': 2, + eqeqeq: 2, + 'func-call-spacing': 0, + 'func-name-matching': 2, + 'func-names': 0, + 'func-style': 0, + 'generator-star-spacing': 2, + 'global-require': 2, + 'guard-for-in': 0, + 'handle-callback-err': 2, + 'id-blacklist': 0, + 'id-length': 0, + 'id-match': 2, + indent: [2, 2, {SwitchCase: 1, VariableDeclarator: {var: 2, let: 2, const: 3}}], + 'init-declarations': 0, + 'jsx-quotes': 0, + 'key-spacing': 0, + 'keyword-spacing': 2, + 'line-comment-position': 0, + 'linebreak-style': [2, 'unix'], + 'lines-around-comment': 0, + 'lines-around-directive': 2, + 'max-depth': 0, + 'max-len': 0, + 'max-lines': 0, + 'max-nested-callbacks': 0, + 'max-params': 0, + 'max-statements-per-line': 0, + 'max-statements': 0, + 'multiline-ternary': 0, + 'new-cap': 2, + 'new-parens': 2, + 'newline-after-var': 0, + 'newline-before-return': 0, + 'newline-per-chained-call': 0, + 'no-alert': 0, + 'no-array-constructor': 2, + 'no-caller': 2, + 'no-case-declarations': 2, + 'no-catch-shadow': 2, + 'no-class-assign': 2, + 'no-cond-assign': 2, + 'no-confusing-arrow': 2, + 'no-console': 0, + 'no-const-assign': 2, + 'no-constant-condition': 2, + 'no-control-regex': 2, + 'no-debugger': 2, + 'no-delete-var': 2, + 'no-div-regex': 2, + 'no-dupe-args': 2, + 'no-dupe-class-members': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-duplicate-imports': 2, + 'no-else-return': 2, + 'no-empty-character-class': 2, + 'no-empty-function': 0, + 'no-empty-pattern': 2, + 'no-empty': 2, + 'no-eq-null': 2, + 'no-eval': 2, + 'no-ex-assign': 2, + 'no-extend-native': 2, + 'no-extra-bind': 2, + 'no-extra-boolean-cast': 2, + 'no-extra-label': 2, + 'no-extra-parens': [2, 'all', {nestedBinaryExpressions: false}], + 'no-extra-semi': 2, + 'no-fallthrough': 2, + 'no-floating-decimal': 2, + 'no-func-assign': 2, + 'no-global-assign': 2, + 'no-implicit-coercion': 2, + 'no-implicit-globals': 2, + 'no-implied-eval': 2, + 'no-inline-comments': 0, + 'no-inner-declarations': [2, 'both'], + 'no-invalid-regexp': 2, + 'no-invalid-this': 2, + 'no-irregular-whitespace': 2, + 'no-iterator': 2, + 'no-label-var': 2, + 'no-labels': 2, + 'no-lone-blocks': 2, + 'no-lonely-if': 0, + 'no-loop-func': 2, + 'no-magic-numbers': 0, + 'no-mixed-operators': 0, + 'no-mixed-requires': 0, + 'no-mixed-spaces-and-tabs': 2, + 'no-multi-spaces': 0, + 'no-multi-str': 2, + 'no-multiple-empty-lines': [2, {max: 3, maxBOF: 0, maxEOF: 1}], + 'no-native-reassign': 2, + 'no-negated-condition': 0, + 'no-negated-in-lhs': 2, + 'no-nested-ternary': 2, + 'no-new-func': 2, + 'no-new-object': 2, + 'no-new-require': 2, + 'no-new-symbol': 2, + 'no-new-wrappers': 2, + 'no-new': 2, + 'no-obj-calls': 2, + 'no-octal-escape': 2, + 'no-octal': 2, + 'no-param-reassign': 2, + 'no-path-concat': 2, + 'no-plusplus': 0, + 'no-process-env': 2, + 'no-process-exit': 2, + 'no-proto': 2, + 'no-prototype-builtins': 0, + 'no-redeclare': 2, + 'no-regex-spaces': 2, + 'no-restricted-globals': [2, 'event'], + 'no-restricted-imports': 2, + 'no-restricted-modules': 2, + 'no-restricted-properties': 0, + 'no-restricted-syntax': 2, + 'no-return-assign': 0, + 'no-script-url': 2, + 'no-self-assign': 2, + 'no-self-compare': 2, + 'no-sequences': 2, + 'no-shadow-restricted-names': 2, + 'no-shadow': 2, + 'no-spaced-func': 2, + 'no-sparse-arrays': 2, + 'no-sync': 0, + 'no-tabs': 2, + 'no-template-curly-in-string': 2, + 'no-ternary': 0, + 'no-this-before-super': 2, + 'no-throw-literal': 2, + 'no-trailing-spaces': 2, + 'no-undef-init': 2, + 'no-undef': 2, + 'no-undefined': 2, + 'no-underscore-dangle': 0, + 'no-unexpected-multiline': 2, + 'no-unmodified-loop-condition': 2, + 'no-unneeded-ternary': 2, + 'no-unreachable': 2, + 'no-unsafe-finally': 2, + 'no-unsafe-negation': 2, + 'no-unused-expressions': [2, {allowShortCircuit: true, allowTernary: true}], + 'no-unused-labels': 2, + 'no-unused-vars': [2, {vars: 'all', args: 'after-used', varsIgnorePattern: '^_', argsIgnorePattern: '^_'}], + 'no-use-before-define': [2, 'nofunc'], + 'no-useless-call': 2, + 'no-useless-computed-key': 2, + 'no-useless-concat': 2, + 'no-useless-constructor': 2, + 'no-useless-escape': 2, + 'no-useless-rename': 2, + 'no-var': 2, + 'no-void': 2, + 'no-warning-comments': 0, + 'no-whitespace-before-property': 2, + 'no-with': 2, + 'object-curly-newline': 0, + 'object-curly-spacing': 0, + 'object-property-newline': 0, + 'object-shorthand': 2, + 'one-var-declaration-per-line': 0, + 'one-var': 0, + 'operator-assignment': [2, 'always'], + 'operator-linebreak': 0, + 'padded-blocks': 0, + 'prefer-arrow-callback': 2, + 'prefer-const': 2, + 'prefer-numeric-literals': 2, + 'prefer-reflect': 0, + 'prefer-rest-params': 2, + 'prefer-spread': 0, + 'prefer-template': 2, + 'quote-props': [2, 'as-needed'], + quotes: [2, 'single'], + radix: 2, + 'require-jsdoc': 0, + 'require-yield': 2, + 'rest-spread-spacing': 2, + 'semi-spacing': [2, {before: false, after: true}], + semi: 2, + 'sort-imports': 0, + 'sort-keys': 0, + 'sort-vars': 0, + 'space-before-blocks': [2, 'always'], + 'space-before-function-paren': [2, 'never'], + 'space-in-parens': [2, 'never'], + 'space-infix-ops': 2, + 'space-unary-ops': [2, {words: true, nonwords: false}], + 'spaced-comment': 0, + strict: [2, 'function'], + 'symbol-description': 2, + 'template-curly-spacing': [2, 'never'], + 'unicode-bom': 2, + 'use-isnan': 2, + 'valid-jsdoc': 0, + 'valid-typeof': 2, + 'vars-on-top': 2, + 'wrap-iife': 2, + 'wrap-regex': 0, + 'yield-star-spacing': 2, + yoda: [2, 'never'], + }, + ignores: [ + 'js/vendor/*', + 'vite.config.ts' + ] + }, + { + files: ['**/*.js'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off' + } + }, + { + files: ['**/*.ts'], + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off', + 'no-redeclare': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'after-used', varsIgnorePattern: '^_.*', argsIgnorePattern: '^_.*'}], + '@typescript-eslint/no-redeclare': 2, + '@typescript-eslint/no-shadow': 2 + } + }, + { + files: ['**/*.spec.ts', '**/test/*.ts'], + plugins: { + vitest: vitestPlugin + }, + rules: { + ...vitestPlugin.configs.recommended.rules, + 'no-undef': 'off', + 'no-undefined': 'off', + 'no-unused-expressions': 0, + 'vitest/valid-expect': 0, + '@typescript-eslint/no-unused-expressions': 0 + } + } +); diff --git a/assets/js/booru.js b/assets/js/booru.js index 1f963dbe..3e1e7dac 100644 --- a/assets/js/booru.js +++ b/assets/js/booru.js @@ -122,7 +122,7 @@ function initializeFilters() { function unmarshal(data) { try { return JSON.parse(data); } - catch (_) { return data; } + catch { return data; } } function loadBooruData() { diff --git a/assets/js/fingerprint.js b/assets/js/fingerprint.js index cbc27c3a..8fadd183 100644 --- a/assets/js/fingerprint.js +++ b/assets/js/fingerprint.js @@ -41,7 +41,7 @@ function setFingerprintCookie() { fingerprint = `c${createFingerprint()}`; } // If fingerprinting fails, use fakeprint "c1836832948" as a last resort. - catch (err) { + catch { fingerprint = 'c1836832948'; } diff --git a/assets/js/settings.ts b/assets/js/settings.ts index 1faf28d0..9ff6b17f 100644 --- a/assets/js/settings.ts +++ b/assets/js/settings.ts @@ -22,8 +22,9 @@ export function setupSettings() { }); // Theme preview - themeSelect && themeSelect.addEventListener('change', () => { - styleSheet.href = assertNotUndefined(themeSelect.options[themeSelect.selectedIndex].dataset.themePath); - }); - + if (themeSelect) { + themeSelect.addEventListener('change', () => { + styleSheet.href = assertNotUndefined(themeSelect.options[themeSelect.selectedIndex].dataset.themePath); + }); + } } diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts index d47da985..a4eb36a0 100644 --- a/assets/js/shortcuts.ts +++ b/assets/js/shortcuts.ts @@ -53,12 +53,12 @@ const keyCodes: ShortcutKeyMap = { KeyO() { openFullView(); }, // O - open original KeyV() { openFullViewNewTab(); }, // V - open original in a new tab KeyF() { // F - favourite image - getHover() ? click(`a.interaction--fave[data-image-id="${getHover()}"]`) - : click('.block__header a.interaction--fave'); + click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]` + : '.block__header a.interaction--fave'); }, KeyU() { // U - upvote image - getHover() ? click(`a.interaction--upvote[data-image-id="${getHover()}"]`) - : click('.block__header a.interaction--upvote'); + click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]` + : '.block__header a.interaction--upvote'); }, }; diff --git a/assets/js/tagsmisc.ts b/assets/js/tagsmisc.ts index 7e80dce5..09366386 100644 --- a/assets/js/tagsmisc.ts +++ b/assets/js/tagsmisc.ts @@ -9,26 +9,22 @@ import { initTagDropdown } from './tags'; import { setupTagsInput, reloadTagsInput } from './tagsinput'; import '../types/ujs'; -type TagInputActionFunction = (tagInput: HTMLTextAreaElement | null) => void; +type TagInputActionFunction = (tagInput: HTMLTextAreaElement) => void; type TagInputActionList = Record; function tagInputButtons(event: MouseEvent) { const target = assertType(event.target, HTMLElement); const actions: TagInputActionList = { - save(tagInput: HTMLTextAreaElement | null) { - tagInput && store.set('tag_input', tagInput.value); + save(tagInput: HTMLTextAreaElement) { + store.set('tag_input', tagInput.value); }, - load(tagInput: HTMLTextAreaElement | null) { - if (!tagInput) { return; } - + load(tagInput: HTMLTextAreaElement) { // If entry 'tag_input' does not exist, try to use the current list tagInput.value = store.get('tag_input') || tagInput.value; reloadTagsInput(tagInput); }, - clear(tagInput: HTMLTextAreaElement | null) { - if (!tagInput) { return; } - + clear(tagInput: HTMLTextAreaElement) { tagInput.value = ''; reloadTagsInput(tagInput); }, @@ -36,7 +32,7 @@ function tagInputButtons(event: MouseEvent) { for (const [ name, action ] of Object.entries(actions)) { if (target && target.matches(`#tagsinput-${name}`)) { - action($('#image_tag_input')); + action(assertNotNull($('#image_tag_input'))); } } } diff --git a/assets/js/utils/store.ts b/assets/js/utils/store.ts index 21250946..c09a09ce 100644 --- a/assets/js/utils/store.ts +++ b/assets/js/utils/store.ts @@ -11,7 +11,7 @@ export default { localStorage.setItem(key, JSON.stringify(value)); return true; } - catch (err) { + catch { return false; } }, @@ -22,7 +22,7 @@ export default { try { return JSON.parse(value); } - catch (err) { + catch { return value as unknown as Value; } }, @@ -32,7 +32,7 @@ export default { localStorage.removeItem(key); return true; } - catch (err) { + catch { return false; } }, @@ -51,7 +51,7 @@ export default { const lastUpdatedKey = key + lastUpdatedSuffix; const lastUpdatedTime = Date.now() + maxAge; - this.set(key, value) && this.set(lastUpdatedKey, lastUpdatedTime); + return this.set(key, value) && this.set(lastUpdatedKey, lastUpdatedTime); }, // Whether the value of a key set with setWithExpireTime() has expired diff --git a/assets/test/vitest-setup.ts b/assets/test/vitest-setup.ts index a1f3c626..c7d92545 100644 --- a/assets/test/vitest-setup.ts +++ b/assets/test/vitest-setup.ts @@ -5,7 +5,6 @@ import { Blob } from 'node:buffer'; import { fireEvent } from '@testing-library/dom'; window.booru = { - // eslint-disable-next-line @typescript-eslint/no-empty-function timeAgo: () => {}, csrfToken: 'mockCsrfToken', hiddenTag: '/mock-tagblocked.svg', From 3a384f3085d447cf09be41a19c5b9436d37e5096 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 12 Jun 2024 21:42:00 -0400 Subject: [PATCH 21/44] Services updates --- docker-compose.yml | 4 ++-- docker/app/Dockerfile | 2 +- docker/web/Dockerfile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b4d57ea2..369705b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,7 +60,7 @@ services: - '5173:5173' postgres: - image: postgres:16.2-alpine + image: postgres:16.3-alpine environment: - POSTGRES_PASSWORD=postgres volumes: @@ -86,7 +86,7 @@ services: driver: "none" files: - image: andrewgaul/s3proxy:sha-ec12ae0 + image: andrewgaul/s3proxy:sha-4175022 environment: - JCLOUDS_FILESYSTEM_BASEDIR=/srv/philomena/priv/s3 volumes: diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index fb76abbc..b577bd78 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -1,4 +1,4 @@ -FROM elixir:1.16.2-alpine +FROM elixir:1.17-alpine ADD https://api.github.com/repos/philomena-dev/FFmpeg/git/refs/heads/release/6.1 /tmp/ffmpeg_version.json RUN (echo "https://github.com/philomena-dev/prebuilt-ffmpeg/raw/master"; cat /etc/apk/repositories) > /tmp/repositories \ diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index 60f8e21d..778f7609 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -1,4 +1,4 @@ -FROM openresty/openresty:1.25.3.1-2-alpine +FROM openresty/openresty:1.25.3.1-4-alpine ARG APP_DIR ARG S3_SCHEME ARG S3_HOST From b11ae51446fc544338bf1595b1f7fcedf893fab9 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 12 Jun 2024 21:47:15 -0400 Subject: [PATCH 22/44] Fix compile warnings --- lib/philomena_web/controllers/profile_controller.ex | 2 +- lib/philomena_web/templates/markdown/_input.html.slime | 6 +++--- .../templates/profile/commission/_form.html.slime | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/philomena_web/controllers/profile_controller.ex b/lib/philomena_web/controllers/profile_controller.ex index b5f1020d..75f476d4 100644 --- a/lib/philomena_web/controllers/profile_controller.ex +++ b/lib/philomena_web/controllers/profile_controller.ex @@ -212,7 +212,7 @@ defmodule PhilomenaWeb.ProfileController do end defp individual_stat(mapping, stat_name) do - Enum.map(89..0, &(map_fetch(mapping[&1], stat_name) || 0)) + Enum.map(89..0//-1, &(map_fetch(mapping[&1], stat_name) || 0)) end defp map_fetch(nil, _field_name), do: nil diff --git a/lib/philomena_web/templates/markdown/_input.html.slime b/lib/philomena_web/templates/markdown/_input.html.slime index 8ec1ae19..7cc63336 100644 --- a/lib/philomena_web/templates/markdown/_input.html.slime +++ b/lib/philomena_web/templates/markdown/_input.html.slime @@ -1,6 +1,6 @@ - form = assigns[:f] -- action_text = assigns[:action_text] || 'Edit' -- action_icon = assigns[:action_icon] || 'edit' +- action_text = assigns[:action_text] || "Edit" +- action_icon = assigns[:action_icon] || "edit" - field_name = assigns[:name] || :body - field_placeholder = assigns[:placeholder] || "Your message" - is_required = assigns[:required] @@ -11,7 +11,7 @@ = action_text a href="#" data-click-tab="preview" - i.fa.fa-cog.fa-fw.fa-spin.js-preview-loading.hidden> title=raw('Loading preview…') + i.fa.fa-cog.fa-fw.fa-spin.js-preview-loading.hidden> title=raw("Loading preview…") i.fa.fa-eye.fa-fw.js-preview-idle> | Preview diff --git a/lib/philomena_web/templates/profile/commission/_form.html.slime b/lib/philomena_web/templates/profile/commission/_form.html.slime index ac2e286b..1936edf1 100644 --- a/lib/philomena_web/templates/profile/commission/_form.html.slime +++ b/lib/philomena_web/templates/profile/commission/_form.html.slime @@ -23,7 +23,7 @@ .field => label f, :categories, "Art Categories:" br - = collection_checkboxes f, :categories, categories(), selected: f.data.categories, input_opts: [ class: 'checkbox spacing-right' ], wrapper: &Phoenix.HTML.Tag.content_tag(:span, &1, class: "commission__category") + = collection_checkboxes f, :categories, categories(), selected: f.data.categories, input_opts: [ class: "checkbox spacing-right" ], wrapper: &Phoenix.HTML.Tag.content_tag(:span, &1, class: "commission__category") = error_tag f, :categories .field => label f, :sheet_image_id, "Image ID of your commissions sheet (optional but recommended):" From 1faa98f18794ed224b33d86fde8c632af7aa9859 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 12 Jun 2024 22:01:57 -0400 Subject: [PATCH 23/44] mix deps.unlock --all && mix deps.get --- lib/philomena_media/analyzers.ex | 3 +- mix.exs | 11 ++---- mix.lock | 58 +++++++++++++++----------------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/lib/philomena_media/analyzers.ex b/lib/philomena_media/analyzers.ex index a010916f..efa49d9a 100644 --- a/lib/philomena_media/analyzers.ex +++ b/lib/philomena_media/analyzers.ex @@ -54,7 +54,8 @@ defmodule PhilomenaMedia.Analyzers do :error = Analyzers.analyze(file) """ - @spec analyze(Plug.Upload.t() | Path.t()) :: {:ok, Result.t()} | :error + @spec analyze(Plug.Upload.t() | Path.t()) :: + {:ok, Result.t()} | {:unsupported_mime, Mime.t()} | :error def analyze(%Plug.Upload{path: path}), do: analyze(path) def analyze(path) when is_binary(path) do diff --git a/mix.exs b/mix.exs index 130f1ccc..41e15a6f 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,7 @@ defmodule Philomena.MixProject do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.6"}, + {:phoenix, "~> 1.7"}, {:phoenix_pubsub, "~> 2.1"}, {:phoenix_ecto, "~> 4.4"}, {:ecto_sql, "~> 3.9"}, @@ -46,8 +46,7 @@ defmodule Philomena.MixProject do {:jason, "~> 1.4"}, {:ranch, "~> 2.1", override: true}, {:plug_cowboy, "~> 2.6"}, - {:slime, "~> 1.3.0", - github: "liamwhite/slime", ref: "4c8ad4e9e9dcc792f4db769a9ef2ad7d6eba8f31", override: true}, + {:slime, "~> 1.3.1"}, {:phoenix_slime, "~> 0.13", github: "slime-lang/phoenix_slime", ref: "8944de91654d6fcf6bdcc0aed6b8647fe3398241"}, {:phoenix_pubsub_redis, "~> 3.0"}, @@ -83,7 +82,7 @@ defmodule Philomena.MixProject do {:rustler, "~> 0.27"}, # Linting - {:credo, "~> 1.6", only: [:dev, :test], override: true}, + {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:credo_envvar, "~> 0.1", only: [:dev, :test], runtime: false}, {:credo_naming, "~> 2.0", only: [:dev, :test], runtime: false}, @@ -94,10 +93,6 @@ defmodule Philomena.MixProject do # Static analysis {:dialyxir, "~> 1.2", only: :dev, runtime: false}, - # Fixes for OTP/25 - {:neotoma, "~> 1.7.3", manager: :rebar3, override: true}, - {:hut, "~> 1.4.0", manager: :rebar3, override: true}, - # Fixes for Elixir v1.15+ {:canary, "~> 1.1", github: "marcinkoziej/canary", ref: "704debde7a2c0600f78c687807884bf37c45bd79"} diff --git a/mix.lock b/mix.lock index 73184f5a..f8e68868 100644 --- a/mix.lock +++ b/mix.lock @@ -4,24 +4,24 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm", "4269f74153fe89583fe50bd4d5de57bfe01f31258a6b676d296f3681f1483c68"}, "canary": {:git, "https://github.com/marcinkoziej/canary.git", "704debde7a2c0600f78c687807884bf37c45bd79", [ref: "704debde7a2c0600f78c687807884bf37c45bd79"]}, - "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, - "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "credo_envvar": {:hex, :credo_envvar, "0.1.4", "40817c10334e400f031012c0510bfa0d8725c19d867e4ae39cf14f2cbebc3b20", [:mix], [{:credo, "~> 1.0", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "5055cdb4bcbaf7d423bc2bb3ac62b4e2d825e2b1e816884c468dee59d0363009"}, "credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto_network": {:hex, :ecto_network, "1.5.0", "a930c910975e7a91237b858ebf0f4ad7b2aae32fa846275aa203cb858459ec73", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "4d614434ae3e6d373a2f693d56aafaa3f3349714668ffd6d24e760caf578aa2f"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.2", "c7cc7f812af571e50b80294dc2e535821b3b795ce8008d07aa5f336591a185a8", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "73c07f995ac17dbf89d3cfaaf688fcefabcd18b7b004ac63b0dc4ef39499ed6b"}, "elastix": {:hex, :elastix, "0.10.0", "7567da885677ba9deffc20063db5f3ca8cd10f23cff1ab3ed9c52b7063b7e340", [:mix], [{:httpoison, "~> 1.4", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:retry, "~> 0.8", [hex: :retry, repo: "hexpm", optional: false]}], "hexpm", "5fb342ce068b20f7845f5dd198c2dc80d967deafaa940a6e51b846db82696d1d"}, - "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, + "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_aws": {:git, "https://github.com/liamwhite/ex_aws.git", "a340859dd8ac4d63bd7a3948f0994e493e49bda4", [ref: "a340859dd8ac4d63bd7a3948f0994e493e49bda4"]}, @@ -29,62 +29,60 @@ "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "exq": {:hex, :exq, "0.19.0", "06eb92944dad39f0954dc8f63190d3e24d11734eef88cf5800883e57ebf74f3c", [:mix], [{:elixir_uuid, ">= 1.2.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0 and < 6.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:redix, ">= 0.9.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "24fc0ebdd87cc7406e1034fb46c2419f9c8a362f0ec634d23b6b819514d36390"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, - "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, - "hut": {:hex, :hut, "1.4.0", "7a1238ec00f95c9ec75412587ee11ac652eca308a7f4b8cc9629746d579d6cf0", [:"erlang.mk", :rebar3], [], "hexpm", "7af8704b9bae98a336f70d9560fc3c97f15665265fa603dbd05352e63d6ebb03"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, - "mix_audit": {:hex, :mix_audit, "2.1.2", "6cd5c5e2edbc9298629c85347b39fb3210656e541153826efd0b2a63767f3395", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "68d2f06f96b9c445a23434c9d5f09682866a5b4e90f631829db1c64f140e795b"}, - "mua": {:hex, :mua, "0.2.1", "7f1c20dbe7266d514a07bf5b7a3946413d70150be41cb5475b5a95bb517a378f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "0bc556803a1d09dfb69bfebecb838cf33a2d123de84f700c41b6b8134027c11f"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, + "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, + "mua": {:hex, :mua, "0.2.2", "d2997abc1eee43d91e4a355665658743ad2609b8d5992425940ce17b7ff87933", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "cda7e38c65d3105b3017b25ac402b4c9457892abeb2e11c331b25a92d16b04c0"}, "neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], [], "hexpm", "2da322b9b1567ffa0706a7f30f6bbbde70835ae44a1050615f4b4a3d436e0f28"}, - "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "pbkdf2": {:git, "https://github.com/basho/erlang-pbkdf2.git", "7e9bd5fcd3cc3062159e4c9214bb628aa6feb5ca", [ref: "7e9bd5fcd3cc3062159e4c9214bb628aa6feb5ca"]}, - "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.0", "1a1f841ccda19b15f1d82968840a5b895c5f687b6734e430e4b2dbe035ca1837", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "13990570fde09e16959ef214501fe2813e1192d62ca753ec8798980580436f94"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.1", "6ab463cf43938ee9906067b33c8d66782343de4280a70084cd5617accc6345a8", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "e8467d308b61f294f68afe12c81bf585584c7ceed40ec8adde88ec176d480a78"}, + "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_pubsub_redis": {:hex, :phoenix_pubsub_redis, "3.0.1", "d4d856b1e57a21358e448543e1d091e07e83403dde4383b8be04ed9d2c201cbc", [:mix], [{:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1 or ~> 1.6", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 0.10.0 or ~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "0b36a17ff6e9a56159f8df8933d62b5c1f0695eae995a02e0c86c035ace6a309"}, "phoenix_slime": {:git, "https://github.com/slime-lang/phoenix_slime.git", "8944de91654d6fcf6bdcc0aed6b8647fe3398241", [ref: "8944de91654d6fcf6bdcc0aed6b8647fe3398241"]}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, - "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"}, - "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, + "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm", "a266b7fb7be0d3b713912055dde3575927eca920e5d604ded45cd534f6b7a447"}, "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, - "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, - "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, + "redix": {:hex, :redix, "1.5.1", "a2386971e69bf23630fb3a215a831b5478d2ee7dc9ea7ac811ed89186ab5d7b7", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "85224eb2b683c516b80d472eb89b76067d5866913bf0be59d646f550de71f5c4"}, + "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"}, - "rustler": {:hex, :rustler, "0.31.0", "7e5eefe61e6e6f8901e5aa3de60073d360c6320d9ec363027b0197297b80c46a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "99e378459bfb9c3bda6d3548b2b3bc6f9ad97f728f76bdbae7bf5c770a4f8abd"}, + "rustler": {:hex, :rustler, "0.33.0", "4a5b0a7a7b0b51549bea49947beff6fae9bc5d5326104dcd4531261e876b5619", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "7c4752728fee59a815ffd20c3429c55b644041f25129b29cdeb5c470b80ec5fd"}, "scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"}, "scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"}, "secure_compare": {:hex, :secure_compare, "0.1.0", "01b3c93c8edb696e8a5b38397ed48e10958c8a5ec740606656445bcbec0aadb8", [:mix], [], "hexpm", "6391a49eb4a6182f0d7425842fc774bbed715e78b2bfb0c83b99c94e02c78b5c"}, - "slime": {:git, "https://github.com/liamwhite/slime.git", "4c8ad4e9e9dcc792f4db769a9ef2ad7d6eba8f31", [ref: "4c8ad4e9e9dcc792f4db769a9ef2ad7d6eba8f31"]}, + "slime": {:hex, :slime, "1.3.1", "d6781854092a638e451427c33e67be348352651a7917a128155b8a41ac88d0a2", [:mix], [{:neotoma, "~> 1.7", [hex: :neotoma, repo: "hexpm", optional: false]}], "hexpm", "099b09280297e0c6c8d1f56b0033b885fc4eb541ad3c4a75f88a589354e2501b"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, + "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, } From 97feb0ac001445e3fc32c81d14710c72e2bc516e Mon Sep 17 00:00:00 2001 From: Luna D Date: Sat, 8 Jun 2024 14:43:10 -0400 Subject: [PATCH 24/44] Add version 4 TypeScript-based FP calculation --- assets/js/fingerprint.js | 51 ----- assets/js/fp.ts | 210 ++++++++++++++++++ assets/js/when-ready.ts | 4 +- lib/philomena_web/fingerprint.ex | 85 +++++++ lib/philomena_web/plugs/current_ban_plug.ex | 4 +- .../plugs/filter_banned_users_plug.ex | 4 +- lib/philomena_web/router.ex | 2 + lib/philomena_web/user_auth.ex | 3 +- lib/philomena_web/user_fingerprint_updater.ex | 13 +- test/philomena_web/user_auth_test.exs | 1 + test/support/conn_case.ex | 4 +- 11 files changed, 313 insertions(+), 68 deletions(-) delete mode 100644 assets/js/fingerprint.js create mode 100644 assets/js/fp.ts create mode 100644 lib/philomena_web/fingerprint.ex diff --git a/assets/js/fingerprint.js b/assets/js/fingerprint.js deleted file mode 100644 index 8fadd183..00000000 --- a/assets/js/fingerprint.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Fingerprints - */ - -// http://stackoverflow.com/a/34842797 -function hashCode(str) { - return str.split('').reduce((prevHash, currVal) => - ((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0) >>> 0; -} - -function createFingerprint() { - const prints = [ - navigator.userAgent, - navigator.cpuClass, - navigator.oscpu, - navigator.platform, - - navigator.browserLanguage, - navigator.language, - navigator.systemLanguage, - navigator.userLanguage, - - screen.availLeft, - screen.availTop, - screen.availWidth, - screen.height, - screen.width, - - window.devicePixelRatio, - new Date().getTimezoneOffset(), - ]; - - return hashCode(prints.join('')); -} - -function setFingerprintCookie() { - let fingerprint; - - // The prepended 'c' acts as a crude versioning mechanism. - try { - fingerprint = `c${createFingerprint()}`; - } - // If fingerprinting fails, use fakeprint "c1836832948" as a last resort. - catch { - fingerprint = 'c1836832948'; - } - - document.cookie = `_ses=${fingerprint}; path=/; SameSite=Lax`; -} - -export { setFingerprintCookie }; diff --git a/assets/js/fp.ts b/assets/js/fp.ts new file mode 100644 index 00000000..65f0c583 --- /dev/null +++ b/assets/js/fp.ts @@ -0,0 +1,210 @@ +/** + * FP version 4 + * + * Not reliant on deprecated properties, and potentially + * more accurate at what it's supposed to do. + */ + +import store from './utils/store'; + +const storageKey = 'cached_ses_value'; + +declare global { + interface Keyboard { + getLayoutMap: () => Promise> + } + + interface UserAgentData { + brands: [{brand: string, version: string}], + mobile: boolean, + platform: string, + } + + interface Navigator { + deviceMemory: number | undefined, + keyboard: Keyboard | undefined, + userAgentData: UserAgentData | undefined, + } +} + +/** + * Creates a 53-bit non-cryptographic hash of a string. + * + * @param str The string to hash. + * @param seed The seed to use for hash generation. + * @return The resulting hash as a 53-bit number. + * @see {@link https://stackoverflow.com/a/52171480} + */ +function cyrb53(str: string, seed: number = 0x16fe7b0a): number { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + + h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507); + h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909); + h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507); + h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +} + +/** + * Get keyboard layout data from the navigator layout map. + * + * @return String containing layout map entries, or `none` when unavailable + */ +async function getKeyboardData(): Promise { + if (navigator.keyboard) { + const layoutMap = await navigator.keyboard.getLayoutMap(); + + return Array.from(layoutMap.entries()) + .sort() + .map(([k, v]) => `${k}${v}`) + .join(''); + } + + return 'none'; +} + +/** + * Get an approximation of memory available in gigabytes. + * + * @return String containing device memory data, or `1` when unavailable + */ +function getMemoryData(): string { + if (navigator.deviceMemory) { + return navigator.deviceMemory.toString(); + } + + return '1'; +} + +/** + * Get the "brands" of the user agent. + * + * For Chromium-based browsers this returns additional data like "Edge" or "Chrome" + * which may also contain additional data beyond the `userAgent` property. + * + * @return String containing brand data, or `none` when unavailable + */ +function getUserAgentBrands(): string { + const data = navigator.userAgentData; + + if (data) { + let brands = 'none'; + + if (data.brands && data.brands.length > 0) { + // NB: Chromium implements GREASE protocol to prevent ossification of + // the "Not a brand" string - see https://stackoverflow.com/a/64443187 + brands = data.brands + .filter(e => !e.brand.match(/.*ot.*rand.*/gi)) + .map(e => `${e.brand}${e.version}`) + .sort() + .join(''); + } + + return `${brands}${data.mobile}${data.platform}`; + } + + return 'none'; +} + +/** + * Get the size in rems of the default body font. + * + * Causes a forced layout. Be sure to cache this value. + * + * @return String with the rem size + */ +function getFontRemSize(): string { + const testElement = document.createElement('span'); + testElement.style.minWidth = '1rem'; + testElement.style.maxWidth = '1rem'; + testElement.style.position = 'absolute'; + + document.body.appendChild(testElement); + + const width = testElement.clientWidth.toString(); + + document.body.removeChild(testElement); + + return width; +} + +/** + * Create a semi-unique string from browser attributes. + * + * @return Hexadecimally encoded 53 bit number padded to 7 bytes. + */ +async function createFp(): Promise { + const prints: string[] = [ + navigator.userAgent, + navigator.hardwareConcurrency.toString(), + navigator.maxTouchPoints.toString(), + navigator.language, + await getKeyboardData(), + getMemoryData(), + getUserAgentBrands(), + getFontRemSize(), + + screen.height.toString(), + screen.width.toString(), + screen.colorDepth.toString(), + screen.pixelDepth.toString(), + + window.devicePixelRatio.toString(), + new Date().getTimezoneOffset().toString(), + ]; + + return cyrb53(prints.join('')) + .toString(16) + .padStart(14, '0'); +} + +/** + * Gets the existing `_ses` value from local storage or cookies. + * + * @return String `_ses` value or `null` + */ +function getSesValue(): string | null { + // Try storage + const storageValue: string | null = store.get(storageKey); + if (storageValue) { + return storageValue; + } + + // Try cookie + const match = document.cookie.match(/_ses=([a-f0-9]+)/); + if (match && match[1]) { + return match[1]; + } + + // Not found + return null; +} + +/** + * Sets the `_ses` cookie. + * + * If `cached_ses_value` is present in local storage, uses it to set the `_ses` cookie. + * Otherwise, if the `_ses` cookie already exists, uses its value instead. + * Otherwise, attempts to generate a new value for the `_ses` cookie based on + * various browser attributes. + * Failing all previous methods, sets the `_ses` cookie to a fallback value. + */ +export async function setSesCookie() { + let sesValue = getSesValue(); + + if (!sesValue || sesValue.charAt(0) !== 'd' || sesValue.length !== 15) { + // The prepended 'd' acts as a crude versioning mechanism. + sesValue = `d${await createFp()}`; + store.set(storageKey, sesValue); + } + + document.cookie = `_ses=${sesValue}; path=/; SameSite=Lax`; +} diff --git a/assets/js/when-ready.ts b/assets/js/when-ready.ts index 3bd3e049..efefea64 100644 --- a/assets/js/when-ready.ts +++ b/assets/js/when-ready.ts @@ -11,7 +11,7 @@ import { setupBurgerMenu } from './burger'; import { bindCaptchaLinks } from './captcha'; import { setupComments } from './comment'; import { setupDupeReports } from './duplicate_reports'; -import { setFingerprintCookie } from './fingerprint'; +import { setSesCookie } from './fp'; import { setupGalleryEditing } from './galleries'; import { initImagesClientside } from './imagesclientside'; import { bindImageTarget } from './image_expansion'; @@ -44,7 +44,7 @@ whenReady(() => { initImagesClientside(); setupComments(); setupDupeReports(); - setFingerprintCookie(); + setSesCookie(); setupGalleryEditing(); bindImageTarget(); setupEvents(); diff --git a/lib/philomena_web/fingerprint.ex b/lib/philomena_web/fingerprint.ex new file mode 100644 index 00000000..99d8fadd --- /dev/null +++ b/lib/philomena_web/fingerprint.ex @@ -0,0 +1,85 @@ +defmodule PhilomenaWeb.Fingerprint do + import Plug.Conn + + @type t :: String.t() + @name "_ses" + + @doc """ + Assign the current fingerprint to the conn. + """ + @spec fetch_fingerprint(Plug.Conn.t(), any()) :: Plug.Conn.t() + def fetch_fingerprint(conn, _opts) do + conn = + conn + |> fetch_session() + |> fetch_cookies() + + # Try to get the fingerprint from the session, then from the cookie. + fingerprint = upgrade(get_session(conn, @name), conn.cookies[@name]) + + # If the fingerprint is valid, persist to session. + case valid_format?(fingerprint) do + true -> + conn + |> put_session(@name, fingerprint) + |> assign(:fingerprint, fingerprint) + + false -> + assign(conn, :fingerprint, nil) + end + end + + defp upgrade(<<"c", _::binary>> = session_value, <<"d", _::binary>> = cookie_value) do + if valid_format?(cookie_value) do + # When both fingerprint values are valid and the session value + # is an old version, use the cookie value. + cookie_value + else + # Use the session value. + session_value + end + end + + defp upgrade(session_value, cookie_value) do + # Prefer the session value, using the cookie value if it is unavailable. + session_value || cookie_value + end + + @doc """ + Determine whether the fingerprint corresponds to a valid format. + + Valid formats start with `c` or `d` (for the version). The `c` format is a legacy format + corresponding to an integer-valued hash from the frontend. The `d` format is the current + format corresponding to a hex-valued hash from the frontend. By design, it is not + possible to infer anything else about these values from the server. + + See assets/js/fp.ts for additional information on the generation of the `d` format. + + ## Examples + + iex> valid_format?("b2502085657") + false + + iex> valid_format?("c637334158") + true + + iex> valid_format?("d63c4581f8cf58d") + true + + iex> valid_format?("5162549b16e8448") + false + + """ + @spec valid_format?(any()) :: boolean() + def valid_format?(fingerprint) + + def valid_format?(<<"c", rest::binary>>) when byte_size(rest) <= 12 do + match?({_result, ""}, Integer.parse(rest)) + end + + def valid_format?(<<"d", rest::binary>>) when byte_size(rest) == 14 do + match?({:ok, _result}, Base.decode16(rest, case: :lower)) + end + + def valid_format?(_fingerprint), do: false +end diff --git a/lib/philomena_web/plugs/current_ban_plug.ex b/lib/philomena_web/plugs/current_ban_plug.ex index 0924b858..273a7889 100644 --- a/lib/philomena_web/plugs/current_ban_plug.ex +++ b/lib/philomena_web/plugs/current_ban_plug.ex @@ -16,9 +16,7 @@ defmodule PhilomenaWeb.CurrentBanPlug do @doc false @spec call(Conn.t(), any()) :: Conn.t() def call(conn, _opts) do - conn = Conn.fetch_cookies(conn) - - fingerprint = conn.cookies["_ses"] + fingerprint = conn.assigns.fingerprint user = conn.assigns.current_user ip = conn.remote_ip diff --git a/lib/philomena_web/plugs/filter_banned_users_plug.ex b/lib/philomena_web/plugs/filter_banned_users_plug.ex index 5b5c440d..866bde43 100644 --- a/lib/philomena_web/plugs/filter_banned_users_plug.ex +++ b/lib/philomena_web/plugs/filter_banned_users_plug.ex @@ -37,9 +37,7 @@ defmodule PhilomenaWeb.FilterBannedUsersPlug do defp maybe_halt_no_fingerprint(%{method: "GET"} = conn), do: conn defp maybe_halt_no_fingerprint(conn) do - conn = Conn.fetch_cookies(conn) - - case conn.cookies["_ses"] do + case conn.assigns.fingerprint do nil -> PhilomenaWeb.NotAuthorizedPlug.call(conn) diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 8415b112..7a89f4b1 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -2,6 +2,7 @@ defmodule PhilomenaWeb.Router do use PhilomenaWeb, :router import PhilomenaWeb.UserAuth + import PhilomenaWeb.Fingerprint pipeline :browser do plug :accepts, ["html"] @@ -9,6 +10,7 @@ defmodule PhilomenaWeb.Router do plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers + plug :fetch_fingerprint plug :fetch_current_user plug PhilomenaWeb.ContentSecurityPolicyPlug plug PhilomenaWeb.CurrentFilterPlug diff --git a/lib/philomena_web/user_auth.ex b/lib/philomena_web/user_auth.ex index 19a4cdbc..00a9e94e 100644 --- a/lib/philomena_web/user_auth.ex +++ b/lib/philomena_web/user_auth.ex @@ -211,9 +211,8 @@ defmodule PhilomenaWeb.UserAuth do defp update_usages(conn, user) do now = DateTime.utc_now() |> DateTime.truncate(:second) - conn = fetch_cookies(conn) UserIpUpdater.cast(user.id, conn.remote_ip, now) - UserFingerprintUpdater.cast(user.id, conn.cookies["_ses"], now) + UserFingerprintUpdater.cast(user.id, conn.assigns.fingerprint, now) end end diff --git a/lib/philomena_web/user_fingerprint_updater.ex b/lib/philomena_web/user_fingerprint_updater.ex index 62e14270..41863dcf 100644 --- a/lib/philomena_web/user_fingerprint_updater.ex +++ b/lib/philomena_web/user_fingerprint_updater.ex @@ -3,6 +3,8 @@ defmodule PhilomenaWeb.UserFingerprintUpdater do alias Philomena.Repo import Ecto.Query + alias PhilomenaWeb.Fingerprint + def child_spec([]) do %{ id: PhilomenaWeb.UserFingerprintUpdater, @@ -14,14 +16,13 @@ defmodule PhilomenaWeb.UserFingerprintUpdater do {:ok, spawn_link(&init/0)} end - def cast(user_id, <<"c", _rest::binary>> = fingerprint, updated_at) - when byte_size(fingerprint) <= 12 do - pid = Process.whereis(:fingerprint_updater) - if pid, do: send(pid, {user_id, fingerprint, updated_at}) + def cast(user_id, fingerprint, updated_at) do + if Fingerprint.valid_format?(fingerprint) do + pid = Process.whereis(:fingerprint_updater) + if pid, do: send(pid, {user_id, fingerprint, updated_at}) + end end - def cast(_user_id, _fingerprint, _updated_at), do: nil - defp init do Process.register(self(), :fingerprint_updater) run() diff --git a/test/philomena_web/user_auth_test.exs b/test/philomena_web/user_auth_test.exs index deeaeca7..3465111a 100644 --- a/test/philomena_web/user_auth_test.exs +++ b/test/philomena_web/user_auth_test.exs @@ -9,6 +9,7 @@ defmodule PhilomenaWeb.UserAuthTest do conn = conn |> Map.replace!(:secret_key_base, PhilomenaWeb.Endpoint.config(:secret_key_base)) + |> assign(:fingerprint, "d015c342859dde3") |> init_test_session(%{}) %{user: user_fixture(), conn: conn} diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index aa9df751..dd4240a4 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -41,9 +41,11 @@ defmodule PhilomenaWeb.ConnCase do |> Philomena.Filters.change_filter() |> Philomena.Repo.insert!() + fingerprint = to_string(:io_lib.format(~c"d~14.16.0b", [:rand.uniform(2 ** 53)])) + conn = Phoenix.ConnTest.build_conn() - |> Phoenix.ConnTest.put_req_cookie("_ses", Integer.to_string(System.unique_integer())) + |> Phoenix.ConnTest.put_req_cookie("_ses", fingerprint) {:ok, conn: conn} end From bbc1879a1ecb5fa393cc2e342c06fd9035bcf2c0 Mon Sep 17 00:00:00 2001 From: Luna D Date: Thu, 6 Jun 2024 20:44:41 -0400 Subject: [PATCH 25/44] Convert clientside image filtering scripts to TypeScript --- assets/js/imagesclientside.js | 76 ------------------------ assets/js/imagesclientside.ts | 109 ++++++++++++++++++++++++++++++++++ assets/js/utils/tag.ts | 4 +- 3 files changed, 111 insertions(+), 78 deletions(-) delete mode 100644 assets/js/imagesclientside.js create mode 100644 assets/js/imagesclientside.ts diff --git a/assets/js/imagesclientside.js b/assets/js/imagesclientside.js deleted file mode 100644 index 7dc3fb31..00000000 --- a/assets/js/imagesclientside.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Client-side image filtering/spoilering. - */ - -import { $$, escapeHtml } from './utils/dom'; -import { setupInteractions } from './interactions'; -import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image'; -import { getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag'; - -function runFilter(img, test, runCallback) { - if (!test || test.length === 0) return false; - - runCallback(img, test); - - // I don't like this. - window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId); - - return true; -} - -// --- - -function filterThumbSimple(img, tagsHit) { hideThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `[HIDDEN] ${displayTags(tagsHit)}`); } -function spoilerThumbSimple(img, tagsHit) { spoilerThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, displayTags(tagsHit)); } -function filterThumbComplex(img) { hideThumb(img, window.booru.hiddenTag, '[HIDDEN] (Complex Filter)'); } -function spoilerThumbComplex(img) { spoilerThumb(img, window.booru.hiddenTag, '(Complex Filter)'); } - -function filterBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is hidden by `); } -function spoilerBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is spoilered by `); } -function filterBlockComplex(img) { spoilerBlock(img, window.booru.hiddenTag, 'This image was hidden by a complex tag expression in '); } -function spoilerBlockComplex(img) { spoilerBlock(img, window.booru.hiddenTag, 'This image was spoilered by a complex tag expression in '); } - -// --- - -function thumbTagFilter(tags, img) { return runFilter(img, imageHitsTags(img, tags), filterThumbSimple); } -function thumbComplexFilter(complex, img) { return runFilter(img, imageHitsComplex(img, complex), filterThumbComplex); } -function thumbTagSpoiler(tags, img) { return runFilter(img, imageHitsTags(img, tags), spoilerThumbSimple); } -function thumbComplexSpoiler(complex, img) { return runFilter(img, imageHitsComplex(img, complex), spoilerThumbComplex); } - -function blockTagFilter(tags, img) { return runFilter(img, imageHitsTags(img, tags), filterBlockSimple); } -function blockComplexFilter(complex, img) { return runFilter(img, imageHitsComplex(img, complex), filterBlockComplex); } -function blockTagSpoiler(tags, img) { return runFilter(img, imageHitsTags(img, tags), spoilerBlockSimple); } -function blockComplexSpoiler(complex, img) { return runFilter(img, imageHitsComplex(img, complex), spoilerBlockComplex); } - -// --- - -function filterNode(node = document) { - const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags(); - const { hiddenFilter, spoileredFilter } = window.booru; - - // Image thumb boxes with vote and fave buttons on them - $$('.image-container', node) - .filter(img => !thumbTagFilter(hiddenTags, img)) - .filter(img => !thumbComplexFilter(hiddenFilter, img)) - .filter(img => !thumbTagSpoiler(spoileredTags, img)) - .filter(img => !thumbComplexSpoiler(spoileredFilter, img)) - .forEach(img => showThumb(img)); - - // Individual image pages and images in posts/comments - $$('.image-show-container', node) - .filter(img => !blockTagFilter(hiddenTags, img)) - .filter(img => !blockComplexFilter(hiddenFilter, img)) - .filter(img => !blockTagSpoiler(spoileredTags, img)) - .filter(img => !blockComplexSpoiler(spoileredFilter, img)) - .forEach(img => showBlock(img)); -} - -function initImagesClientside() { - window.booru.imagesWithDownvotingDisabled = []; - // This fills the imagesWithDownvotingDisabled array - filterNode(document); - // Once the array is populated, we can initialize interactions - setupInteractions(); -} - -export { initImagesClientside, filterNode }; diff --git a/assets/js/imagesclientside.ts b/assets/js/imagesclientside.ts new file mode 100644 index 00000000..61e1d1ac --- /dev/null +++ b/assets/js/imagesclientside.ts @@ -0,0 +1,109 @@ +/** + * Client-side image filtering/spoilering. + */ + +import { $$, escapeHtml } from './utils/dom'; +import { setupInteractions } from './interactions'; +import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image'; +import { TagData, getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag'; +import { AstMatcher } from './query/types'; + +type CallbackType = 'tags' | 'complex'; +type RunCallback = (img: HTMLDivElement, tags: TagData[], type: CallbackType) => void; + +function run( + img: HTMLDivElement, + tags: TagData[], + complex: AstMatcher, + callback: RunCallback, +): boolean { + const hit = (() => { + // Check tags array first to provide more precise filter explanations + const hitTags = imageHitsTags(img, tags); + if (hitTags.length !== 0) { + callback(img, hitTags, 'tags'); + return true; + } + + // No tags matched, try complex filter AST + const hitComplex = imageHitsComplex(img, complex); + if (hitComplex) { + callback(img, hitTags, 'complex'); + return true; + } + + // Nothing matched at all, image can be shown + return false; + })(); + + if (hit && img.dataset.imageId) { + // Disallow negative interaction on image which is not visible + window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId); + } + + return hit; +} + +function bannerImage(tagsHit: TagData[]) { + if (tagsHit.length > 0) { + return tagsHit[0].spoiler_image_uri || window.booru.hiddenTag; + } + + return window.booru.hiddenTag; +} + +// TODO: this approach is not suitable for translations because it depends on +// markup embedded in the page adjacent to this text + +/* eslint-disable indent */ + +function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { + const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}` + : '[HIDDEN] (Complex Filter)'; + hideThumb(img, bannerImage(tagsHit), bannerText); +} + +function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { + const bannerText = type === 'tags' ? displayTags(tagsHit) + : '(Complex Filter)'; + spoilerThumb(img, bannerImage(tagsHit), bannerText); +} + +function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { + const bannerText = type === 'tags' ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is hidden by ` + : 'This image was hidden by a complex tag expression in '; + spoilerBlock(img, bannerImage(tagsHit), bannerText); +} + +function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { + const bannerText = type === 'tags' ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is spoilered by ` + : 'This image was spoilered by a complex tag expression in '; + spoilerBlock(img, bannerImage(tagsHit), bannerText); +} + +/* eslint-enable indent */ + +export function filterNode(node: Pick) { + const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags(); + const { hiddenFilter, spoileredFilter } = window.booru; + + // Image thumb boxes with vote and fave buttons on them + $$('.image-container', node) + .filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped)) + .filter(img => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped)) + .forEach(img => showThumb(img)); + + // Individual image pages and images in posts/comments + $$('.image-show-container', node) + .filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped)) + .filter(img => !run(img, spoileredTags, spoileredFilter, spoilerBlockTyped)) + .forEach(img => showBlock(img)); +} + +export function initImagesClientside() { + window.booru.imagesWithDownvotingDisabled = []; + // This fills the imagesWithDownvotingDisabled array + filterNode(document); + // Once the array is populated, we can initialize interactions + setupInteractions(); +} diff --git a/assets/js/utils/tag.ts b/assets/js/utils/tag.ts index ea104213..26c6bdf9 100644 --- a/assets/js/utils/tag.ts +++ b/assets/js/utils/tag.ts @@ -28,13 +28,13 @@ function sortTags(hidden: boolean, a: TagData, b: TagData): number { return a.spoiler_image_uri ? -1 : 1; } -export function getHiddenTags() { +export function getHiddenTags(): TagData[] { return unique(window.booru.hiddenTagList) .map(tagId => getTag(tagId)) .sort(sortTags.bind(null, true)); } -export function getSpoileredTags() { +export function getSpoileredTags(): TagData[] { if (window.booru.spoilerType === 'off') return []; return unique(window.booru.spoileredTagList) From 9f031725acc80198d556cdd920c05d164258e617 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 16 Jun 2024 09:13:25 -0400 Subject: [PATCH 26/44] Fix resource name for fp ban controller --- .../controllers/admin/fingerprint_ban_controller.ex | 13 +++++++++---- .../templates/admin/fingerprint_ban/edit.html.slime | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex index 3ae57571..4e520c33 100644 --- a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex @@ -7,7 +7,12 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do import Ecto.Query plug :verify_authorized - plug :load_resource, model: FingerprintBan, only: [:edit, :update, :delete] + + plug :load_resource, + model: FingerprintBan, + as: :fingerprint_ban, + only: [:edit, :update, :delete] + plug :check_can_delete when action in [:delete] def index(conn, %{"q" => q}) when is_binary(q) do @@ -56,12 +61,12 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do end def edit(conn, _params) do - changeset = Bans.change_fingerprint(conn.assigns.fingerprint) + changeset = Bans.change_fingerprint(conn.assigns.fingerprint_ban) render(conn, "edit.html", title: "Editing Fingerprint Ban", changeset: changeset) end def update(conn, %{"fingerprint" => fingerprint_ban_params}) do - case Bans.update_fingerprint(conn.assigns.fingerprint, fingerprint_ban_params) do + case Bans.update_fingerprint(conn.assigns.fingerprint_ban, fingerprint_ban_params) do {:ok, fingerprint_ban} -> conn |> put_flash(:info, "Fingerprint ban successfully updated.") @@ -74,7 +79,7 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do end def delete(conn, _params) do - {:ok, fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint) + {:ok, fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint_ban) conn |> put_flash(:info, "Fingerprint ban successfully deleted.") diff --git a/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime b/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime index eb424776..1f9ff5ff 100644 --- a/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime +++ b/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime @@ -1,6 +1,6 @@ h1 Editing ban -= render PhilomenaWeb.Admin.FingerprintBanView, "_form.html", changeset: @changeset, action: ~p"/admin/fingerprint_bans/#{@fingerprint}", conn: @conn += render PhilomenaWeb.Admin.FingerprintBanView, "_form.html", changeset: @changeset, action: ~p"/admin/fingerprint_bans/#{@fingerprint_ban}", conn: @conn br = link "Back", to: ~p"/admin/fingerprint_bans" From d0771c7216589578794f3e681000ed65aa990c03 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 17 Jun 2024 19:36:41 -0400 Subject: [PATCH 27/44] Add tests for clientside image filtering --- assets/js/__tests__/imagesclientside.spec.ts | 161 +++++++++++++++++++ assets/js/imagesclientside.ts | 11 +- 2 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 assets/js/__tests__/imagesclientside.spec.ts diff --git a/assets/js/__tests__/imagesclientside.spec.ts b/assets/js/__tests__/imagesclientside.spec.ts new file mode 100644 index 00000000..3f3feb88 --- /dev/null +++ b/assets/js/__tests__/imagesclientside.spec.ts @@ -0,0 +1,161 @@ +import { filterNode, initImagesClientside } from '../imagesclientside'; +import { parseSearch } from '../match_query'; +import { matchNone } from '../query/boolean'; +import { assertNotNull } from '../utils/assert'; +import { $ } from '../utils/dom'; + +describe('filterNode', () => { + beforeEach(() => { + window.booru.hiddenTagList = []; + window.booru.spoileredTagList = []; + window.booru.ignoredTagList = []; + window.booru.imagesWithDownvotingDisabled = []; + + window.booru.hiddenFilter = matchNone(); + window.booru.spoileredFilter = matchNone(); + }); + + function makeMediaContainer() { + const element = document.createElement('div'); + element.innerHTML = ` +
+
+ +
+ `; + return [ element, assertNotNull($('.js-spoiler-info-overlay', element)) ]; + } + + it('should show image media boxes not matching any filter', () => { + const [ container, spoilerOverlay ] = makeMediaContainer(); + + filterNode(container); + expect(spoilerOverlay).not.toContainHTML('(Complex Filter)'); + expect(spoilerOverlay).not.toContainHTML('(unknown tag)'); + expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1'); + }); + + it('should spoiler media boxes spoilered by a tag filter', () => { + const [ container, spoilerOverlay ] = makeMediaContainer(); + window.booru.spoileredTagList = [1]; + + filterNode(container); + expect(spoilerOverlay).toContainHTML('(unknown tag)'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + it('should spoiler media boxes spoilered by a complex filter', () => { + const [ container, spoilerOverlay ] = makeMediaContainer(); + window.booru.spoileredFilter = parseSearch('id:1'); + + filterNode(container); + expect(spoilerOverlay).toContainHTML('(Complex Filter)'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + it('should hide media boxes hidden by a tag filter', () => { + const [ container, spoilerOverlay ] = makeMediaContainer(); + window.booru.hiddenTagList = [1]; + + filterNode(container); + expect(spoilerOverlay).toContainHTML('[HIDDEN]'); + expect(spoilerOverlay).toContainHTML('(unknown tag)'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + it('should hide media boxes hidden by a complex filter', () => { + const [ container, spoilerOverlay ] = makeMediaContainer(); + window.booru.hiddenFilter = parseSearch('id:1'); + + filterNode(container); + expect(spoilerOverlay).toContainHTML('[HIDDEN]'); + expect(spoilerOverlay).toContainHTML('(Complex Filter)'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + function makeImageBlock(): HTMLElement[] { + const element = document.createElement('div'); + element.innerHTML = ` +
+ + +
+ `; + return [ + element, + assertNotNull($('.image-filtered', element)), + assertNotNull($('.image-show', element)), + assertNotNull($('.filter-explanation', element)) + ]; + } + + it('should show image blocks not matching any filter', () => { + const [ container, imageFiltered, imageShow ] = makeImageBlock(); + + filterNode(container); + expect(imageFiltered).toHaveClass('hidden'); + expect(imageShow).not.toHaveClass('hidden'); + expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1'); + }); + + it('should spoiler image blocks spoilered by a tag filter', () => { + const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + window.booru.spoileredTagList = [1]; + + filterNode(container); + expect(imageFiltered).not.toHaveClass('hidden'); + expect(imageShow).toHaveClass('hidden'); + expect(filterExplanation).toContainHTML('spoilered by'); + expect(filterExplanation).toContainHTML('(unknown tag)'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + it('should spoiler image blocks spoilered by a complex filter', () => { + const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + window.booru.spoileredFilter = parseSearch('id:1'); + + filterNode(container); + expect(imageFiltered).not.toHaveClass('hidden'); + expect(imageShow).toHaveClass('hidden'); + expect(filterExplanation).toContainHTML('spoilered by'); + expect(filterExplanation).toContainHTML('complex tag expression'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + it('should hide image blocks hidden by a tag filter', () => { + const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + window.booru.hiddenTagList = [1]; + + filterNode(container); + expect(imageFiltered).not.toHaveClass('hidden'); + expect(imageShow).toHaveClass('hidden'); + expect(filterExplanation).toContainHTML('hidden by'); + expect(filterExplanation).toContainHTML('(unknown tag)'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + + it('should hide image blocks hidden by a complex filter', () => { + const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + window.booru.hiddenFilter = parseSearch('id:1'); + + filterNode(container); + expect(imageFiltered).not.toHaveClass('hidden'); + expect(imageShow).toHaveClass('hidden'); + expect(filterExplanation).toContainHTML('hidden by'); + expect(filterExplanation).toContainHTML('complex tag expression'); + expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); + }); + +}); + +describe('initImagesClientside', () => { + it('should initialize the imagesWithDownvotingDisabled array', () => { + initImagesClientside(); + expect(window.booru.imagesWithDownvotingDisabled).toEqual([]); + }); +}); diff --git a/assets/js/imagesclientside.ts b/assets/js/imagesclientside.ts index 61e1d1ac..add108c9 100644 --- a/assets/js/imagesclientside.ts +++ b/assets/js/imagesclientside.ts @@ -2,6 +2,7 @@ * Client-side image filtering/spoilering. */ +import { assertNotUndefined } from './utils/assert'; import { $$, escapeHtml } from './utils/dom'; import { setupInteractions } from './interactions'; import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image'; @@ -15,20 +16,20 @@ function run( img: HTMLDivElement, tags: TagData[], complex: AstMatcher, - callback: RunCallback, + runCallback: RunCallback ): boolean { const hit = (() => { // Check tags array first to provide more precise filter explanations const hitTags = imageHitsTags(img, tags); if (hitTags.length !== 0) { - callback(img, hitTags, 'tags'); + runCallback(img, hitTags, 'tags'); return true; } // No tags matched, try complex filter AST const hitComplex = imageHitsComplex(img, complex); if (hitComplex) { - callback(img, hitTags, 'complex'); + runCallback(img, hitTags, 'complex'); return true; } @@ -36,9 +37,9 @@ function run( return false; })(); - if (hit && img.dataset.imageId) { + if (hit) { // Disallow negative interaction on image which is not visible - window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId); + window.booru.imagesWithDownvotingDisabled.push(assertNotUndefined(img.dataset.imageId)); } return hit; From 16e15e9c67109f6fa778b1553f0b347b7788bb14 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 13:18:12 -0400 Subject: [PATCH 28/44] Add extra type and callback documentation --- lib/philomena/comments/query.ex | 2 +- lib/philomena/filters/query.ex | 2 +- lib/philomena/galleries/query.ex | 2 +- lib/philomena/images/query.ex | 2 +- lib/philomena/posts/query.ex | 2 +- lib/philomena/reports/query.ex | 2 +- lib/philomena/tags/query.ex | 2 +- lib/philomena_media/analyzers/analyzer.ex | 3 ++ lib/philomena_media/intensities.ex | 2 +- lib/philomena_media/processors.ex | 18 ++++++------ lib/philomena_media/processors/processor.ex | 20 +++++++++---- lib/philomena_proxy/scrapers.ex | 6 ++-- lib/philomena_query/batch.ex | 18 ++++++++++++ lib/philomena_query/parse/parser.ex | 28 +++++++++++++++++-- lib/philomena_query/search.ex | 26 +++++++++++++++++ lib/philomena_query/search_index.ex | 31 ++++++++++++++++++--- 16 files changed, 134 insertions(+), 32 deletions(-) diff --git a/lib/philomena/comments/query.ex b/lib/philomena/comments/query.ex index 6b2bea42..9e9c8986 100644 --- a/lib/philomena/comments/query.ex +++ b/lib/philomena/comments/query.ex @@ -88,7 +88,7 @@ defmodule Philomena.Comments.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/filters/query.ex b/lib/philomena/filters/query.ex index adf53b09..3b6bb3ef 100644 --- a/lib/philomena/filters/query.ex +++ b/lib/philomena/filters/query.ex @@ -29,7 +29,7 @@ defmodule Philomena.Filters.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/galleries/query.ex b/lib/philomena/galleries/query.ex index 9baad469..e04ceecc 100644 --- a/lib/philomena/galleries/query.ex +++ b/lib/philomena/galleries/query.ex @@ -18,7 +18,7 @@ defmodule Philomena.Galleries.Query do query_string = query_string || "" fields() - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string) end end diff --git a/lib/philomena/images/query.ex b/lib/philomena/images/query.ex index 575c6e71..9eedcd74 100644 --- a/lib/philomena/images/query.ex +++ b/lib/philomena/images/query.ex @@ -140,7 +140,7 @@ defmodule Philomena.Images.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/posts/query.ex b/lib/philomena/posts/query.ex index 27773776..331655c7 100644 --- a/lib/philomena/posts/query.ex +++ b/lib/philomena/posts/query.ex @@ -86,7 +86,7 @@ defmodule Philomena.Posts.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/reports/query.ex b/lib/philomena/reports/query.ex index d5adc2cc..c9d9be44 100644 --- a/lib/philomena/reports/query.ex +++ b/lib/philomena/reports/query.ex @@ -16,7 +16,7 @@ defmodule Philomena.Reports.Query do def compile(query_string) do fields() - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string || "", %{}) end end diff --git a/lib/philomena/tags/query.ex b/lib/philomena/tags/query.ex index 5bfd2126..da148da4 100644 --- a/lib/philomena/tags/query.ex +++ b/lib/philomena/tags/query.ex @@ -19,7 +19,7 @@ defmodule Philomena.Tags.Query do def compile(query_string) do fields() - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string || "") end end diff --git a/lib/philomena_media/analyzers/analyzer.ex b/lib/philomena_media/analyzers/analyzer.ex index cf3b28ec..c96f0005 100644 --- a/lib/philomena_media/analyzers/analyzer.ex +++ b/lib/philomena_media/analyzers/analyzer.ex @@ -1,5 +1,8 @@ defmodule PhilomenaMedia.Analyzers.Analyzer do @moduledoc false + @doc """ + Generate a `m:PhilomenaMedia.Analyzers.Result` for file at the given path. + """ @callback analyze(Path.t()) :: PhilomenaMedia.Analyzers.Result.t() end diff --git a/lib/philomena_media/intensities.ex b/lib/philomena_media/intensities.ex index 5abd71e0..ea095295 100644 --- a/lib/philomena_media/intensities.ex +++ b/lib/philomena_media/intensities.ex @@ -36,7 +36,7 @@ defmodule PhilomenaMedia.Intensities do > #### Info {: .info} > - > Clients should prefer to use `m:PhilomenaMedia.Processors.intensities/2`, as it handles + > Clients should prefer to use `PhilomenaMedia.Processors.intensities/2`, as it handles > media files of any type supported by this library, not just PNG or JPEG. ## Examples diff --git a/lib/philomena_media/processors.ex b/lib/philomena_media/processors.ex index 23c49dcf..22ce613b 100644 --- a/lib/philomena_media/processors.ex +++ b/lib/philomena_media/processors.ex @@ -62,29 +62,31 @@ defmodule PhilomenaMedia.Processors do alias PhilomenaMedia.Processors.{Gif, Jpeg, Png, Svg, Webm} alias PhilomenaMedia.Mime - # The name of a version, like :large + @typedoc "The name of a version, like `:large`." @type version_name :: atom() @type dimensions :: {integer(), integer()} @type version_list :: [{version_name(), dimensions()}] - # The file name of a processed version, like "large.png" + @typedoc "The file name of a processed version, like `large.png`." @type version_filename :: String.t() - # A single file to be copied to satisfy a request for a version name + @typedoc "A single file to be copied to satisfy a request for a version name." @type copy_request :: {:copy, Path.t(), version_filename()} - # A list of thumbnail versions to copy into place + @typedoc "A list of thumbnail versions to copy into place." @type thumbnails :: {:thumbnails, [copy_request()]} - # Replace the original file to strip metadata or losslessly optimize + @typedoc "Replace the original file to strip metadata or losslessly optimize." @type replace_original :: {:replace_original, Path.t()} - # Apply the computed corner intensities + @typedoc "Apply the computed corner intensities." @type intensities :: {:intensities, Intensities.t()} - # An edit script, representing the changes to apply to the storage backend - # after successful processing + @typedoc """ + An edit script, representing the changes to apply to the storage backend + after successful processing. + """ @type edit_script :: [thumbnails() | replace_original() | intensities()] @doc """ diff --git a/lib/philomena_media/processors/processor.ex b/lib/philomena_media/processors/processor.ex index 2c3acc0b..8b9f568f 100644 --- a/lib/philomena_media/processors/processor.ex +++ b/lib/philomena_media/processors/processor.ex @@ -5,17 +5,25 @@ defmodule PhilomenaMedia.Processors.Processor do alias PhilomenaMedia.Processors alias PhilomenaMedia.Intensities - # Generate a list of version filenames for the given version list. + @doc """ + Generate a list of version filenames for the given version list. + """ @callback versions(Processors.version_list()) :: [Processors.version_filename()] - # Process the media at the given path against the given version list, and return an - # edit script with the resulting files + @doc """ + Process the media at the given path against the given version list, and return an + edit script with the resulting files. + """ @callback process(Result.t(), Path.t(), Processors.version_list()) :: Processors.edit_script() - # Perform post-processing optimization tasks on the file, to reduce its size - # and strip non-essential metadata + @doc """ + Perform post-processing optimization tasks on the file, to reduce its size + and strip non-essential metadata. + """ @callback post_process(Result.t(), Path.t()) :: Processors.edit_script() - # Generate corner intensities for the given path + @doc """ + Generate corner intensities for the given path. + """ @callback intensities(Result.t(), Path.t()) :: Intensities.t() end diff --git a/lib/philomena_proxy/scrapers.ex b/lib/philomena_proxy/scrapers.ex index 67711045..a96f0817 100644 --- a/lib/philomena_proxy/scrapers.ex +++ b/lib/philomena_proxy/scrapers.ex @@ -3,16 +3,16 @@ defmodule PhilomenaProxy.Scrapers do Scrape utilities to facilitate uploading media from other websites. """ - # The URL to fetch, as a string. + @typedoc "The URL to fetch, as a string." @type url :: String.t() - # An individual image in a list associated with a scrape result. + @typedoc "An individual image in a list associated with a scrape result." @type image_result :: %{ url: url(), camo_url: url() } - # Result of a successful scrape. + @typedoc "Result of a successful scrape." @type scrape_result :: %{ source_url: url(), description: String.t() | nil, diff --git a/lib/philomena_query/batch.ex b/lib/philomena_query/batch.ex index ce78cb6a..676b436a 100644 --- a/lib/philomena_query/batch.ex +++ b/lib/philomena_query/batch.ex @@ -13,13 +13,31 @@ defmodule PhilomenaQuery.Batch do alias Philomena.Repo import Ecto.Query + @typedoc """ + Represents an object which may be operated on via `m:Ecto.Query`. + + This could be a schema object (e.g. `m:Philomena.Images.Image`) or a fully formed query + `from i in Image, where: i.hidden_from_users == false`. + """ @type queryable :: any() @type batch_size :: {:batch_size, integer()} @type id_field :: {:id_field, atom()} @type batch_options :: [batch_size() | id_field()] + @typedoc """ + The callback for `record_batches/3`. + + Takes a list of schema structs which were returned in the batch. Return value is ignored. + """ @type record_batch_callback :: ([struct()] -> any()) + + @typedoc """ + The callback for `query_batches/3`. + + Takes an `m:Ecto.Query` that can be processed with `m:Philomena.Repo` query commmands, such + as `Philomena.Repo.update_all/3` or `Philomena.Repo.delete_all/2`. Return value is ignored. + """ @type query_batch_callback :: ([Ecto.Query.t()] -> any()) @doc """ diff --git a/lib/philomena_query/parse/parser.ex b/lib/philomena_query/parse/parser.ex index ba4b0597..a9d40222 100644 --- a/lib/philomena_query/parse/parser.ex +++ b/lib/philomena_query/parse/parser.ex @@ -41,12 +41,34 @@ defmodule PhilomenaQuery.Parse.Parser do TermRangeParser } + @typedoc """ + User-supplied context argument. + + Provided to `parse/3` and passed to the transform callback. + """ @type context :: any() + + @typedoc "Query in the search engine JSON query language." @type query :: map() + @typedoc "Whether the default field is `:term` (not analyzed) or `:ngram` (analyzed)." @type default_field_type :: :term | :ngram + @typedoc """ + Return value of the transform callback. + + On `{:ok, query}`, the query is incorporated into the parse tree at the current location. + On `{:error, error}`, parsing immediately stops and the error is returned from the parser. + """ @type transform_result :: {:ok, query()} | {:error, String.t()} + + @typedoc """ + Type of the transform callback. + + The transform callback receives the context argument passed to `parse/3` and the remainder of + the term. For instance `my:example` would match a transform rule with the key `"my"`, and + the remainder passed to the callback would be `"example"`. + """ @type transform :: (context, String.t() -> transform_result()) @type t :: %__MODULE__{ @@ -112,11 +134,11 @@ defmodule PhilomenaQuery.Parse.Parser do aliases: %{"hidden" => "hidden_from_users"} ] - Parser.parser(options) + Parser.new(options) """ - @spec parser(keyword()) :: t() - def parser(options) do + @spec new(keyword()) :: t() + def new(options) do parser = struct(Parser, options) fields = diff --git a/lib/philomena_query/search.ex b/lib/philomena_query/search.ex index b4960657..140adf67 100644 --- a/lib/philomena_query/search.ex +++ b/lib/philomena_query/search.ex @@ -18,10 +18,36 @@ defmodule PhilomenaQuery.Search do # todo: fetch through compile_env? @policy Philomena.SearchPolicy + @typedoc """ + Any schema module which has an associated search index. See the policy module + for more information. + """ @type schema_module :: @policy.schema_module() + + @typedoc """ + Represents an object which may be operated on via `m:Ecto.Query`. + + This could be a schema object (e.g. `m:Philomena.Images.Image`) or a fully formed query + `from i in Image, where: i.hidden_from_users == false`. + """ @type queryable :: any() + + @typedoc """ + A query body, as deliverable to any index's `_search` endpoint. + + See the query DSL documentation for additional information: + https://opensearch.org/docs/latest/query-dsl/ + """ @type query_body :: map() + @typedoc """ + Given a term at the given path, replace the old term with the new term. + + `path` is a list of names to be followed to find the old term. For example, + a document containing `{"condiments": "dijon"}` would permit `["condiments"]` + as the path, and a document containing `{"namespaced_tags": {"name": ["old"]}}` + would permit `["namespaced_tags", "name"]` as the path. + """ @type replacement :: %{ path: [String.t()], old: term(), diff --git a/lib/philomena_query/search_index.ex b/lib/philomena_query/search_index.ex index 3a4fe9da..119d2613 100644 --- a/lib/philomena_query/search_index.ex +++ b/lib/philomena_query/search_index.ex @@ -1,11 +1,34 @@ defmodule PhilomenaQuery.SearchIndex do - # Returns the index name for the index. - # This is usually a collection name like "images". + @moduledoc """ + Behaviour module for schemas with search indexing. + """ + + @doc """ + Returns the index name for the index. + + This is usually a collection name like "images". + + See https://opensearch.org/docs/latest/api-reference/index-apis/create-index/ for + reference on index naming restrictions. + """ @callback index_name() :: String.t() - # Returns the mapping and settings for the index. + @doc """ + Returns the mapping and settings for the index. + + See https://opensearch.org/docs/latest/api-reference/index-apis/put-mapping/ for + reference on the mapping syntax, and the following pages for which types may be + used in mappings: + - https://opensearch.org/docs/latest/field-types/ + - https://opensearch.org/docs/latest/analyzers/index-analyzers/ + """ @callback mapping() :: map() - # Returns the JSON representation of the given struct for indexing in OpenSearch. + @doc """ + Returns the JSON representation of the given struct for indexing in OpenSearch. + + See https://opensearch.org/docs/latest/api-reference/document-apis/index-document/ for + reference on how this value is used. + """ @callback as_json(struct()) :: map() end From 7df6ef3ab592202b594df02704a3487e29b43fb2 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 13:21:06 -0400 Subject: [PATCH 29/44] Add ex_doc for documentation generation --- mix.exs | 4 +++- mix.lock | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 41e15a6f..f9cded72 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,8 @@ defmodule Philomena.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), - dialyzer: [plt_add_apps: [:mix]] + dialyzer: [plt_add_apps: [:mix]], + docs: [formatters: ["html"]] ] end @@ -85,6 +86,7 @@ defmodule Philomena.MixProject do {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:credo_envvar, "~> 0.1", only: [:dev, :test], runtime: false}, {:credo_naming, "~> 2.0", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.30", only: [:dev], runtime: false}, # Security checks {:sobelow, "~> 0.11", only: [:dev, :test], runtime: true}, diff --git a/mix.lock b/mix.lock index f8e68868..46729250 100644 --- a/mix.lock +++ b/mix.lock @@ -17,6 +17,7 @@ "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto_network": {:hex, :ecto_network, "1.5.0", "a930c910975e7a91237b858ebf0f4ad7b2aae32fa846275aa203cb858459ec73", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "4d614434ae3e6d373a2f693d56aafaa3f3349714668ffd6d24e760caf578aa2f"}, "ecto_sql": {:hex, :ecto_sql, "3.11.2", "c7cc7f812af571e50b80294dc2e535821b3b795ce8008d07aa5f336591a185a8", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "73c07f995ac17dbf89d3cfaaf688fcefabcd18b7b004ac63b0dc4ef39499ed6b"}, @@ -26,6 +27,7 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_aws": {:git, "https://github.com/liamwhite/ex_aws.git", "a340859dd8ac4d63bd7a3948f0994e493e49bda4", [ref: "a340859dd8ac4d63bd7a3948f0994e493e49bda4"]}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, + "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "exq": {:hex, :exq, "0.19.0", "06eb92944dad39f0954dc8f63190d3e24d11734eef88cf5800883e57ebf74f3c", [:mix], [{:elixir_uuid, ">= 1.2.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0 and < 6.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:redix, ">= 0.9.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "24fc0ebdd87cc7406e1034fb46c2419f9c8a362f0ec634d23b6b819514d36390"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, @@ -37,6 +39,9 @@ "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, From 31ae13fe282f422b3340cff080b5b9cf3ef9314b Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 13:46:16 -0400 Subject: [PATCH 30/44] Add typos workflow --- .github/workflows/elixir.yml | 8 ++++++++ .typos.toml | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .typos.toml diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index dfdfab46..95269dbc 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -27,6 +27,14 @@ jobs: run: | docker compose run app mix sobelow --config docker compose run app mix deps.audit + + typos: + name: 'Check for spelling errors' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master + lint-and-test: name: 'JavaScript Linting and Unit Tests' runs-on: ubuntu-latest diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..898ad2e4 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,10 @@ +[default] +extend-ignore-re = [ + # Ignore development secret key. Production secret key should + # be in environment files and not checked into source control. + ".*secret_key_base.*", + + # Key constraints with encoded names + "fk_rails_[a-f0-9]+" +] + From ee02fa131df839a46c869b7105b66e3da2156723 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 13:53:42 -0400 Subject: [PATCH 31/44] Fix typos --- .../js/utils/__tests__/local-autocompleter.spec.ts | 12 +++++++----- lib/philomena/tags.ex | 2 +- lib/philomena/users/user_notifier.ex | 2 +- .../controllers/admin/site_notice_controller.ex | 4 ++-- .../templates/conversation/show.html.slime | 2 +- lib/philomena_web/templates/filter/_form.html.slime | 2 +- .../templates/topic/poll/_form.html.slime | 6 +++--- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/assets/js/utils/__tests__/local-autocompleter.spec.ts b/assets/js/utils/__tests__/local-autocompleter.spec.ts index bc612a08..182e1308 100644 --- a/assets/js/utils/__tests__/local-autocompleter.spec.ts +++ b/assets/js/utils/__tests__/local-autocompleter.spec.ts @@ -28,12 +28,12 @@ describe('Local Autocompleter', () => { }); describe('instantiation', () => { - it('should be constructable with compatible data', () => { + it('should be constructible with compatible data', () => { const result = new LocalAutocompleter(mockData); expect(result).toBeInstanceOf(LocalAutocompleter); }); - it('should NOT be constructable with incompatible data', () => { + it('should NOT be constructible with incompatible data', () => { const versionDataOffset = 12; const mockIncompatibleDataArray = new Array(versionDataOffset).fill(0); // Set data version to 1 @@ -45,6 +45,8 @@ describe('Local Autocompleter', () => { }); describe('topK', () => { + const termStem = ['f', 'o'].join(''); + let localAc: LocalAutocompleter; beforeAll(() => { @@ -66,7 +68,7 @@ describe('Local Autocompleter', () => { }); it('should return suggestions sorted by image count', () => { - const result = localAc.topK('fo', defaultK); + const result = localAc.topK(termStem, defaultK); expect(result).toEqual([ expect.objectContaining({ name: 'forest', imageCount: 3 }), expect.objectContaining({ name: 'fog', imageCount: 1 }), @@ -82,13 +84,13 @@ describe('Local Autocompleter', () => { }); it('should return only the required number of suggestions', () => { - const result = localAc.topK('fo', 1); + const result = localAc.topK(termStem, 1); expect(result).toEqual([expect.objectContaining({ name: 'forest', imageCount: 3 })]); }); it('should NOT return suggestions associated with hidden tags', () => { window.booru.hiddenTagList = [1]; - const result = localAc.topK('fo', defaultK); + const result = localAc.topK(termStem, defaultK); expect(result).toEqual([]); }); diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 1f6e80c6..de7a0171 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -251,7 +251,7 @@ defmodule Philomena.Tags do |> where(tag_id: ^tag.id) |> Repo.delete_all() - # Update other assocations + # Update other associations ArtistLink |> where(tag_id: ^tag.id) |> Repo.update_all(set: [tag_id: target_tag.id]) diff --git a/lib/philomena/users/user_notifier.ex b/lib/philomena/users/user_notifier.ex index 48083321..2cd85d86 100644 --- a/lib/philomena/users/user_notifier.ex +++ b/lib/philomena/users/user_notifier.ex @@ -88,7 +88,7 @@ defmodule Philomena.Users.UserNotifier do Your account has been automatically locked due to too many attempts to sign in. - You can unlock your account by visting the URL below: + You can unlock your account by visiting the URL below: #{url} diff --git a/lib/philomena_web/controllers/admin/site_notice_controller.ex b/lib/philomena_web/controllers/admin/site_notice_controller.ex index 7612422e..ff284a75 100644 --- a/lib/philomena_web/controllers/admin/site_notice_controller.ex +++ b/lib/philomena_web/controllers/admin/site_notice_controller.ex @@ -44,7 +44,7 @@ defmodule PhilomenaWeb.Admin.SiteNoticeController do case SiteNotices.update_site_notice(conn.assigns.site_notice, site_notice_params) do {:ok, _site_notice} -> conn - |> put_flash(:info, "Succesfully updated site notice.") + |> put_flash(:info, "Successfully updated site notice.") |> redirect(to: ~p"/admin/site_notices") {:error, changeset} -> @@ -56,7 +56,7 @@ defmodule PhilomenaWeb.Admin.SiteNoticeController do {:ok, _site_notice} = SiteNotices.delete_site_notice(conn.assigns.site_notice) conn - |> put_flash(:info, "Sucessfully deleted site notice.") + |> put_flash(:info, "Successfully deleted site notice.") |> redirect(to: ~p"/admin/site_notices") end diff --git a/lib/philomena_web/templates/conversation/show.html.slime b/lib/philomena_web/templates/conversation/show.html.slime index 5af5ead9..85bf0299 100644 --- a/lib/philomena_web/templates/conversation/show.html.slime +++ b/lib/philomena_web/templates/conversation/show.html.slime @@ -59,5 +59,5 @@ h1 = @conversation.title p You've managed to send over 1,000 messages in this conversation! p We'd like to ask you to make a new conversation. Don't worry, this one won't go anywhere if you need to refer back to it. p - => link "Click here", to: ~p"/conversations/new?#{[receipient: other.name]}" + => link "Click here", to: ~p"/conversations/new?#{[recipient: other.name]}" ' to make a new conversation with this user. diff --git a/lib/philomena_web/templates/filter/_form.html.slime b/lib/philomena_web/templates/filter/_form.html.slime index 77965906..e46cda9a 100644 --- a/lib/philomena_web/templates/filter/_form.html.slime +++ b/lib/philomena_web/templates/filter/_form.html.slime @@ -73,7 +73,7 @@ .fieldlabel strong You probably do not want to check this unless you know what you are doing - it cannot be changed later - | . Pulic filters can be shared with other users and used by them; if you make changes to a filter, it will update all users of that filter. + | . Public filters can be shared with other users and used by them; if you make changes to a filter, it will update all users of that filter. - input_value(f, :public) == true -> .fieldlabel diff --git a/lib/philomena_web/templates/topic/poll/_form.html.slime b/lib/philomena_web/templates/topic/poll/_form.html.slime index c1ba66cf..f5d960b0 100644 --- a/lib/philomena_web/templates/topic/poll/_form.html.slime +++ b/lib/philomena_web/templates/topic/poll/_form.html.slime @@ -29,10 +29,10 @@ p.fieldlabel = select @f, :vote_method, ["-": "", "Single option": :single, "Multiple options": :multiple], class: "input" = error_tag @f, :vote_method -= inputs_for @f, :options, fn fo -> += inputs_for @f, :options, fn opt -> .field.js-poll-option.field--inline.flex--no-wrap.flex--centered - = text_input fo, :label, class: "input flex__grow js-option-label", placeholder: "Option" - = error_tag fo, :label + = text_input opt, :label, class: "input flex__grow js-option-label", placeholder: "Option" + = error_tag opt, :label label.input--separate-left.flex__fixed.flex--centered a.js-option-remove href="#" From 1558fb0a3046079d3079f4eb0b850bafbeda0a04 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 18:51:49 -0400 Subject: [PATCH 32/44] Caching --- .github/workflows/elixir.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 95269dbc..aed4c099 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -14,8 +14,19 @@ jobs: with: path: | _build + .cargo deps - key: ${{ runner.os }}-build-deps-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} + + - name: Enable caching + run: | + # Disable volumes so caching can take effect + sed -i -Ee 's/- app_[a-z]+_data:.*$//g' docker-compose.yml + + # Make ourselves the owner + echo "RUN addgroup -g $(id -g) -S appgroup && adduser -u $(id -u) -S appuser -G appgroup" >> docker/app/Dockerfile + echo "USER appuser" >> docker/app/Dockerfile + echo "RUN mix local.hex --force && mix local.rebar --force" >> docker/app/Dockerfile - run: docker compose pull - run: docker compose build From 2f887035c69e413618c5449419dce0458676483d Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 19:45:35 -0400 Subject: [PATCH 33/44] Add dialyzer run --- .github/workflows/elixir.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index aed4c099..25558653 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -16,7 +16,7 @@ jobs: _build .cargo deps - key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-deps-2-${{ hashFiles('mix.lock') }} - name: Enable caching run: | @@ -39,6 +39,10 @@ jobs: docker compose run app mix sobelow --config docker compose run app mix deps.audit + - name: Dialyzer + run: | + docker compose run app mix dialyzer + typos: name: 'Check for spelling errors' runs-on: ubuntu-latest From 74e61640c8412149ad4612bb6f1e2dc972b968ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 02:23:40 +0000 Subject: [PATCH 34/44] Bump ws from 8.17.0 to 8.17.1 in /assets Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.17.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.0...8.17.1) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] --- assets/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index ea901f29..bb2b24cd 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -4997,9 +4997,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, From 868c7dac254d231425e46c3ce0fefd7b6baa073b Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 18 Jun 2024 23:22:49 -0400 Subject: [PATCH 35/44] Fix typo --- lib/philomena_query/batch.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/philomena_query/batch.ex b/lib/philomena_query/batch.ex index 676b436a..918a3b5e 100644 --- a/lib/philomena_query/batch.ex +++ b/lib/philomena_query/batch.ex @@ -35,7 +35,7 @@ defmodule PhilomenaQuery.Batch do @typedoc """ The callback for `query_batches/3`. - Takes an `m:Ecto.Query` that can be processed with `m:Philomena.Repo` query commmands, such + Takes an `m:Ecto.Query` that can be processed with `m:Philomena.Repo` query commands, such as `Philomena.Repo.update_all/3` or `Philomena.Repo.delete_all/2`. Return value is ignored. """ @type query_batch_callback :: ([Ecto.Query.t()] -> any()) From 6170e0b5ddbf636f554ca0109ebe99ba7c5d0c41 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 19 Jun 2024 23:03:44 -0400 Subject: [PATCH 36/44] Replace Tesla with Req --- config/config.exs | 2 - .../artist_links/automatic_verifier.ex | 2 +- lib/philomena/channels/picarto_channel.ex | 2 +- lib/philomena/channels/piczel_channel.ex | 2 +- lib/philomena_proxy/http.ex | 83 +++++++++++-------- lib/philomena_proxy/scrapers/deviantart.ex | 8 +- lib/philomena_proxy/scrapers/pillowfort.ex | 2 +- lib/philomena_proxy/scrapers/raw.ex | 2 +- lib/philomena_proxy/scrapers/tumblr.ex | 4 +- lib/philomena_proxy/scrapers/twitter.ex | 2 +- .../plugs/compromised_password_check_plug.ex | 2 +- lib/philomena_web/plugs/scraper_plug.ex | 2 +- mix.exs | 4 +- mix.lock | 4 +- 14 files changed, 67 insertions(+), 54 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9d943587..fbfedf78 100644 --- a/config/config.exs +++ b/config/config.exs @@ -46,8 +46,6 @@ config :phoenix, :template_engines, slime: PhoenixSlime.Engine, slimleex: PhoenixSlime.LiveViewEngine -config :tesla, adapter: Tesla.Adapter.Mint - # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", diff --git a/lib/philomena/artist_links/automatic_verifier.ex b/lib/philomena/artist_links/automatic_verifier.ex index 57fd8fd2..f2a5bebd 100644 --- a/lib/philomena/artist_links/automatic_verifier.ex +++ b/lib/philomena/artist_links/automatic_verifier.ex @@ -12,7 +12,7 @@ defmodule Philomena.ArtistLinks.AutomaticVerifier do end end - defp contains_verification_code?({:ok, %Tesla.Env{body: body, status: 200}}, code) do + defp contains_verification_code?({:ok, %{body: body, status: 200}}, code) do String.contains?(body, code) end diff --git a/lib/philomena/channels/picarto_channel.ex b/lib/philomena/channels/picarto_channel.ex index a27a3615..1eacb28f 100644 --- a/lib/philomena/channels/picarto_channel.ex +++ b/lib/philomena/channels/picarto_channel.ex @@ -6,7 +6,7 @@ defmodule Philomena.Channels.PicartoChannel do @api_online |> PhilomenaProxy.Http.get() |> case do - {:ok, %Tesla.Env{body: body, status: 200}} -> + {:ok, %{body: body, status: 200}} -> body |> Jason.decode!() |> Map.new(&{&1["name"], fetch(&1, now)}) diff --git a/lib/philomena/channels/piczel_channel.ex b/lib/philomena/channels/piczel_channel.ex index 56da9e34..817dd486 100644 --- a/lib/philomena/channels/piczel_channel.ex +++ b/lib/philomena/channels/piczel_channel.ex @@ -6,7 +6,7 @@ defmodule Philomena.Channels.PiczelChannel do @api_online |> PhilomenaProxy.Http.get() |> case do - {:ok, %Tesla.Env{body: body, status: 200}} -> + {:ok, %{body: body, status: 200}} -> body |> Jason.decode!() |> Map.new(&{&1["slug"], fetch(&1, now)}) diff --git a/lib/philomena_proxy/http.ex b/lib/philomena_proxy/http.ex index 9a5af4ec..70172f0c 100644 --- a/lib/philomena_proxy/http.ex +++ b/lib/philomena_proxy/http.ex @@ -17,9 +17,13 @@ defmodule PhilomenaProxy.Http do @type url :: String.t() @type header_list :: [{String.t(), String.t()}] - @type body :: binary() + @type body :: iodata() + @type result :: {:ok, Req.Response.t()} | {:error, Exception.t()} - @type client_options :: keyword() + @user_agent "Mozilla/5.0 (X11; Philomena; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0" + @max_body 125_000_000 + + @max_body_key :resp_body_size @doc ~S""" Perform a HTTP GET request. @@ -27,15 +31,15 @@ defmodule PhilomenaProxy.Http do ## Example iex> PhilomenaProxy.Http.get("http://example.com", [{"authorization", "Bearer #{token}"}]) - {:ok, %Tesla.Env{...}} + {:ok, %{status: 200, body: ...}} iex> PhilomenaProxy.Http.get("http://nonexistent.example.com") - {:error, %Mint.TransportError{reason: :nxdomain}} + {:error, %Req.TransportError{reason: :nxdomain}} """ - @spec get(url(), header_list(), client_options()) :: Tesla.Env.result() - def get(url, headers \\ [], options \\ []) do - Tesla.get(client(headers), url, opts: [adapter: adapter_opts(options)]) + @spec get(url(), header_list()) :: result() + def get(url, headers \\ []) do + request(:get, url, [], headers) end @doc ~S""" @@ -44,15 +48,15 @@ defmodule PhilomenaProxy.Http do ## Example iex> PhilomenaProxy.Http.head("http://example.com", [{"authorization", "Bearer #{token}"}]) - {:ok, %Tesla.Env{...}} + {:ok, %{status: 200, body: ...}} iex> PhilomenaProxy.Http.head("http://nonexistent.example.com") - {:error, %Mint.TransportError{reason: :nxdomain}} + {:error, %Req.TransportError{reason: :nxdomain}} """ - @spec head(url(), header_list(), client_options()) :: Tesla.Env.result() - def head(url, headers \\ [], options \\ []) do - Tesla.head(client(headers), url, opts: [adapter: adapter_opts(options)]) + @spec head(url(), header_list()) :: result() + def head(url, headers \\ []) do + request(:head, url, [], headers) end @doc ~S""" @@ -61,26 +65,41 @@ defmodule PhilomenaProxy.Http do ## Example iex> PhilomenaProxy.Http.post("http://example.com", "", [{"authorization", "Bearer #{token}"}]) - {:ok, %Tesla.Env{...}} + {:ok, %{status: 200, body: ...}} iex> PhilomenaProxy.Http.post("http://nonexistent.example.com", "") - {:error, %Mint.TransportError{reason: :nxdomain}} + {:error, %Req.TransportError{reason: :nxdomain}} """ - @spec post(url(), body(), header_list(), client_options()) :: Tesla.Env.result() - def post(url, body, headers \\ [], options \\ []) do - Tesla.post(client(headers), url, body, opts: [adapter: adapter_opts(options)]) + @spec post(url(), body(), header_list()) :: result() + def post(url, body, headers \\ []) do + request(:post, url, body, headers) end - defp adapter_opts(opts) do - opts = Keyword.merge(opts, max_body: 125_000_000, inet6: true) + @spec request(atom(), String.t(), iodata(), header_list()) :: result() + defp request(method, url, body, headers) do + Req.new( + method: method, + url: url, + body: body, + headers: [{:user_agent, @user_agent} | headers], + max_redirects: 1, + connect_options: connect_options(), + inet6: true, + into: &stream_response_callback/2, + decode_body: false + ) + |> Req.Request.put_private(@max_body_key, 0) + |> Req.request() + end + defp connect_options do case Application.get_env(:philomena, :proxy_host) do nil -> - opts + [] url -> - Keyword.merge(opts, proxy: proxy_opts(URI.parse(url))) + [proxy: proxy_opts(URI.parse(url))] end end @@ -90,18 +109,14 @@ defmodule PhilomenaProxy.Http do defp proxy_opts(%{host: host, port: port, scheme: "http"}), do: {:http, host, port, [transport_opts: [inet6: true]]} - defp client(headers) do - Tesla.client( - [ - {Tesla.Middleware.FollowRedirects, max_redirects: 1}, - {Tesla.Middleware.Headers, - [ - {"User-Agent", - "Mozilla/5.0 (X11; Philomena; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"} - | headers - ]} - ], - Tesla.Adapter.Mint - ) + defp stream_response_callback({:data, data}, {req, resp}) do + req = update_in(req.private[@max_body_key], &(&1 + byte_size(data))) + resp = update_in(resp.body, &<<&1::binary, data::binary>>) + + if req.private.resp_body_size < @max_body do + {:cont, {req, resp}} + else + {:halt, {req, RuntimeError.exception("body too big")}} + end end end diff --git a/lib/philomena_proxy/scrapers/deviantart.ex b/lib/philomena_proxy/scrapers/deviantart.ex index 10985133..138d67e1 100644 --- a/lib/philomena_proxy/scrapers/deviantart.ex +++ b/lib/philomena_proxy/scrapers/deviantart.ex @@ -38,7 +38,7 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do |> try_old_hires!() end - defp extract_data!({:ok, %Tesla.Env{body: body, status: 200}}) do + defp extract_data!({:ok, %{body: body, status: 200}}) do [image] = Regex.run(@image_regex, body, capture: :all_but_first) [source] = Regex.run(@source_regex, body, capture: :all_but_first) [artist] = Regex.run(@artist_regex, source, capture: :all_but_first) @@ -60,7 +60,7 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do with [domain, object_uuid, object_name] <- Regex.run(@cdnint_regex, image.url, capture: :all_but_first), built_url <- "#{domain}/intermediary/f/#{object_uuid}/#{object_name}", - {:ok, %Tesla.Env{status: 200}} <- PhilomenaProxy.Http.head(built_url) do + {:ok, %{status: 200}} <- PhilomenaProxy.Http.head(built_url) do # This is the high resolution URL. %{ data @@ -120,7 +120,7 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do built_url = "http://orig01.deviantart.net/x_by_x-d#{base36}.png" case PhilomenaProxy.Http.get(built_url) do - {:ok, %Tesla.Env{status: 301, headers: headers}} -> + {:ok, %{status: 301, headers: headers}} -> # Location header provides URL of high res image. {_location, link} = Enum.find(headers, fn {header, _val} -> header == "location" end) @@ -145,7 +145,7 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do defp follow_redirect(url, max_times) do case PhilomenaProxy.Http.get(url) do - {:ok, %Tesla.Env{headers: headers, status: code}} when code in [301, 302] -> + {:ok, %{headers: headers, status: code}} when code in [301, 302] -> location = Enum.find_value(headers, &location_header/1) follow_redirect(location, max_times - 1) diff --git a/lib/philomena_proxy/scrapers/pillowfort.ex b/lib/philomena_proxy/scrapers/pillowfort.ex index 6e083c9c..91c5a90d 100755 --- a/lib/philomena_proxy/scrapers/pillowfort.ex +++ b/lib/philomena_proxy/scrapers/pillowfort.ex @@ -24,7 +24,7 @@ defmodule PhilomenaProxy.Scrapers.Pillowfort do |> process_response!(url) end - defp json!({:ok, %Tesla.Env{body: body, status: 200}}), + defp json!({:ok, %{body: body, status: 200}}), do: Jason.decode!(body) defp process_response!(post_json, url) do diff --git a/lib/philomena_proxy/scrapers/raw.ex b/lib/philomena_proxy/scrapers/raw.ex index ed31d10b..a8c08d97 100644 --- a/lib/philomena_proxy/scrapers/raw.ex +++ b/lib/philomena_proxy/scrapers/raw.ex @@ -12,7 +12,7 @@ defmodule PhilomenaProxy.Scrapers.Raw do def can_handle?(_uri, url) do PhilomenaProxy.Http.head(url) |> case do - {:ok, %Tesla.Env{status: 200, headers: headers}} -> + {:ok, %{status: 200, headers: headers}} -> headers |> Enum.any?(fn {k, v} -> String.downcase(k) == "content-type" and String.downcase(v) in @mime_types diff --git a/lib/philomena_proxy/scrapers/tumblr.ex b/lib/philomena_proxy/scrapers/tumblr.ex index fe648e66..4863fb39 100644 --- a/lib/philomena_proxy/scrapers/tumblr.ex +++ b/lib/philomena_proxy/scrapers/tumblr.ex @@ -37,7 +37,7 @@ defmodule PhilomenaProxy.Scrapers.Tumblr do |> process_response!() end - defp json!({:ok, %Tesla.Env{body: body, status: 200}}), + defp json!({:ok, %{body: body, status: 200}}), do: Jason.decode!(body) defp process_response!(%{"response" => %{"posts" => [post | _rest]}}), @@ -76,7 +76,7 @@ defmodule PhilomenaProxy.Scrapers.Tumblr do end defp url_ok?(url) do - match?({:ok, %Tesla.Env{status: 200}}, PhilomenaProxy.Http.head(url)) + match?({:ok, %{status: 200}}, PhilomenaProxy.Http.head(url)) end defp add_meta(post, images) do diff --git a/lib/philomena_proxy/scrapers/twitter.ex b/lib/philomena_proxy/scrapers/twitter.ex index def1a374..a3b167f9 100644 --- a/lib/philomena_proxy/scrapers/twitter.ex +++ b/lib/philomena_proxy/scrapers/twitter.ex @@ -18,7 +18,7 @@ defmodule PhilomenaProxy.Scrapers.Twitter do [user, status_id] = Regex.run(@url_regex, url, capture: :all_but_first) api_url = "https://api.fxtwitter.com/#{user}/status/#{status_id}" - {:ok, %Tesla.Env{status: 200, body: body}} = PhilomenaProxy.Http.get(api_url) + {:ok, %{status: 200, body: body}} = PhilomenaProxy.Http.get(api_url) json = Jason.decode!(body) tweet = json["tweet"] diff --git a/lib/philomena_web/plugs/compromised_password_check_plug.ex b/lib/philomena_web/plugs/compromised_password_check_plug.ex index b46e597f..43fe2d4d 100644 --- a/lib/philomena_web/plugs/compromised_password_check_plug.ex +++ b/lib/philomena_web/plugs/compromised_password_check_plug.ex @@ -36,7 +36,7 @@ defmodule PhilomenaWeb.CompromisedPasswordCheckPlug do |> Base.encode16() case PhilomenaProxy.Http.get(make_api_url(prefix)) do - {:ok, %Tesla.Env{body: body, status: 200}} -> String.contains?(body, rest) + {:ok, %{body: body, status: 200}} -> String.contains?(body, rest) _ -> false end end diff --git a/lib/philomena_web/plugs/scraper_plug.ex b/lib/philomena_web/plugs/scraper_plug.ex index 2e4e1769..4694d084 100644 --- a/lib/philomena_web/plugs/scraper_plug.ex +++ b/lib/philomena_web/plugs/scraper_plug.ex @@ -26,7 +26,7 @@ defmodule PhilomenaWeb.ScraperPlug do # Writing the tempfile doesn't allow traversal # sobelow_skip ["Traversal.FileModule"] defp maybe_fixup_params( - {:ok, %Tesla.Env{body: body, status: 200, headers: headers}}, + {:ok, %{body: body, status: 200, headers: headers}}, url, opts, conn diff --git a/mix.exs b/mix.exs index f9cded72..dc6aae6e 100644 --- a/mix.exs +++ b/mix.exs @@ -64,9 +64,7 @@ defmodule Philomena.MixProject do {:redix, "~> 1.2"}, {:remote_ip, "~> 1.1"}, {:briefly, "~> 0.4"}, - {:tesla, "~> 1.5"}, - {:castore, "~> 1.0", override: true}, - {:mint, "~> 1.4"}, + {:req, "~> 0.5"}, {:exq, "~> 0.17"}, {:ex_aws, "~> 2.0", github: "liamwhite/ex_aws", ref: "a340859dd8ac4d63bd7a3948f0994e493e49bda4", override: true}, diff --git a/mix.lock b/mix.lock index 46729250..db8ea4b3 100644 --- a/mix.lock +++ b/mix.lock @@ -31,6 +31,7 @@ "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "exq": {:hex, :exq, "0.19.0", "06eb92944dad39f0954dc8f63190d3e24d11734eef88cf5800883e57ebf74f3c", [:mix], [{:elixir_uuid, ">= 1.2.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0 and < 6.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:redix, ">= 0.9.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "24fc0ebdd87cc7406e1034fb46c2419f9c8a362f0ec634d23b6b819514d36390"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, @@ -51,6 +52,7 @@ "neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], [], "hexpm", "2da322b9b1567ffa0706a7f30f6bbbde70835ae44a1050615f4b4a3d436e0f28"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "pbkdf2": {:git, "https://github.com/basho/erlang-pbkdf2.git", "7e9bd5fcd3cc3062159e4c9214bb628aa6feb5ca", [ref: "7e9bd5fcd3cc3062159e4c9214bb628aa6feb5ca"]}, "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, @@ -72,6 +74,7 @@ "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, "redix": {:hex, :redix, "1.5.1", "a2386971e69bf23630fb3a215a831b5478d2ee7dc9ea7ac811ed89186ab5d7b7", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "85224eb2b683c516b80d472eb89b76067d5866913bf0be59d646f550de71f5c4"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, + "req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"}, "retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"}, "rustler": {:hex, :rustler, "0.33.0", "4a5b0a7a7b0b51549bea49947beff6fae9bc5d5326104dcd4531261e876b5619", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "7c4752728fee59a815ffd20c3429c55b644041f25129b29cdeb5c470b80ec5fd"}, "scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"}, @@ -83,7 +86,6 @@ "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, From 29dc68c7141666a9a4fa4b18020fec33cc885da2 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 19 Jun 2024 23:35:56 -0400 Subject: [PATCH 37/44] Add Req backend for ExAws --- config/runtime.exs | 12 +++--------- lib/philomena_media/req.ex | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 lib/philomena_media/req.ex diff --git a/config/runtime.exs b/config/runtime.exs index 935ba124..190a1da7 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -74,8 +74,7 @@ config :philomena, :s3_primary_options, host: System.fetch_env!("S3_HOST"), port: System.fetch_env!("S3_PORT"), access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"), - secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY"), - http_opts: [timeout: 180_000, recv_timeout: 180_000] + secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY") config :philomena, :s3_primary_bucket, System.fetch_env!("S3_BUCKET") @@ -85,8 +84,7 @@ config :philomena, :s3_secondary_options, host: System.get_env("ALT_S3_HOST"), port: System.get_env("ALT_S3_PORT"), access_key_id: System.get_env("ALT_AWS_ACCESS_KEY_ID"), - secret_access_key: System.get_env("ALT_AWS_SECRET_ACCESS_KEY"), - http_opts: [timeout: 180_000, recv_timeout: 180_000] + secret_access_key: System.get_env("ALT_AWS_SECRET_ACCESS_KEY") config :philomena, :s3_secondary_bucket, System.get_env("ALT_S3_BUCKET") @@ -94,11 +92,7 @@ config :philomena, :s3_secondary_bucket, System.get_env("ALT_S3_BUCKET") config :elastix, httpoison_options: [ssl: [verify: :verify_none]] -config :ex_aws, :hackney_opts, - timeout: 180_000, - recv_timeout: 180_000, - use_default_pool: false, - pool: false +config :ex_aws, http_client: PhilomenaMedia.Req config :ex_aws, :retries, max_attempts: 20, diff --git a/lib/philomena_media/req.ex b/lib/philomena_media/req.ex new file mode 100644 index 00000000..ff92d949 --- /dev/null +++ b/lib/philomena_media/req.ex @@ -0,0 +1,31 @@ +defmodule PhilomenaMedia.Req do + @behaviour ExAws.Request.HttpClient + + @moduledoc """ + Configuration for `m:Req`. + + Options can be set for `m:Req` with the following config: + + config :philomena, :req_opts, + receive_timeout: 30_000 + + The default config handles setting the above. + """ + + @default_opts [receive_timeout: 30_000] + + @impl true + def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do + [method: method, url: url, body: body, headers: headers, decode_body: false] + |> Keyword.merge(Application.get_env(:philomena, :req_opts, @default_opts)) + |> Keyword.merge(http_opts) + |> Req.request() + |> case do + {:ok, %{status: status, headers: headers, body: body}} -> + {:ok, %{status_code: status, headers: headers, body: body}} + + {:error, reason} -> + {:error, %{reason: reason}} + end + end +end From 44c160b905cb7e409b1c0cfc11cd4279cea5d848 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 19 Jun 2024 23:57:00 -0400 Subject: [PATCH 38/44] Remove extremely outdated redirect follower from DA scraper --- lib/philomena_proxy/scrapers/deviantart.ex | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/lib/philomena_proxy/scrapers/deviantart.ex b/lib/philomena_proxy/scrapers/deviantart.ex index 138d67e1..d292e8aa 100644 --- a/lib/philomena_proxy/scrapers/deviantart.ex +++ b/lib/philomena_proxy/scrapers/deviantart.ex @@ -31,7 +31,7 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do @spec scrape(URI.t(), Scrapers.url()) :: Scrapers.scrape_result() def scrape(_uri, url) do url - |> follow_redirect(2) + |> PhilomenaProxy.Http.get() |> extract_data!() |> try_intermediary_hires!() |> try_new_hires!() @@ -139,22 +139,4 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do data end end - - # Workaround for benoitc/hackney#273 - defp follow_redirect(_url, 0), do: nil - - defp follow_redirect(url, max_times) do - case PhilomenaProxy.Http.get(url) do - {:ok, %{headers: headers, status: code}} when code in [301, 302] -> - location = Enum.find_value(headers, &location_header/1) - follow_redirect(location, max_times - 1) - - response -> - response - end - end - - defp location_header({"Location", location}), do: location - defp location_header({"location", location}), do: location - defp location_header(_), do: nil end From a344062d533efebb7f8f51b1d5fa1a8e4edf6117 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 20 Jun 2024 19:22:22 -0400 Subject: [PATCH 39/44] Update response header usages for list format --- lib/philomena_proxy/scrapers/deviantart.ex | 34 --------------------- lib/philomena_proxy/scrapers/raw.ex | 12 +++----- lib/philomena_web/plugs/scraper_plug.ex | 35 ++++++++++------------ 3 files changed, 19 insertions(+), 62 deletions(-) diff --git a/lib/philomena_proxy/scrapers/deviantart.ex b/lib/philomena_proxy/scrapers/deviantart.ex index d292e8aa..cf8009d0 100644 --- a/lib/philomena_proxy/scrapers/deviantart.ex +++ b/lib/philomena_proxy/scrapers/deviantart.ex @@ -9,7 +9,6 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do @image_regex ~r|data-rh="true" rel="preload" href="([^"]*)" as="image"| @source_regex ~r|rel="canonical" href="([^"]*)"| @artist_regex ~r|https://www.deviantart.com/([^/]*)/art| - @serial_regex ~r|https://www.deviantart.com/(?:.*?)-(\d+)\z| @cdnint_regex ~r|(https://images-wixmp-[0-9a-f]+.wixmp.com)(?:/intermediary)?/f/([^/]*)/([^/?]*)| @png_regex ~r|(https://[0-9a-z\-\.]+(?:/intermediary)?/f/[0-9a-f\-]+/[0-9a-z\-]+\.png/v1/fill/[0-9a-z_,]+/[0-9a-z_\-]+)(\.png)(.*)| @jpg_regex ~r|(https://[0-9a-z\-\.]+(?:/intermediary)?/f/[0-9a-f\-]+/[0-9a-z\-]+\.jpg/v1/fill/w_[0-9]+,h_[0-9]+,q_)([0-9]+)(,[a-z]+\/[a-z0-6_\-]+\.jpe?g.*)| @@ -35,7 +34,6 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do |> extract_data!() |> try_intermediary_hires!() |> try_new_hires!() - |> try_old_hires!() end defp extract_data!({:ok, %{body: body, status: 200}}) do @@ -107,36 +105,4 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do data end end - - defp try_old_hires!(%{source_url: source, images: [image]} = data) do - [serial] = Regex.run(@serial_regex, source, capture: :all_but_first) - - base36 = - serial - |> String.to_integer() - |> Integer.to_string(36) - |> String.downcase() - - built_url = "http://orig01.deviantart.net/x_by_x-d#{base36}.png" - - case PhilomenaProxy.Http.get(built_url) do - {:ok, %{status: 301, headers: headers}} -> - # Location header provides URL of high res image. - {_location, link} = Enum.find(headers, fn {header, _val} -> header == "location" end) - - %{ - data - | images: [ - %{ - url: link, - camo_url: image.camo_url - } - ] - } - - _ -> - # Nothing to be found here, move along... - data - end - end end diff --git a/lib/philomena_proxy/scrapers/raw.ex b/lib/philomena_proxy/scrapers/raw.ex index a8c08d97..a6985444 100644 --- a/lib/philomena_proxy/scrapers/raw.ex +++ b/lib/philomena_proxy/scrapers/raw.ex @@ -10,14 +10,10 @@ defmodule PhilomenaProxy.Scrapers.Raw do @spec can_handle?(URI.t(), String.t()) :: boolean() def can_handle?(_uri, url) do - PhilomenaProxy.Http.head(url) - |> case do - {:ok, %{status: 200, headers: headers}} -> - headers - |> Enum.any?(fn {k, v} -> - String.downcase(k) == "content-type" and String.downcase(v) in @mime_types - end) - + with {:ok, %{status: 200, headers: headers}} <- PhilomenaProxy.Http.head(url), + [type | _] <- headers["content-type"] do + String.downcase(type) in @mime_types + else _ -> false end diff --git a/lib/philomena_web/plugs/scraper_plug.ex b/lib/philomena_web/plugs/scraper_plug.ex index 4694d084..c8064d69 100644 --- a/lib/philomena_web/plugs/scraper_plug.ex +++ b/lib/philomena_web/plugs/scraper_plug.ex @@ -1,10 +1,12 @@ defmodule PhilomenaWeb.ScraperPlug do @filename_regex ~r/filename="([^"]+)"/ + @spec init(keyword()) :: keyword() def init(opts) do opts end + @spec call(Plug.Conn.t(), keyword()) :: Plug.Conn.t() def call(conn, opts) do params_name = Keyword.get(opts, :params_name, "image") params_key = Keyword.get(opts, :params_key, "image") @@ -25,18 +27,13 @@ defmodule PhilomenaWeb.ScraperPlug do # Writing the tempfile doesn't allow traversal # sobelow_skip ["Traversal.FileModule"] - defp maybe_fixup_params( - {:ok, %{body: body, status: 200, headers: headers}}, - url, - opts, - conn - ) do + defp maybe_fixup_params({:ok, %{status: 200} = resp}, url, opts, conn) do params_name = Keyword.get(opts, :params_name, "image") params_key = Keyword.get(opts, :params_key, "image") - name = extract_filename(url, headers) + name = extract_filename(url, resp.headers) file = Plug.Upload.random_file!(UUID.uuid1()) - File.write!(file, body) + File.write!(file, resp.body) fake_upload = %Plug.Upload{ path: file, @@ -44,22 +41,20 @@ defmodule PhilomenaWeb.ScraperPlug do filename: name } - updated_form = Map.put(conn.params[params_name], params_key, fake_upload) - - updated_params = Map.put(conn.params, params_name, updated_form) - - %Plug.Conn{conn | params: updated_params} + put_in(conn.params[params_name][params_key], fake_upload) end defp maybe_fixup_params(_response, _url, _opts, conn), do: conn - defp extract_filename(url, resp_headers) do - {_, header} = - Enum.find(resp_headers, {nil, "filename=\"#{Path.basename(url)}\""}, fn {key, value} -> - key == "content-disposition" and Regex.match?(@filename_regex, value) - end) - - [name] = Regex.run(@filename_regex, header, capture: :all_but_first) + defp extract_filename(url, headers) do + name = + with [value | _] <- headers["content-disposition"], + [name] <- Regex.run(@filename_regex, value, capture: :all_but_first) do + name + else + _ -> + Path.basename(url) + end String.slice(name, 0, 127) end From 9bdb1b0fb8cc8f706d9eeeb0730f1e0c63ab4d28 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 20 Jun 2024 16:20:36 -0400 Subject: [PATCH 40/44] Remove cowboy and replace with bandit --- config/config.exs | 1 + lib/philomena/application.ex | 5 +---- mix.exs | 3 +-- mix.lock | 7 ++----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/config/config.exs b/config/config.exs index fbfedf78..c44f3bf0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -31,6 +31,7 @@ config :canary, # Configures the endpoint config :philomena, PhilomenaWeb.Endpoint, + adapter: Bandit.PhoenixAdapter, url: [host: "localhost"], secret_key_base: "xZYTon09JNRrj8snd7KL31wya4x71jmo5aaSSRmw1dGjWLRmEwWMTccwxgsGFGjM", render_errors: [view: PhilomenaWeb.ErrorView, accepts: ~w(html json)], diff --git a/lib/philomena/application.ex b/lib/philomena/application.ex index 4d1a7a4b..f85de948 100644 --- a/lib/philomena/application.ex +++ b/lib/philomena/application.ex @@ -32,10 +32,7 @@ defmodule Philomena.Application do PhilomenaWeb.AdvertUpdater, PhilomenaWeb.UserFingerprintUpdater, PhilomenaWeb.UserIpUpdater, - PhilomenaWeb.Endpoint, - - # Connection drainer for SIGTERM - {Plug.Cowboy.Drainer, refs: [PhilomenaWeb.Endpoint.HTTP]} + PhilomenaWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/mix.exs b/mix.exs index dc6aae6e..6286042d 100644 --- a/mix.exs +++ b/mix.exs @@ -45,8 +45,7 @@ defmodule Philomena.MixProject do {:phoenix_live_reload, "~> 1.4", only: :dev}, {:gettext, "~> 0.22"}, {:jason, "~> 1.4"}, - {:ranch, "~> 2.1", override: true}, - {:plug_cowboy, "~> 2.6"}, + {:bandit, "~> 1.2"}, {:slime, "~> 1.3.1"}, {:phoenix_slime, "~> 0.13", github: "slime-lang/phoenix_slime", ref: "8944de91654d6fcf6bdcc0aed6b8647fe3398241"}, diff --git a/mix.lock b/mix.lock index db8ea4b3..389ea193 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, "briefly": {:hex, :briefly, "0.5.1", "ee10d48da7f79ed2aebdc3e536d5f9a0c3e36ff76c0ad0d4254653a152b13a8a", [:mix], [], "hexpm", "bd684aa92ad8b7b4e0d92c31200993c4bc1469fc68cd6d5f15144041bd15cb57"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, @@ -8,9 +9,6 @@ "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, - "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, - "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "credo_envvar": {:hex, :credo_envvar, "0.1.4", "40817c10334e400f031012c0510bfa0d8725c19d867e4ae39cf14f2cbebc3b20", [:mix], [{:credo, "~> 1.0", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "5055cdb4bcbaf7d423bc2bb3ac62b4e2d825e2b1e816884c468dee59d0363009"}, "credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"}, @@ -65,13 +63,11 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm", "a266b7fb7be0d3b713912055dde3575927eca920e5d604ded45cd534f6b7a447"}, - "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, "redix": {:hex, :redix, "1.5.1", "a2386971e69bf23630fb3a215a831b5478d2ee7dc9ea7ac811ed89186ab5d7b7", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "85224eb2b683c516b80d472eb89b76067d5866913bf0be59d646f550de71f5c4"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"}, @@ -86,6 +82,7 @@ "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, From ad2b4b004c1982e4ba8af5d4acf9789c626da0e4 Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 21 Jun 2024 08:53:23 -0400 Subject: [PATCH 41/44] Work around verification failure for ssl roots using SHA-1 --- lib/philomena_proxy/http.ex | 41 +++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/philomena_proxy/http.ex b/lib/philomena_proxy/http.ex index 70172f0c..5558f697 100644 --- a/lib/philomena_proxy/http.ex +++ b/lib/philomena_proxy/http.ex @@ -84,7 +84,7 @@ defmodule PhilomenaProxy.Http do body: body, headers: [{:user_agent, @user_agent} | headers], max_redirects: 1, - connect_options: connect_options(), + connect_options: connect_options(url), inet6: true, into: &stream_response_callback/2, decode_body: false @@ -93,14 +93,39 @@ defmodule PhilomenaProxy.Http do |> Req.request() end - defp connect_options do - case Application.get_env(:philomena, :proxy_host) do - nil -> - [] + defp connect_options(url) do + transport_opts = + case URI.parse(url) do + %{scheme: "https"} -> + # SSL defaults validate SHA-1 on root certificates but this is unnecessary because many + # many roots are still signed with SHA-1 and it isn't relevant for security. Relax to + # allow validation of SHA-1, even though this creates a less secure client. + # https://github.com/erlang/otp/issues/8601 + [ + transport_opts: [ + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ], + signature_algs_cert: :ssl.signature_algs(:default, :"tlsv1.3") ++ [sha: :rsa] + ] + ] - url -> - [proxy: proxy_opts(URI.parse(url))] - end + _ -> + # Do not pass any options for non-HTTPS schemes. Finch will raise badarg if the above + # options are passed. + [] + end + + proxy_opts = + case Application.get_env(:philomena, :proxy_host) do + nil -> + [] + + url -> + [proxy: proxy_opts(URI.parse(url))] + end + + transport_opts ++ proxy_opts end defp proxy_opts(%{host: host, port: port, scheme: "https"}), From c616fbff5db4e5df3d828933e5f347a8e10179d6 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 19 Jun 2024 16:55:45 -0400 Subject: [PATCH 42/44] Timebase independent video GIF previewer --- lib/philomena_media/gif_preview.ex | 117 +++++++++++++++++++++++++ lib/philomena_media/processors/webm.ex | 55 ++---------- 2 files changed, 123 insertions(+), 49 deletions(-) create mode 100644 lib/philomena_media/gif_preview.ex diff --git a/lib/philomena_media/gif_preview.ex b/lib/philomena_media/gif_preview.ex new file mode 100644 index 00000000..f1c6fce6 --- /dev/null +++ b/lib/philomena_media/gif_preview.ex @@ -0,0 +1,117 @@ +defmodule PhilomenaMedia.GifPreview do + @moduledoc """ + GIF preview generation for video files. + """ + + @type duration :: float() + @type dimensions :: {pos_integer(), pos_integer()} + + @type num_images :: integer() + @type target_framerate :: 1..50 + @type opts :: [ + num_images: num_images(), + target_framerate: target_framerate() + ] + + @doc """ + Generate a GIF preview of the given video input with evenly-spaced sample points. + + The input should have pre-computed duration `duration`. The `dimensions` + are a `{target_width, target_height}` tuple of the largest dimensions desired, + and the image will be resized to fit inside the box of those dimensions, + preserving aspect ratio. + + Depending on the input file, this may take a long time to process. + + Options: + - `:target_framerate` - framerate of the output GIF, must be between 1 and 50. + Default 2. + - `:num_images` - number of images to sample from the video. + Default is determined by the duration: + * 90 or above: 20 images + * 30 or above: 10 images + * 1 or above: 5 images + * otherwise: 2 images + """ + @spec preview(Path.t(), Path.t(), duration(), dimensions(), opts()) :: :ok + def preview(video, gif, duration, dimensions, opts \\ []) do + target_framerate = Keyword.get(opts, :target_framerate, 2) + + num_images = + Keyword.get_lazy(opts, :num_images, fn -> + cond do + duration >= 90 -> 20 + duration >= 30 -> 10 + duration >= 1 -> 5 + true -> 2 + end + end) + + {_output, 0} = + System.cmd( + "ffmpeg", + commands(video, gif, clamp(duration), dimensions, num_images, target_framerate) + ) + + :ok + end + + @spec commands(Path.t(), Path.t(), duration(), dimensions(), num_images(), target_framerate()) :: + [String.t()] + defp commands(video, gif, duration, {target_width, target_height}, num_images, target_framerate) do + # Compute range [0, num_images) + image_range = 0..(num_images - 1) + + # Generate input list in the following form: + # -ss 0.0 -i input.webm + input_arguments = + Enum.flat_map(image_range, &["-ss", "#{&1 * duration / num_images}", "-i", video]) + + # Generate graph in the following form: + # [0:v] trim=end_frame=1 [t0]; [1:v] trim=end_frame=1 [t1] ... + trim_filters = + Enum.map_join(image_range, ";", &"[#{&1}:v] trim=end_frame=1 [t#{&1}]") + + # Generate graph in the following form: + # [t0][t1]... concat=n=10 [concat] + concat_input_pads = + Enum.map_join(image_range, "", &"[t#{&1}]") + + concat_filter = + "#{concat_input_pads} concat=n=#{num_images}, settb=1/#{target_framerate}, setpts=N [concat]" + + scale_filter = + "[concat] scale=width=#{target_width}:height=#{target_height}:" <> + "force_original_aspect_ratio=decrease [scale]" + + split_filter = "[scale] split [s0][s1]" + + palettegen_filter = + "[s0] palettegen=stats_mode=single:max_colors=255:reserve_transparent=1 [palettegen]" + + paletteuse_filter = + "[s1][palettegen] paletteuse=dither=bayer:bayer_scale=5:new=1:alpha_threshold=255" + + filter_graph = + [ + trim_filters, + concat_filter, + scale_filter, + split_filter, + palettegen_filter, + paletteuse_filter + ] + |> Enum.join(";") + + # Delay in centiseconds - otherwise it will be computed incorrectly + final_delay = 100.0 / target_framerate + + ["-loglevel", "0", "-y"] + |> Kernel.++(input_arguments) + |> Kernel.++(["-lavfi", filter_graph]) + |> Kernel.++(["-f", "gif", "-final_delay", "#{final_delay}", gif]) + end + + defp clamp(duration) when duration <= 0, do: 1.0 + defp clamp(duration), do: duration +end diff --git a/lib/philomena_media/processors/webm.ex b/lib/philomena_media/processors/webm.ex index c86e1969..ad446645 100644 --- a/lib/philomena_media/processors/webm.ex +++ b/lib/philomena_media/processors/webm.ex @@ -3,6 +3,7 @@ defmodule PhilomenaMedia.Processors.Webm do alias PhilomenaMedia.Intensities alias PhilomenaMedia.Analyzers.Result + alias PhilomenaMedia.GifPreview alias PhilomenaMedia.Processors.Processor alias PhilomenaMedia.Processors import Bitwise @@ -28,12 +29,11 @@ defmodule PhilomenaMedia.Processors.Webm do duration = analysis.duration stripped = strip(file) preview = preview(duration, stripped) - palette = gif_palette(stripped, duration) mp4 = scale_mp4_only(stripped, dimensions, dimensions) {:ok, intensities} = Intensities.file(preview) - scaled = Enum.flat_map(versions, &scale(stripped, palette, duration, dimensions, &1)) + scaled = Enum.flat_map(versions, &scale(stripped, duration, dimensions, &1)) mp4 = [{:copy, mp4, "full.mp4"}] [ @@ -82,12 +82,12 @@ defmodule PhilomenaMedia.Processors.Webm do stripped end - defp scale(file, palette, duration, dimensions, {thumb_name, target_dimensions}) do + defp scale(file, duration, dimensions, {thumb_name, target_dimensions}) do {webm, mp4} = scale_videos(file, dimensions, target_dimensions) cond do thumb_name in [:thumb, :thumb_small, :thumb_tiny] -> - gif = scale_gif(file, palette, duration, target_dimensions) + gif = scale_gif(file, duration, target_dimensions) [ {:copy, webm, "#{thumb_name}.webm"}, @@ -199,53 +199,14 @@ defmodule PhilomenaMedia.Processors.Webm do mp4 end - defp scale_gif(file, palette, duration, {width, height}) do + defp scale_gif(file, duration, dimensions) do gif = Briefly.create!(extname: ".gif") - scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease" - palette_filter = "paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" - rate_filter = rate_filter(duration) - filter_graph = "[0:v]#{scale_filter},#{rate_filter}[x];[x][1:v]#{palette_filter}" - {_output, 0} = - System.cmd("ffmpeg", [ - "-loglevel", - "0", - "-y", - "-i", - file, - "-i", - palette, - "-lavfi", - filter_graph, - "-r", - "2", - gif - ]) + GifPreview.preview(file, gif, duration, dimensions) gif end - defp gif_palette(file, duration) do - palette = Briefly.create!(extname: ".png") - palette_filter = "palettegen=stats_mode=diff" - rate_filter = rate_filter(duration) - filter_graph = "#{rate_filter},#{palette_filter}" - - {_output, 0} = - System.cmd("ffmpeg", [ - "-loglevel", - "0", - "-y", - "-i", - file, - "-vf", - filter_graph, - palette - ]) - - palette - end - # x264 requires image dimensions to be a multiple of 2 # -2 = ~1 def box_dimensions({width, height}, {target_width, target_height}) do @@ -255,8 +216,4 @@ defmodule PhilomenaMedia.Processors.Webm do {new_width, new_height} end - - # Avoid division by zero - def rate_filter(duration) when duration > 0.5, do: "fps=1/#{duration / 10},settb=1/2,setpts=N" - def rate_filter(_duration), do: "setpts=N/TB/2" end From 7570f18dcb21372c82c485aa349e13de92776299 Mon Sep 17 00:00:00 2001 From: Luna D Date: Sat, 22 Jun 2024 19:51:48 +0200 Subject: [PATCH 43/44] preconnect to cdn host --- lib/philomena_web/templates/layout/app.html.slime | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime index 8fc29580..2a174357 100644 --- a/lib/philomena_web/templates/layout/app.html.slime +++ b/lib/philomena_web/templates/layout/app.html.slime @@ -10,6 +10,7 @@ html lang="en" ' - Derpibooru - else ' Derpibooru + link rel="preconnect" href="https://#{cdn_host()}" link rel="stylesheet" href=stylesheet_path(@current_user) = if is_nil(@current_user) do link rel="stylesheet" href=dark_stylesheet_path() media="(prefers-color-scheme: dark)" From cc21b402a04d3860d9f547443d4a467349701671 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 22 Jun 2024 18:16:14 -0400 Subject: [PATCH 44/44] Fix weird indentation --- lib/philomena/attribution.ex | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/philomena/attribution.ex b/lib/philomena/attribution.ex index 53fca736..94d6c35e 100644 --- a/lib/philomena/attribution.ex +++ b/lib/philomena/attribution.ex @@ -1,22 +1,22 @@ defprotocol Philomena.Attribution do @doc """ - Provides the "parent object" identifier for this object. This is so - that anonymous posts under the same topic id can return the same hash - for the same user. + Provides the "parent object" identifier for this object. This is so + that anonymous posts under the same topic id can return the same hash + for the same user. """ @spec object_identifier(struct()) :: String.t() def object_identifier(object) @doc """ - Provides the "best" user identifier for an object. Usually this will be - the user_id, but may also be the fingerprint or IP address if other - information is unavailable. + Provides the "best" user identifier for an object. Usually this will be + the user_id, but may also be the fingerprint or IP address if other + information is unavailable. """ @spec best_user_identifier(struct()) :: String.t() def best_user_identifier(object) @doc """ - Return whether this object is considered to be anonymous. + Return whether this object is considered to be anonymous. """ @spec anonymous?(struct()) :: true | false def anonymous?(object)