From 945b6a326fe98294d0e5c174ecbc6ee5bba9a0fa Mon Sep 17 00:00:00 2001 From: MareStare Date: Sun, 30 Mar 2025 19:42:05 +0000 Subject: [PATCH 01/61] Fix the tag updates atomicity and deadlocks --- lib/philomena/images.ex | 21 +++--- lib/philomena/tags.ex | 160 +++++++++++++++++++++++++++++++--------- 2 files changed, 138 insertions(+), 43 deletions(-) diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index fe3f27d01..605cc3b85 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -96,22 +96,25 @@ defmodule Philomena.Images do |> Multi.insert(:image, image) |> Multi.run(:added_tag_count, fn repo, %{image: image} -> tag_ids = image.added_tags |> Enum.map(& &1.id) - tags = Tag |> where([t], t.id in ^tag_ids) - {count, nil} = repo.update_all(tags, inc: [images_count: 1]) + count = Tags.update_images_counts(repo, +1, tag_ids) {:ok, count} end) |> maybe_subscribe_on(:image, attribution[:user], :watch_on_upload) |> Repo.transaction() |> case do - {:ok, %{image: image}} = result -> - async_upload(image, attrs["image"]) + {:ok, %{image: image}} -> + upload_pid = async_upload(image, attrs["image"]) reindex_image(image) Tags.reindex_tags(image.added_tags) maybe_approve_image(image, attribution[:user]) - result + # Return the upload PID along with the created image so that the caller + # can control the lifecycle of the upload if needed. It's useful, for + # example for the seeding process to know when to delete the temp file + # used for uploading. + {:ok, %{image: image, upload_pid: upload_pid}} result -> result @@ -138,6 +141,8 @@ defmodule Philomena.Images do # Free up the linked process send(linked_pid, :ready) + + linked_pid end defp try_upload(image, retry_count) when retry_count < 100 do @@ -666,9 +671,8 @@ defmodule Philomena.Images do repo, %{image: {_image, _added, removed_tags}} -> tag_ids = removed_tags |> Enum.map(& &1.id) - tags = Tag |> where([t], t.id in ^tag_ids) - {count, nil} = repo.update_all(tags, inc: [images_count: -1]) + count = Tags.update_images_counts(repo, -1, tag_ids) {:ok, count} end) @@ -955,9 +959,8 @@ defmodule Philomena.Images do # to way too much drift, and the index has to be # maintained. tag_ids = Enum.map(image.tags, & &1.id) - query = where(Tag, [t], t.id in ^tag_ids) - repo.update_all(query, inc: [images_count: -1]) + Tags.update_images_counts(repo, -1, tag_ids) {:ok, image.tags} end) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 11595f3a7..dc484748d 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -4,6 +4,7 @@ defmodule Philomena.Tags do """ import Ecto.Query, warn: false + alias Ecto.Multi alias Philomena.Repo alias PhilomenaQuery.Search @@ -24,6 +25,47 @@ defmodule Philomena.Tags do alias Philomena.DnpEntries.DnpEntry alias Philomena.Channels.Channel + # There is a really delicate nuance that must be known to avoid deadlocks in + # vectorized mutation queries such as `INSERT ON CONFLICT UPDATE`, `UPDATE`, + # `DELETE`, `SELECT FOR [NO KEY] UPDATE` that touch multiple records. Note that + # `INSERT ON CONFLICT DO NOTHING` doesn't lock the conflicting records, so this + # nuance doesn't apply in that case (https://dba.stackexchange.com/questions/322912/will-insert-on-conflict-do-nothing-lock-the-row-in-case-of-conflict) + # + # If a vectorized mutation is run without a consistent locking order of the records, + # it can end up with a deadlock where one transaction locks a set of records + # that overlap with the other transaction while the other transaction locks + # the other set that overlaps with the first transaction. Thus, both transactions + # wait for each other to release the locks on records they locked resulting in + # a deadlock. + # + # For raw `UPDATE/DELETE ... WHERE ... IN (...)` queries, the items inside `IN (...)` + # don't influence the order of locking. These queries also don't have an `ORDER BY` + # clause. Thus, this function returns a `SELECT [lock_type]` query that establishes a + # consistent order of records by primary keys that must be used with all vectorized + # mutation queries to avoid deadlocks. This query can be used as a subquery in + # the `WHERE` clause for the vectorized mutation. + # + # If no locking order is set, the deadlock can appear randomly and its probability + # increases with the amount of items in the vectorized mutation query and with + # the number of overlapping records in concurrent transactions. + # + # This phenomena was discovered when @MareStare was trying to parallelize + # the image creation process for seeding the images during development, where + # tons of image uploads are issued in parallel with many overlapping tags + # (https://github.com/philomena-dev/philomena/pull/481). + # + # Big thanks to this StackOverflow post for explanations: + # https://stackoverflow.com/questions/27262900/postgres-update-and-lock-ordering/27263824#27263824 + defmacro vectorized_mutation_lock(lock_type, tag_ids) do + quote do + Tag + |> select([t], t.id) + |> lock(unquote(lock_type)) + |> where([t], t.id in ^unquote(tag_ids)) + |> order_by([t], t.id) + end + end + @doc """ Gets existing tags or creates new ones from a tag list string. @@ -39,43 +81,74 @@ defmodule Philomena.Tags do """ @spec get_or_create_tags(String.t()) :: list() def get_or_create_tags(tag_list) do - tag_names = Tag.parse_tag_list(tag_list) - - existent_tags = - Tag - |> where([t], t.name in ^tag_names) - |> preload([:implied_tags, aliased_tag: :implied_tags]) - |> Repo.all() - |> Enum.uniq_by(& &1.name) - - existent_tag_names = - existent_tags - |> Map.new(&{&1.name, true}) + case Tag.parse_tag_list(tag_list) do + [] -> [] + tag_names -> get_or_create_non_empty_tags_list(tag_names) + end + end - nonexistent_tag_names = + @spec get_or_create_non_empty_tags_list(list(String.t())) :: list() + defp get_or_create_non_empty_tags_list(tag_names) do + tags = tag_names - |> Enum.reject(&existent_tag_names[&1]) - - # Now get rid of the aliases - existent_tags = - existent_tags - |> Enum.map(&(&1.aliased_tag || &1)) - - new_tags = - nonexistent_tag_names - |> Enum.map(fn name -> - {:ok, tag} = - %Tag{} - |> Tag.creation_changeset(%{name: name}) - |> Repo.insert() - - %{tag | implied_tags: []} - end) + |> Enum.sort() + |> Enum.dedup() + |> Enum.map( + &(%Tag{ + created_at: {:placeholder, :timestamp}, + updated_at: {:placeholder, :timestamp} + } + |> Tag.creation_changeset(%{name: &1}) + |> Ecto.Changeset.apply_changes() + |> Map.from_struct() + |> Map.drop([ + :id, + :__meta__, + :aliases, + :aliased_tag, + :channels, + :implied_tags, + :implied_by_tags, + :verified_links, + :public_links, + :hidden_links, + :dnp_entries, + :uploaded_image, + :removed_image, + :implied_tag_list + ])) + ) + + %{new_tags: {_rows_affected, new_tags}, all_tags: all_tags} = + Multi.new() + |> Multi.insert_all( + :new_tags, + Tag, + tags, + placeholders: %{timestamp: DateTime.utc_now() |> DateTime.truncate(:second)}, + on_conflict: :nothing, + returning: [:id] + ) + |> Multi.all( + :all_tags, + Tag + |> where([t], t.name in ^tag_names) + |> distinct([t], t.name) + |> preload([:implied_tags, aliased_tag: :implied_tags]) + ) + |> Repo.transaction() + |> case do + {:ok, ok} -> + ok + + {:error, err} -> + raise "get_or_create_tags failed: #{inspect(err)}\ntag_names: #{inspect(tag_names)}" + end new_tags |> reindex_tags() - existent_tags ++ new_tags + all_tags end @doc """ @@ -545,9 +618,7 @@ defmodule Philomena.Tags do tag_ids = Enum.map(taggings, & &1.tag_id) - Tag - |> where([t], t.id in ^tag_ids) - |> Repo.update_all(inc: [images_count: 1]) + update_images_counts(Repo, +1, tag_ids) tag_ids end) @@ -557,6 +628,27 @@ defmodule Philomena.Tags do |> Repo.all() end + @doc """ + Accepts IDs of tags and increments their `images_count` by 1. + """ + @spec update_images_counts(term(), integer(), [integer()]) :: integer() + def update_images_counts(repo, diff, tag_ids) do + case tag_ids do + [] -> + 0 + + _ -> + locked_tags = vectorized_mutation_lock("FOR NO KEY UPDATE", tag_ids) + + {rows_affected, _} = + Tag + |> where([t], t.id in subquery(locked_tags)) + |> repo.update_all(inc: [images_count: diff]) + + rows_affected + end + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking tag changes. From e72f7db356a7715cc4bbb0ca520349d481e9b4cb Mon Sep 17 00:00:00 2001 From: MareStare Date: Sun, 30 Mar 2025 21:03:50 +0000 Subject: [PATCH 02/61] Replace `Map.drop()` with `Map.take()` as per Liam's feedback in DM --- lib/philomena/tags.ex | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index dc484748d..55422e723 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -94,29 +94,27 @@ defmodule Philomena.Tags do |> Enum.sort() |> Enum.dedup() |> Enum.map( - &(%Tag{ - created_at: {:placeholder, :timestamp}, - updated_at: {:placeholder, :timestamp} - } + &(%Tag{} |> Tag.creation_changeset(%{name: &1}) |> Ecto.Changeset.apply_changes() - |> Map.from_struct() - |> Map.drop([ - :id, - :__meta__, - :aliases, - :aliased_tag, - :channels, - :implied_tags, - :implied_by_tags, - :verified_links, - :public_links, - :hidden_links, - :dnp_entries, - :uploaded_image, - :removed_image, - :implied_tag_list - ])) + |> Map.take([ + :slug, + :name, + :category, + :images_count, + :description, + :short_description, + :namespace, + :name_in_namespace, + :image, + :image_format, + :image_mime_type, + :mod_notes + ]) + |> Map.merge(%{ + created_at: {:placeholder, :timestamp}, + updated_at: {:placeholder, :timestamp} + })) ) %{new_tags: {_rows_affected, new_tags}, all_tags: all_tags} = From a7f4105cce5c713a73c629e49f26499b87aa3992 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 31 Mar 2025 20:37:26 +0000 Subject: [PATCH 03/61] Fix error handling since there will be a tuple of more than 2 items returned as per docs --- lib/philomena/tags.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 55422e723..9e542929d 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -139,8 +139,8 @@ defmodule Philomena.Tags do {:ok, ok} -> ok - {:error, err} -> - raise "get_or_create_tags failed: #{inspect(err)}\ntag_names: #{inspect(tag_names)}" + result -> + raise "get_or_create_tags failed: #{inspect(result)}\ntag_names: #{inspect(tag_names)}" end new_tags From 79c794c0c4fa62f7da021482600c77217c23bfc7 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 12 May 2025 08:29:33 +0000 Subject: [PATCH 04/61] Replace `+1` with `1` for update_images_counts --- lib/philomena/images.ex | 2 +- lib/philomena/tags.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index 605cc3b85..73c9f00cd 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -97,7 +97,7 @@ defmodule Philomena.Images do |> Multi.run(:added_tag_count, fn repo, %{image: image} -> tag_ids = image.added_tags |> Enum.map(& &1.id) - count = Tags.update_images_counts(repo, +1, tag_ids) + count = Tags.update_images_counts(repo, 1, tag_ids) {:ok, count} end) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 9e542929d..bd0b979e2 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -616,7 +616,7 @@ defmodule Philomena.Tags do tag_ids = Enum.map(taggings, & &1.tag_id) - update_images_counts(Repo, +1, tag_ids) + update_images_counts(Repo, 1, tag_ids) tag_ids end) From 270825c5ccae6a26370fe72ee97cdee699c62cc2 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 12 May 2025 08:43:57 +0000 Subject: [PATCH 05/61] Rename `update_images_counts` to `update_image_counts` and replace `case` with a function-level match --- lib/philomena/images.ex | 6 +++--- lib/philomena/tags.ex | 26 ++++++++++++-------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index 73c9f00cd..2ab2d282e 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -97,7 +97,7 @@ defmodule Philomena.Images do |> Multi.run(:added_tag_count, fn repo, %{image: image} -> tag_ids = image.added_tags |> Enum.map(& &1.id) - count = Tags.update_images_counts(repo, 1, tag_ids) + count = Tags.update_image_counts(repo, 1, tag_ids) {:ok, count} end) @@ -672,7 +672,7 @@ defmodule Philomena.Images do repo, %{image: {_image, _added, removed_tags}} -> tag_ids = removed_tags |> Enum.map(& &1.id) - count = Tags.update_images_counts(repo, -1, tag_ids) + count = Tags.update_image_counts(repo, -1, tag_ids) {:ok, count} end) @@ -960,7 +960,7 @@ defmodule Philomena.Images do # maintained. tag_ids = Enum.map(image.tags, & &1.id) - Tags.update_images_counts(repo, -1, tag_ids) + Tags.update_image_counts(repo, -1, tag_ids) {:ok, image.tags} end) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index bd0b979e2..e735a632f 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -616,7 +616,7 @@ defmodule Philomena.Tags do tag_ids = Enum.map(taggings, & &1.tag_id) - update_images_counts(Repo, 1, tag_ids) + update_image_counts(Repo, 1, tag_ids) tag_ids end) @@ -629,22 +629,20 @@ defmodule Philomena.Tags do @doc """ Accepts IDs of tags and increments their `images_count` by 1. """ - @spec update_images_counts(term(), integer(), [integer()]) :: integer() - def update_images_counts(repo, diff, tag_ids) do - case tag_ids do - [] -> - 0 + @spec update_image_counts(term(), integer(), [integer()]) :: integer() + def update_image_counts(repo, diff, tag_ids) - _ -> - locked_tags = vectorized_mutation_lock("FOR NO KEY UPDATE", tag_ids) + def update_image_counts(nil, _diff, []), do: 0 - {rows_affected, _} = - Tag - |> where([t], t.id in subquery(locked_tags)) - |> repo.update_all(inc: [images_count: diff]) + def update_image_counts(repo, diff, tag_ids) do + locked_tags = vectorized_mutation_lock("FOR NO KEY UPDATE", tag_ids) - rows_affected - end + {rows_affected, _} = + Tag + |> where([t], t.id in subquery(locked_tags)) + |> repo.update_all(inc: [images_count: diff]) + + rows_affected end @doc """ From 343b6e1751d7553733da0168d770928f40166cf2 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 12 May 2025 08:52:59 +0000 Subject: [PATCH 06/61] Replace the capture operator with an explicit `fn tag_name ->` --- lib/philomena/tags.ex | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index e735a632f..fb53cfd35 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -93,29 +93,29 @@ defmodule Philomena.Tags do tag_names |> Enum.sort() |> Enum.dedup() - |> Enum.map( - &(%Tag{} - |> Tag.creation_changeset(%{name: &1}) - |> Ecto.Changeset.apply_changes() - |> Map.take([ - :slug, - :name, - :category, - :images_count, - :description, - :short_description, - :namespace, - :name_in_namespace, - :image, - :image_format, - :image_mime_type, - :mod_notes - ]) - |> Map.merge(%{ - created_at: {:placeholder, :timestamp}, - updated_at: {:placeholder, :timestamp} - })) - ) + |> Enum.map(fn tag_name -> + %Tag{} + |> Tag.creation_changeset(%{name: tag_name}) + |> Ecto.Changeset.apply_changes() + |> Map.take([ + :slug, + :name, + :category, + :images_count, + :description, + :short_description, + :namespace, + :name_in_namespace, + :image, + :image_format, + :image_mime_type, + :mod_notes + ]) + |> Map.merge(%{ + created_at: {:placeholder, :timestamp}, + updated_at: {:placeholder, :timestamp} + }) + end) %{new_tags: {_rows_affected, new_tags}, all_tags: all_tags} = Multi.new() From 1a601a91727516ea9f21b842d15f4cb986ab5ac8 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 12 May 2025 08:53:37 +0000 Subject: [PATCH 07/61] Fix remove repo nil match autocompleted by Github Copilot (bruh) --- lib/philomena/tags.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index fb53cfd35..62cc874a9 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -632,7 +632,7 @@ defmodule Philomena.Tags do @spec update_image_counts(term(), integer(), [integer()]) :: integer() def update_image_counts(repo, diff, tag_ids) - def update_image_counts(nil, _diff, []), do: 0 + def update_image_counts(_repo, _diff, []), do: 0 def update_image_counts(repo, diff, tag_ids) do locked_tags = vectorized_mutation_lock("FOR NO KEY UPDATE", tag_ids) From e896931ba725d07318edf0e83a9f7812d2c9ae16 Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 2 Apr 2025 11:52:44 +0000 Subject: [PATCH 08/61] Fix the grammar in autocomplete test context comment --- assets/js/autocomplete/__tests__/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/autocomplete/__tests__/context.ts b/assets/js/autocomplete/__tests__/context.ts index 89e949eb1..5bf636aa7 100644 --- a/assets/js/autocomplete/__tests__/context.ts +++ b/assets/js/autocomplete/__tests__/context.ts @@ -22,7 +22,7 @@ import { GetTagSuggestionsResponse, TagSuggestion } from 'autocomplete/client'; * `jsdom`, so it's expected that you define a single test per file so that * `vitest` runs every test in an isolated process, where no cleanup is needed. * - * I wish `vitest` actually did that by default, because cleanup logic and test + * I wish `vitest` actually did that by default, because cleanup logic and * in-process test isolation is just boilerplate that we could avoid at this * scale at least. */ From 9ca11b7902ca6904efea42eda8ea4148f7356c99 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Wed, 2 Apr 2025 08:47:56 +0000 Subject: [PATCH 09/61] Use unique input names for search forms --- lib/philomena_web/controllers/admin/artist_link_controller.ex | 2 +- lib/philomena_web/controllers/admin/dnp_entry_controller.ex | 2 +- .../controllers/admin/fingerprint_ban_controller.ex | 2 +- lib/philomena_web/controllers/admin/subnet_ban_controller.ex | 2 +- lib/philomena_web/controllers/admin/user_ban_controller.ex | 2 +- lib/philomena_web/controllers/admin/user_controller.ex | 2 +- .../templates/admin/artist_link/index.html.slime | 2 +- lib/philomena_web/templates/admin/dnp_entry/index.html.slime | 2 +- .../templates/admin/fingerprint_ban/index.html.slime | 2 +- lib/philomena_web/templates/admin/subnet_ban/index.html.slime | 2 +- lib/philomena_web/templates/admin/user/index.html.slime | 2 +- lib/philomena_web/templates/admin/user_ban/index.html.slime | 2 +- lib/philomena_web/templates/search/_form.html.slime | 2 +- lib/philomena_web/views/admin/ban_view.ex | 4 ++-- lib/philomena_web/views/admin/user_view.ex | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/philomena_web/controllers/admin/artist_link_controller.ex b/lib/philomena_web/controllers/admin/artist_link_controller.ex index 298dba81d..a785360a8 100644 --- a/lib/philomena_web/controllers/admin/artist_link_controller.ex +++ b/lib/philomena_web/controllers/admin/artist_link_controller.ex @@ -11,7 +11,7 @@ defmodule PhilomenaWeb.Admin.ArtistLinkController do load_links(ArtistLink, conn) end - def index(conn, %{"q" => query}) do + def index(conn, %{"lq" => query}) do query = "%#{query}%" ArtistLink diff --git a/lib/philomena_web/controllers/admin/dnp_entry_controller.ex b/lib/philomena_web/controllers/admin/dnp_entry_controller.ex index 359454dd0..ea6e1a61e 100644 --- a/lib/philomena_web/controllers/admin/dnp_entry_controller.ex +++ b/lib/philomena_web/controllers/admin/dnp_entry_controller.ex @@ -15,7 +15,7 @@ defmodule PhilomenaWeb.Admin.DnpEntryController do |> load_entries(conn) end - def index(conn, %{"q" => q}) when is_binary(q) do + def index(conn, %{"eq" => q}) when is_binary(q) do q = to_ilike(q) DnpEntry diff --git a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex index d3c17c125..598de81e3 100644 --- a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex @@ -15,7 +15,7 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do plug :check_can_delete when action in [:delete] - def index(conn, %{"q" => q}) when is_binary(q) do + def index(conn, %{"bq" => q}) when is_binary(q) do FingerprintBan |> where( [fb], diff --git a/lib/philomena_web/controllers/admin/subnet_ban_controller.ex b/lib/philomena_web/controllers/admin/subnet_ban_controller.ex index f41639216..17bbab605 100644 --- a/lib/philomena_web/controllers/admin/subnet_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/subnet_ban_controller.ex @@ -10,7 +10,7 @@ defmodule PhilomenaWeb.Admin.SubnetBanController do plug :load_resource, model: SubnetBan, only: [:edit, :update, :delete] plug :check_can_delete when action in [:delete] - def index(conn, %{"q" => q}) when is_binary(q) do + def index(conn, %{"bq" => q}) when is_binary(q) do SubnetBan |> where( [sb], diff --git a/lib/philomena_web/controllers/admin/user_ban_controller.ex b/lib/philomena_web/controllers/admin/user_ban_controller.ex index f79fecd74..11d232f3e 100644 --- a/lib/philomena_web/controllers/admin/user_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/user_ban_controller.ex @@ -11,7 +11,7 @@ defmodule PhilomenaWeb.Admin.UserBanController do plug :load_resource, model: UserBan, only: [:edit, :update, :delete], preload: :user plug :check_can_delete when action in [:delete] - def index(conn, %{"q" => q}) when is_binary(q) do + def index(conn, %{"bq" => q}) when is_binary(q) do like_q = "%#{q}%" UserBan diff --git a/lib/philomena_web/controllers/admin/user_controller.ex b/lib/philomena_web/controllers/admin/user_controller.ex index 266291de8..8a760cba0 100644 --- a/lib/philomena_web/controllers/admin/user_controller.ex +++ b/lib/philomena_web/controllers/admin/user_controller.ex @@ -17,7 +17,7 @@ defmodule PhilomenaWeb.Admin.UserController do plug :load_roles when action in [:edit, :update] - def index(conn, %{"q" => q}) do + def index(conn, %{"uq" => q}) do User |> where([u], u.email == ^q or ilike(u.name, ^"%#{q}%")) |> load_users(conn) diff --git a/lib/philomena_web/templates/admin/artist_link/index.html.slime b/lib/philomena_web/templates/admin/artist_link/index.html.slime index 633ff2c18..01996fc7a 100644 --- a/lib/philomena_web/templates/admin/artist_link/index.html.slime +++ b/lib/philomena_web/templates/admin/artist_link/index.html.slime @@ -4,7 +4,7 @@ p Verifying a link will automatically award an artist badge if the link is publi = form_for :artist_link, ~p"/admin/artist_links", [method: "get", class: "hform"], fn f -> .field - = text_input f, :q, name: :q, value: @conn.params["q"], class: "input hform__text", placeholder: "Search query", autocapitalize: "none" + = text_input f, :lq, name: :lq, value: @conn.params["lq"], class: "input hform__text", placeholder: "Search query", autocapitalize: "none" = submit "Search", class: "hform__button button" - route = fn p -> ~p"/admin/artist_links?#{p}" end diff --git a/lib/philomena_web/templates/admin/dnp_entry/index.html.slime b/lib/philomena_web/templates/admin/dnp_entry/index.html.slime index bf932f69e..989ed65e7 100644 --- a/lib/philomena_web/templates/admin/dnp_entry/index.html.slime +++ b/lib/philomena_web/templates/admin/dnp_entry/index.html.slime @@ -2,7 +2,7 @@ h2 Do-Not-Post Requests = form_for :dnp_entry, ~p"/admin/dnp_entries", [method: "get", class: "hform"], fn f -> .field - = text_input f, :q, name: :q, value: @conn.params["q"], class: "input hform__text", placeholder: "Search query", autocapitalize: "none" + = text_input f, :eq, name: :eq, value: @conn.params["eq"], class: "input hform__text", placeholder: "Search query", autocapitalize: "none" = submit "Search", class: "hform__button button" - route = fn p -> ~p"/admin/dnp_entries?#{p}" end diff --git a/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime b/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime index 7e42314af..a11d2f404 100644 --- a/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime +++ b/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime @@ -5,7 +5,7 @@ h1 Fingerprint Bans = form_for :fingerprint_ban, ~p"/admin/fingerprint_bans", [method: "get", class: "hform"], fn f -> .field - = text_input f, :q, name: "q", class: "hform__text input", placeholder: "Search" + = text_input f, :bq, name: :bq, value: @conn.params["bq"], class: "hform__text input", placeholder: "Search" = submit "Search", class: "button hform__button" .block diff --git a/lib/philomena_web/templates/admin/subnet_ban/index.html.slime b/lib/philomena_web/templates/admin/subnet_ban/index.html.slime index c051a10a1..f4ad64677 100644 --- a/lib/philomena_web/templates/admin/subnet_ban/index.html.slime +++ b/lib/philomena_web/templates/admin/subnet_ban/index.html.slime @@ -5,7 +5,7 @@ h1 Subnet Bans = form_for :subnet_ban, ~p"/admin/subnet_bans", [method: "get", class: "hform"], fn f -> .field - = text_input f, :q, name: "q", class: "hform__text input", placeholder: "Search" + = text_input f, :bq, name: :bq, value: @conn.params["bq"], class: "hform__text input", placeholder: "Search" = submit "Search", class: "button hform__button" .block diff --git a/lib/philomena_web/templates/admin/user/index.html.slime b/lib/philomena_web/templates/admin/user/index.html.slime index e3077f94c..5e13d130f 100644 --- a/lib/philomena_web/templates/admin/user/index.html.slime +++ b/lib/philomena_web/templates/admin/user/index.html.slime @@ -2,7 +2,7 @@ h1 Users = form_for :user, ~p"/admin/users", [method: "get", class: "hform"], fn f -> .field - => text_input f, :q, name: "q", class: "hform__text input", placeholder: "Search query" + => text_input f, :uq, name: :uq, value: @conn.params["uq"], class: "hform__text input", placeholder: "Search query" = submit "Search", class: "button hform__button" => link "Site staff", to: ~p"/admin/users?#{[staff: 1]}" diff --git a/lib/philomena_web/templates/admin/user_ban/index.html.slime b/lib/philomena_web/templates/admin/user_ban/index.html.slime index c6a35ac4f..76388e211 100644 --- a/lib/philomena_web/templates/admin/user_ban/index.html.slime +++ b/lib/philomena_web/templates/admin/user_ban/index.html.slime @@ -5,7 +5,7 @@ h1 User Bans = form_for :user_ban, ~p"/admin/user_bans", [method: "get", class: "hform"], fn f -> .field - = text_input f, :q, name: "q", class: "hform__text input", placeholder: "Search" + = text_input f, :bq, name: :bq, value: @conn.params["bq"], class: "hform__text input", placeholder: "Search" = submit "Search", class: "button hform__button" .block diff --git a/lib/philomena_web/templates/search/_form.html.slime b/lib/philomena_web/templates/search/_form.html.slime index 0eafd0d82..5325dd467 100644 --- a/lib/philomena_web/templates/search/_form.html.slime +++ b/lib/philomena_web/templates/search/_form.html.slime @@ -4,7 +4,7 @@ h1 Search = text_input f, :q, class: "input input--wide js-search-field", placeholder: "Search terms are chained with commas", autocapitalize: "none", - name: "q", + name: :q, value: @conn.params["q"], autocomplete: if(@conn.cookies["enable_search_ac"], do: "on", else: "off"), inputmode: "search", diff --git a/lib/philomena_web/views/admin/ban_view.ex b/lib/philomena_web/views/admin/ban_view.ex index 59fbe1de1..27f9dfc23 100644 --- a/lib/philomena_web/views/admin/ban_view.ex +++ b/lib/philomena_web/views/admin/ban_view.ex @@ -14,10 +14,10 @@ defmodule PhilomenaWeb.Admin.BanView do end def page_params(params) do - case params["q"] do + case params["bq"] do nil -> [] "" -> [] - q -> [q: q] + q -> [bq: q] end end end diff --git a/lib/philomena_web/views/admin/user_view.ex b/lib/philomena_web/views/admin/user_view.ex index 19f33b482..954454472 100644 --- a/lib/philomena_web/views/admin/user_view.ex +++ b/lib/philomena_web/views/admin/user_view.ex @@ -3,7 +3,7 @@ defmodule PhilomenaWeb.Admin.UserView do def page_params(params) do [] - |> page_param(params, "q", :q) + |> page_param(params, "uq", :uq) |> page_param(params, "staff", :staff) |> page_param(params, "twofactor", :twofactor) end From 7d2cd0e4852d591494a514aaa4979c6a17f21870 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Wed, 2 Apr 2025 03:12:16 +0000 Subject: [PATCH 10/61] Reindex tag itself on "Rebuild index" --- lib/philomena_web/controllers/tag/reindex_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/philomena_web/controllers/tag/reindex_controller.ex b/lib/philomena_web/controllers/tag/reindex_controller.ex index aee27c282..b7b277ed9 100644 --- a/lib/philomena_web/controllers/tag/reindex_controller.ex +++ b/lib/philomena_web/controllers/tag/reindex_controller.ex @@ -15,6 +15,7 @@ defmodule PhilomenaWeb.Tag.ReindexController do def create(conn, _params) do {:ok, tag} = Tags.reindex_tag_images(conn.assigns.tag) + Tags.reindex_tag(tag) conn |> put_flash(:info, "Tag reindex started.") From 56b0afa1133fe82dc462579d6898c55fe9f5a153 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 05:00:53 +0400 Subject: [PATCH 11/61] Enclosing keyboard navigation test into describe block I want to share the same context, but make a separate test for the edge-case scenario. --- .../autocomplete/__tests__/keyboard.spec.ts | 232 +++++++++--------- 1 file changed, 119 insertions(+), 113 deletions(-) diff --git a/assets/js/autocomplete/__tests__/keyboard.spec.ts b/assets/js/autocomplete/__tests__/keyboard.spec.ts index 11a3d4469..862434304 100644 --- a/assets/js/autocomplete/__tests__/keyboard.spec.ts +++ b/assets/js/autocomplete/__tests__/keyboard.spec.ts @@ -1,114 +1,120 @@ -import { init } from './context'; - -it('supports navigation via keyboard', async () => { - const ctx = await init(); - - await ctx.setInput('f'); - - await ctx.keyDown('ArrowDown'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest<>", - "suggestions": [ - "👉 forest 3", - "force field 1", - "fog 1", - "flower 1", - ], - } - `); - - await ctx.keyDown('ArrowDown'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "force field<>", - "suggestions": [ - "forest 3", - "👉 force field 1", - "fog 1", - "flower 1", - ], - } - `); - - await ctx.keyDown('ArrowDown', { ctrlKey: true }); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "flower<>", - "suggestions": [ - "forest 3", - "force field 1", - "fog 1", - "👉 flower 1", - ], - } - `); - - await ctx.keyDown('ArrowUp', { ctrlKey: true }); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest<>", - "suggestions": [ - "👉 forest 3", - "force field 1", - "fog 1", - "flower 1", - ], - } - `); - - await ctx.keyDown('Enter'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest<>", - "suggestions": [], - } - `); - - await ctx.setInput('forest, t<>, safe'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest, t<>, safe", - "suggestions": [ - "artist:test 1", - ], - } - `); - - await ctx.keyDown('ArrowDown'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest, artist:test<>, safe", - "suggestions": [ - "👉 artist:test 1", - ], - } - `); - - await ctx.keyDown('Escape'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest, artist:test<>, safe", - "suggestions": [], - } - `); - - await ctx.setInput('forest, t<>, safe'); - await ctx.keyDown('ArrowDown'); - await ctx.keyDown('Enter'); - - ctx.expectUi().toMatchInlineSnapshot(` - { - "input": "forest, artist:test<>, safe", - "suggestions": [], - } - `); +import { init, TestContext } from './context'; + +describe('Autocomplete keyboard navigation', () => { + let ctx: TestContext; + + beforeAll(async () => { + ctx = await init(); + }); + + it('supports navigation via keyboard', async () => { + await ctx.setInput('f'); + + await ctx.keyDown('ArrowDown'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest<>", + "suggestions": [ + "👉 forest 3", + "force field 1", + "fog 1", + "flower 1", + ], + } + `); + + await ctx.keyDown('ArrowDown'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "force field<>", + "suggestions": [ + "forest 3", + "👉 force field 1", + "fog 1", + "flower 1", + ], + } + `); + + await ctx.keyDown('ArrowDown', { ctrlKey: true }); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "flower<>", + "suggestions": [ + "forest 3", + "force field 1", + "fog 1", + "👉 flower 1", + ], + } + `); + + await ctx.keyDown('ArrowUp', { ctrlKey: true }); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest<>", + "suggestions": [ + "👉 forest 3", + "force field 1", + "fog 1", + "flower 1", + ], + } + `); + + await ctx.keyDown('Enter'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest<>", + "suggestions": [], + } + `); + + await ctx.setInput('forest, t<>, safe'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest, t<>, safe", + "suggestions": [ + "artist:test 1", + ], + } + `); + + await ctx.keyDown('ArrowDown'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest, artist:test<>, safe", + "suggestions": [ + "👉 artist:test 1", + ], + } + `); + + await ctx.keyDown('Escape'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest, artist:test<>, safe", + "suggestions": [], + } + `); + + await ctx.setInput('forest, t<>, safe'); + await ctx.keyDown('ArrowDown'); + await ctx.keyDown('Enter'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest, artist:test<>, safe", + "suggestions": [], + } + `); + }); }); From 8288a3b7a3774b2cdc13b2cc543976577e395e84 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 05:06:18 +0400 Subject: [PATCH 12/61] Added the test case for the keydown event with empty code --- assets/js/autocomplete/__tests__/context.ts | 2 +- .../autocomplete/__tests__/keyboard.spec.ts | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/assets/js/autocomplete/__tests__/context.ts b/assets/js/autocomplete/__tests__/context.ts index 5bf636aa7..c7e5369c9 100644 --- a/assets/js/autocomplete/__tests__/context.ts +++ b/assets/js/autocomplete/__tests__/context.ts @@ -146,7 +146,7 @@ export class TestContext { await vi.runAllTimersAsync(); } - async keyDown(code: string, params?: { ctrlKey?: boolean }) { + async keyDown(code: string, params?: { ctrlKey?: boolean; key?: string }) { fireEvent.keyDown(this.input, { code, ...(params ?? {}) }); await vi.runAllTimersAsync(); } diff --git a/assets/js/autocomplete/__tests__/keyboard.spec.ts b/assets/js/autocomplete/__tests__/keyboard.spec.ts index 862434304..3e1350343 100644 --- a/assets/js/autocomplete/__tests__/keyboard.spec.ts +++ b/assets/js/autocomplete/__tests__/keyboard.spec.ts @@ -117,4 +117,30 @@ describe('Autocomplete keyboard navigation', () => { } `); }); + + it('should handle Enter key presses with empty code property', async () => { + await ctx.setInput('f'); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "f<>", + "suggestions": [ + "forest 3", + "force field 1", + "fog 1", + "flower 1", + ], + } + `); + + await ctx.keyDown('ArrowDown'); + await ctx.keyDown('', { key: 'Enter' }); + + ctx.expectUi().toMatchInlineSnapshot(` + { + "input": "forest<>", + "suggestions": [], + } + `); + }); }); From 8379df22b07091bea100b5271fdec14ccf962499 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 05:09:14 +0400 Subject: [PATCH 13/61] Fixed `Enter` key presses not being handled properly on Android devices --- assets/js/autocomplete/index.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/assets/js/autocomplete/index.ts b/assets/js/autocomplete/index.ts index 8bffa7d0e..0acc4846a 100644 --- a/assets/js/autocomplete/index.ts +++ b/assets/js/autocomplete/index.ts @@ -2,12 +2,12 @@ import { LocalAutocompleter } from '../utils/local-autocompleter'; import * as history from './history'; import { AutocompletableInput, TextInputElement } from './input'; import { - SuggestionsPopupComponent, - Suggestions, - TagSuggestionComponent, - Suggestion, HistorySuggestionComponent, ItemSelectedEvent, + Suggestion, + Suggestions, + SuggestionsPopupComponent, + TagSuggestionComponent, } from '../utils/suggestions-view'; import { prefixMatchParts } from '../utils/suggestions-model'; import { $$ } from '../utils/dom'; @@ -243,13 +243,21 @@ class Autocomplete { if (!this.isActive() || this.input.element !== event.target) { return; } - if ((event.key === ',' || event.code === 'Enter') && this.input.type === 'single-tag') { + + let keyCode = event.code; + + // Chrome & Firefox on Android devices return empty code when "Enter" is pressed. + if (!keyCode && event.key === 'Enter') { + keyCode = event.key; + } + + if ((event.key === ',' || keyCode === 'Enter') && this.input.type === 'single-tag') { // Comma/Enter mean the end of input for the current tag in single-tag mode. this.hidePopup(`The user accepted the existing input via key: '${event.key}', code: '${event.code}'`); return; } - switch (event.code) { + switch (keyCode) { case 'Enter': { const { selectedSuggestion } = this.popup; if (!selectedSuggestion) { From 0d3e94bce295d82d1dc884874c4aa3619c805f43 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 17:11:10 +0400 Subject: [PATCH 14/61] Uninstalled `eslint-plugin-vitest` Looks like package was renamed in NPM registry to `@vitest/eslint-plugin`. This package is from same exact repository. --- assets/package-lock.json | 158 --------------------------------------- assets/package.json | 1 - 2 files changed, 159 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index 52ea697bb..87a739e1e 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -22,7 +22,6 @@ "@testing-library/jest-dom": "^6.6.3", "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.16.0", - "eslint-plugin-vitest": "^0.5.4", "stylelint": "^16.11.0", "stylelint-config-standard": "^36.0.1", "typescript-eslint": "8.17.0", @@ -2501,163 +2500,6 @@ } } }, - "node_modules/eslint-plugin-vitest": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.5.4.tgz", - "integrity": "sha512-um+odCkccAHU53WdKAw39MY61+1x990uXjSPguUCq3VcEHdqJrOb8OTMrbYlY6f9jAKx7x98kLVlIe3RJeJqoQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^7.7.1" - }, - "engines": { - "node": "^18.0.0 || >= 20.0.0" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "vitest": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.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.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "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.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.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.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.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/eslint-plugin-vitest/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.18.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.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "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": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", diff --git a/assets/package.json b/assets/package.json index a2cf1ae20..3d8a9d68e 100644 --- a/assets/package.json +++ b/assets/package.json @@ -27,7 +27,6 @@ "@testing-library/jest-dom": "^6.6.3", "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.16.0", - "eslint-plugin-vitest": "^0.5.4", "stylelint": "^16.11.0", "stylelint-config-standard": "^36.0.1", "typescript-eslint": "8.17.0", From d865b52f8a983a29a47c8ce8d135d0da502b1e50 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 17:17:15 +0400 Subject: [PATCH 15/61] Bumped `typescript-eslint` from 8.17.0 to 8.29.0 Resolves issue with conflicting dependencies when trying to install `@vitest/eslint-plugin`: npm error Could not resolve dependency: npm error dev @vitest/eslint-plugin@"*" from the root project npm error npm error Conflicting peer dependency: @typescript-eslint/utils@8.29.0 npm error node_modules/@typescript-eslint/utils npm error peer @typescript-eslint/utils@"^8.24.0" from @vitest/eslint-plugin@1.1.38 npm error node_modules/@vitest/eslint-plugin npm error dev @vitest/eslint-plugin@"*" from the root project --- assets/package-lock.json | 160 +++++++++++++++++---------------------- assets/package.json | 2 +- 2 files changed, 70 insertions(+), 92 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index 87a739e1e..5c47b98e3 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -24,7 +24,7 @@ "eslint": "^9.16.0", "stylelint": "^16.11.0", "stylelint-config-standard": "^36.0.1", - "typescript-eslint": "8.17.0", + "typescript-eslint": "8.29.0", "vitest": "^2.1.9", "vitest-fetch-mock": "^0.4.2" } @@ -1442,20 +1442,20 @@ "integrity": "sha512-jxXqkf4sBn/WV9YsOlB5fFzWo9kGafMDF62VmVC1mFF367BuRn/2txr0ZaEchPsNIvyiLckMpxO6Xz3knpC6Nw==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", - "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", + "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/type-utils": "8.17.0", - "@typescript-eslint/utils": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/type-utils": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1466,24 +1466,20 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", - "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", + "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4" }, "engines": { @@ -1494,22 +1490,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", - "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0" + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1520,15 +1512,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", - "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", + "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.17.0", - "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/utils": "8.29.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1538,18 +1530,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", - "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1560,19 +1548,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", - "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1581,10 +1569,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1612,15 +1598,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", - "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0" + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1630,21 +1616,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", - "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -4700,15 +4682,15 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/type-check": { @@ -4736,14 +4718,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.17.0.tgz", - "integrity": "sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", + "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.17.0", - "@typescript-eslint/parser": "8.17.0", - "@typescript-eslint/utils": "8.17.0" + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@typescript-eslint/utils": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4753,12 +4735,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/undici-types": { diff --git a/assets/package.json b/assets/package.json index 3d8a9d68e..5e4013dd8 100644 --- a/assets/package.json +++ b/assets/package.json @@ -29,7 +29,7 @@ "eslint": "^9.16.0", "stylelint": "^16.11.0", "stylelint-config-standard": "^36.0.1", - "typescript-eslint": "8.17.0", + "typescript-eslint": "8.29.0", "vitest": "^2.1.9", "vitest-fetch-mock": "^0.4.2" } From 78e10b0a53a9c6478b51f9ff8b17074316773050 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 17:19:23 +0400 Subject: [PATCH 16/61] Installed `@vitest/eslint-plugin` 1.1.38 --- assets/package-lock.json | 22 ++++++++++++++++++++++ assets/package.json | 1 + 2 files changed, 23 insertions(+) diff --git a/assets/package-lock.json b/assets/package-lock.json index 5c47b98e3..4e82d090d 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -21,6 +21,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@vitest/coverage-v8": "^2.1.9", + "@vitest/eslint-plugin": "^1.1.38", "eslint": "^9.16.0", "stylelint": "^16.11.0", "stylelint-config-standard": "^36.0.1", @@ -1670,6 +1671,27 @@ } } }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.38.tgz", + "integrity": "sha512-KcOTZyVz8RiM5HyriiDVrP1CyBGuhRxle+lBsmSs6NTJEO/8dKVAq+f5vQzHj1/Kc7bYXSDO6yBe62Zx0t5iaw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/utils": "^8.24.0", + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", diff --git a/assets/package.json b/assets/package.json index 5e4013dd8..1477cd882 100644 --- a/assets/package.json +++ b/assets/package.json @@ -26,6 +26,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@vitest/coverage-v8": "^2.1.9", + "@vitest/eslint-plugin": "^1.1.38", "eslint": "^9.16.0", "stylelint": "^16.11.0", "stylelint-config-standard": "^36.0.1", From e3a1b951e1e6d93188f7d16b583490a3f5af659e Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 17:21:52 +0400 Subject: [PATCH 17/61] Replaced import from `eslint-plugin-vitest` to `@vitest/eslint-plugin` --- assets/eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eslint.config.js b/assets/eslint.config.js index 51cc1e265..c39019e81 100644 --- a/assets/eslint.config.js +++ b/assets/eslint.config.js @@ -1,5 +1,5 @@ import tsEslint from 'typescript-eslint'; -import vitestPlugin from 'eslint-plugin-vitest'; +import vitestPlugin from '@vitest/eslint-plugin'; import globals from 'globals'; export default tsEslint.config( From e941708036a023d993a9afe06783e7d72230f1cd Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 17:33:55 +0400 Subject: [PATCH 18/61] Moved `init()` directly to the `describe` block As suggested by @MareStare --- assets/js/autocomplete/__tests__/keyboard.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/assets/js/autocomplete/__tests__/keyboard.spec.ts b/assets/js/autocomplete/__tests__/keyboard.spec.ts index 3e1350343..0eab9d020 100644 --- a/assets/js/autocomplete/__tests__/keyboard.spec.ts +++ b/assets/js/autocomplete/__tests__/keyboard.spec.ts @@ -1,11 +1,7 @@ import { init, TestContext } from './context'; -describe('Autocomplete keyboard navigation', () => { - let ctx: TestContext; - - beforeAll(async () => { - ctx = await init(); - }); +describe('Autocomplete keyboard navigation', async () => { + const ctx: TestContext = await init(); it('supports navigation via keyboard', async () => { await ctx.setInput('f'); From 4fd8de8a13a99c9bd66f6fc8121896cbb41c4b54 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 2 Apr 2025 18:20:48 +0400 Subject: [PATCH 19/61] Revert "Moved `init()` directly to the `describe` block" This reverts commit e68d7c3e1c9943edc76db00c3c07b4b201501c05. --- assets/js/autocomplete/__tests__/keyboard.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/assets/js/autocomplete/__tests__/keyboard.spec.ts b/assets/js/autocomplete/__tests__/keyboard.spec.ts index 0eab9d020..3e1350343 100644 --- a/assets/js/autocomplete/__tests__/keyboard.spec.ts +++ b/assets/js/autocomplete/__tests__/keyboard.spec.ts @@ -1,7 +1,11 @@ import { init, TestContext } from './context'; -describe('Autocomplete keyboard navigation', async () => { - const ctx: TestContext = await init(); +describe('Autocomplete keyboard navigation', () => { + let ctx: TestContext; + + beforeAll(async () => { + ctx = await init(); + }); it('supports navigation via keyboard', async () => { await ctx.setInput('f'); From 12959f8f5c80c69ed8e75d22014575494f83af4e Mon Sep 17 00:00:00 2001 From: mdashlw Date: Mon, 24 Mar 2025 05:38:53 +0000 Subject: [PATCH 20/61] Respect IP mask when looking up users --- lib/philomena_web/controllers/ip_profile_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/philomena_web/controllers/ip_profile_controller.ex b/lib/philomena_web/controllers/ip_profile_controller.ex index c4909fae8..5406a7df2 100644 --- a/lib/philomena_web/controllers/ip_profile_controller.ex +++ b/lib/philomena_web/controllers/ip_profile_controller.ex @@ -13,7 +13,7 @@ defmodule PhilomenaWeb.IpProfileController do user_ips = UserIp - |> where(ip: ^ip) + |> where(fragment("? >>= ip", ^ip)) |> order_by(desc: :updated_at) |> preload(:user) |> Repo.all() From 6dcc50fc1737adf265a815bf559831e761d3a713 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Sun, 23 Mar 2025 09:45:05 +0000 Subject: [PATCH 21/61] Log DNP entry transitions --- .../controllers/admin/dnp_entry/transition_controller.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex b/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex index 8f0fc52e0..784cd0859 100644 --- a/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex +++ b/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex @@ -16,6 +16,7 @@ defmodule PhilomenaWeb.Admin.DnpEntry.TransitionController do {:ok, dnp_entry} -> conn |> put_flash(:info, "Successfully updated DNP entry.") + |> moderation_log(details: &log_details/2, data: dnp_entry) |> redirect(to: ~p"/dnp/#{dnp_entry}") {:error, _changeset} -> @@ -31,4 +32,11 @@ defmodule PhilomenaWeb.Admin.DnpEntry.TransitionController do _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) end end + + defp log_details(_action, dnp_entry) do + %{ + body: "#{String.capitalize(dnp_entry.aasm_state)} DNP entry #{dnp_entry.id}", + subject_path: ~p"/dnp/#{dnp_entry}" + } + end end From acbc01b416ff92364dbce89c11603ff182905600 Mon Sep 17 00:00:00 2001 From: mdashlw Date: Mon, 24 Mar 2025 02:48:15 +0000 Subject: [PATCH 22/61] Preload tag in dnp transition controller --- .../admin/dnp_entry/transition_controller.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex b/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex index 784cd0859..128253da6 100644 --- a/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex +++ b/lib/philomena_web/controllers/admin/dnp_entry/transition_controller.ex @@ -5,7 +5,13 @@ defmodule PhilomenaWeb.Admin.DnpEntry.TransitionController do alias Philomena.DnpEntries plug :verify_authorized - plug :load_resource, model: DnpEntry, only: [:create], id_name: "dnp_entry_id", persisted: true + + plug :load_resource, + model: DnpEntry, + only: [:create], + id_name: "dnp_entry_id", + preload: [:tag], + persisted: true def create(conn, %{"state" => new_state}) do case DnpEntries.transition_dnp_entry( @@ -35,7 +41,8 @@ defmodule PhilomenaWeb.Admin.DnpEntry.TransitionController do defp log_details(_action, dnp_entry) do %{ - body: "#{String.capitalize(dnp_entry.aasm_state)} DNP entry #{dnp_entry.id}", + body: + "#{String.capitalize(dnp_entry.aasm_state)} DNP entry #{dnp_entry.id} on #{dnp_entry.tag.name}", subject_path: ~p"/dnp/#{dnp_entry}" } end From f03de446915300ccf2f415d1ddab8d3401d8da7a Mon Sep 17 00:00:00 2001 From: mdashlw Date: Mon, 3 Mar 2025 04:10:58 +0000 Subject: [PATCH 23/61] Update override display logic for tags --- lib/philomena_web/templates/search/index.html.slime | 2 +- lib/philomena_web/views/search_view.ex | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/philomena_web/templates/search/index.html.slime b/lib/philomena_web/templates/search/index.html.slime index 130d7dd41..91178d519 100644 --- a/lib/philomena_web/templates/search/index.html.slime +++ b/lib/philomena_web/templates/search/index.html.slime @@ -1,5 +1,5 @@ = cond do - - Enum.any?(@images) or override_display(@tags) -> + - Enum.any?(@images) or override_display(@conn, @tags) -> = render PhilomenaWeb.ImageView, "index.html", conn: @conn, tags: @tags, images: @images, header: "Searching for #{@conn.params["q"]}", route: fn p -> ~p"/search?#{p}" end, scope: scope(@conn) - assigns[:error] -> diff --git a/lib/philomena_web/views/search_view.ex b/lib/philomena_web/views/search_view.ex index d03b689c3..70f049bba 100644 --- a/lib/philomena_web/views/search_view.ex +++ b/lib/philomena_web/views/search_view.ex @@ -4,9 +4,10 @@ defmodule PhilomenaWeb.SearchView do def scope(conn), do: PhilomenaWeb.ImageScope.scope(conn) def hides_images?(conn), do: can?(conn, :hide, %Philomena.Images.Image{}) - def override_display([{_tag, _description, dnp_entries}]) do - Enum.any?(dnp_entries) + def override_display(conn, [{tag, _description, dnp_entries}]) do + tag.images_count > 0 or Enum.any?(dnp_entries) or + (present?(tag.mod_notes) and can?(conn, :edit, tag)) end - def override_display(_), do: false + def override_display(_conn, _tags), do: false end From 6ce69a94d0afe3e266275476a5c5e078cf60d45b Mon Sep 17 00:00:00 2001 From: "L. Fox" Date: Fri, 4 Apr 2025 09:25:09 +0200 Subject: [PATCH 24/61] bump software versions --- docker-compose.yml | 8 ++++---- docker/app/Dockerfile | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1f13cb2f7..cef044380 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: - '5173:5173' postgres: - image: postgres:17.2-alpine + image: postgres:17.4-alpine environment: - POSTGRES_PASSWORD=postgres volumes: @@ -67,7 +67,7 @@ services: attach: false opensearch: - image: opensearchproject/opensearch:2.18.0 + image: opensearchproject/opensearch:2.19.1 volumes: - opensearch_data:/usr/share/opensearch/data - ./docker/opensearch/opensearch.yml:/usr/share/opensearch/config/opensearch.yml @@ -78,11 +78,11 @@ services: hard: 65536 valkey: - image: valkey/valkey:8.0-alpine + image: valkey/valkey:8.1-alpine attach: false files: - image: andrewgaul/s3proxy:sha-3ff077f + image: andrewgaul/s3proxy:sha-db34f6b environment: - JCLOUDS_FILESYSTEM_BASEDIR=/srv/philomena/priv/s3 volumes: diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 56fcd29d5..ead01a601 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -1,4 +1,4 @@ -FROM elixir:1.18.1-alpine +FROM elixir:1.18.3-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 \ From 725f57fcf3aac1fb8ceef7a71b2578da0ee48b10 Mon Sep 17 00:00:00 2001 From: "L. Fox" Date: Fri, 4 Apr 2025 09:25:38 +0200 Subject: [PATCH 25/61] postgres17-client --- docker/app/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index ead01a601..fddd19b3f 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -22,7 +22,7 @@ RUN (echo "https://github.com/philomena-dev/prebuilt-ffmpeg/raw/master"; cat /et librsvg \ rsvg-convert \ imagemagick \ - postgresql16-client \ + postgresql17-client \ wget \ rust \ cargo \ From baaf40ee5fec39f8809e15aaea409e2852ac1c27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:33:11 +0000 Subject: [PATCH 26/61] Bump vite in /assets Bumps and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies needed to be updated together. Updates `vite` from 6.0.11 to 6.2.5 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.2.5/packages/vite) Updates `vite` from 5.4.14 to 6.2.5 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.2.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.2.5 dependency-type: direct:production - dependency-name: vite dependency-version: 6.2.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- assets/package-lock.json | 430 +++++++++++++++++++++------------------ assets/package.json | 2 +- 2 files changed, 234 insertions(+), 198 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index 4e82d090d..0d0537b0a 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -15,7 +15,7 @@ "postcss-mixins": "^11.0.3", "postcss-simple-vars": "^7.0.1", "typescript": "^5.7.2", - "vite": "^6.0.2" + "vite": "^6.2.5" }, "devDependencies": { "@testing-library/dom": "^10.4.0", @@ -361,9 +361,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], @@ -377,9 +377,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], @@ -409,9 +409,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], @@ -425,9 +425,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], @@ -441,9 +441,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], @@ -457,9 +457,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], @@ -473,9 +473,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], @@ -489,9 +489,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], @@ -505,9 +505,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], @@ -521,9 +521,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], @@ -537,9 +537,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], @@ -553,9 +553,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], @@ -569,9 +569,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], @@ -585,9 +585,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], @@ -601,9 +601,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], @@ -617,9 +617,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], @@ -633,9 +633,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], @@ -649,9 +649,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], @@ -665,9 +665,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], @@ -681,9 +681,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], @@ -697,9 +697,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], @@ -713,9 +713,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], @@ -729,9 +729,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], @@ -745,9 +745,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], @@ -1117,228 +1117,260 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz", + "integrity": "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz", + "integrity": "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz", + "integrity": "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz", + "integrity": "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz", + "integrity": "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz", + "integrity": "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz", + "integrity": "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz", + "integrity": "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz", + "integrity": "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz", + "integrity": "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz", + "integrity": "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==", "cpu": [ "loong64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz", + "integrity": "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz", + "integrity": "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz", + "integrity": "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz", + "integrity": "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==", "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz", + "integrity": "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz", + "integrity": "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz", + "integrity": "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz", + "integrity": "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz", + "integrity": "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1409,9 +1441,10 @@ "dev": true }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -2386,9 +2419,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2398,31 +2431,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/escalade": { @@ -3758,9 +3791,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -3775,8 +3808,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4012,11 +4046,12 @@ } }, "node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz", + "integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==", + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -4026,25 +4061,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.39.0", + "@rollup/rollup-android-arm64": "4.39.0", + "@rollup/rollup-darwin-arm64": "4.39.0", + "@rollup/rollup-darwin-x64": "4.39.0", + "@rollup/rollup-freebsd-arm64": "4.39.0", + "@rollup/rollup-freebsd-x64": "4.39.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.39.0", + "@rollup/rollup-linux-arm-musleabihf": "4.39.0", + "@rollup/rollup-linux-arm64-gnu": "4.39.0", + "@rollup/rollup-linux-arm64-musl": "4.39.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.39.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.39.0", + "@rollup/rollup-linux-riscv64-gnu": "4.39.0", + "@rollup/rollup-linux-riscv64-musl": "4.39.0", + "@rollup/rollup-linux-s390x-gnu": "4.39.0", + "@rollup/rollup-linux-x64-gnu": "4.39.0", + "@rollup/rollup-linux-x64-musl": "4.39.0", + "@rollup/rollup-win32-arm64-msvc": "4.39.0", + "@rollup/rollup-win32-ia32-msvc": "4.39.0", + "@rollup/rollup-win32-x64-msvc": "4.39.0", "fsevents": "~2.3.2" } }, @@ -4813,14 +4849,14 @@ "dev": true }, "node_modules/vite": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", - "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" diff --git a/assets/package.json b/assets/package.json index 1477cd882..43c9b86e0 100644 --- a/assets/package.json +++ b/assets/package.json @@ -20,7 +20,7 @@ "postcss-mixins": "^11.0.3", "postcss-simple-vars": "^7.0.1", "typescript": "^5.7.2", - "vite": "^6.0.2" + "vite": "^6.2.5" }, "devDependencies": { "@testing-library/dom": "^10.4.0", From 96611c91657affd51fca796e663c68999132cf20 Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Wed, 12 Jun 2024 20:11:43 +0200 Subject: [PATCH 27/61] improve dev seeds (wip) --- priv/repo/seeds.exs | 2 +- priv/repo/seeds/dev/communications.json | 58 ++ priv/repo/seeds/dev/images.json | 388 ++++++++ priv/repo/seeds/dev/pages.json | 70 ++ priv/repo/seeds/dev/users.json | 32 + priv/repo/seeds/pages/about.md | 7 + priv/repo/seeds/pages/advertising.md | 11 + priv/repo/seeds/pages/api.md | 1156 +++++++++++++++++++++++ priv/repo/seeds/pages/approval.md | 43 + priv/repo/seeds/pages/contact.md | 1 + priv/repo/seeds/pages/donations.md | 1 + priv/repo/seeds/pages/faq.md | 1 + priv/repo/seeds/pages/markdown.md | 395 ++++++++ priv/repo/seeds/pages/privacy.md | 1 + priv/repo/seeds/pages/rules.md | 0 priv/repo/seeds/pages/search_syntax.md | 1 + priv/repo/seeds/pages/shortcuts.md | 1 + priv/repo/seeds/pages/spoilers.md | 1 + priv/repo/seeds/pages/start.md | 1 + priv/repo/seeds/pages/tags.md | 1 + priv/repo/seeds/pages/takedowns.md | 1 + priv/repo/seeds/pages/uploading.md | 1 + priv/repo/{ => seeds}/seeds.json | 2 +- priv/repo/seeds/seeds_development.json | 2 + priv/repo/seeds_development.exs | 317 +++++-- priv/repo/seeds_development.json | 87 -- 26 files changed, 2396 insertions(+), 185 deletions(-) create mode 100644 priv/repo/seeds/dev/communications.json create mode 100644 priv/repo/seeds/dev/images.json create mode 100644 priv/repo/seeds/dev/pages.json create mode 100644 priv/repo/seeds/dev/users.json create mode 100644 priv/repo/seeds/pages/about.md create mode 100644 priv/repo/seeds/pages/advertising.md create mode 100644 priv/repo/seeds/pages/api.md create mode 100644 priv/repo/seeds/pages/approval.md create mode 100644 priv/repo/seeds/pages/contact.md create mode 100644 priv/repo/seeds/pages/donations.md create mode 100644 priv/repo/seeds/pages/faq.md create mode 100644 priv/repo/seeds/pages/markdown.md create mode 100644 priv/repo/seeds/pages/privacy.md create mode 100644 priv/repo/seeds/pages/rules.md create mode 100644 priv/repo/seeds/pages/search_syntax.md create mode 100644 priv/repo/seeds/pages/shortcuts.md create mode 100644 priv/repo/seeds/pages/spoilers.md create mode 100644 priv/repo/seeds/pages/start.md create mode 100644 priv/repo/seeds/pages/tags.md create mode 100644 priv/repo/seeds/pages/takedowns.md create mode 100644 priv/repo/seeds/pages/uploading.md rename priv/repo/{ => seeds}/seeds.json (98%) create mode 100644 priv/repo/seeds/seeds_development.json delete mode 100644 priv/repo/seeds_development.json diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 1fe20dcd8..58a20bd83 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -40,7 +40,7 @@ for model <- [Image, Comment, Gallery, Tag, Post, Report, Filter] do end resources = - "priv/repo/seeds.json" + "priv/repo/seeds/seeds.json" |> File.read!() |> Jason.decode!() diff --git a/priv/repo/seeds/dev/communications.json b/priv/repo/seeds/dev/communications.json new file mode 100644 index 000000000..43e235660 --- /dev/null +++ b/priv/repo/seeds/dev/communications.json @@ -0,0 +1,58 @@ +{ + "demos": [ + "bold is **bold**, italic is _italic_, spoiler is ||spoiler||, code is `code`, underline is __underline__, strike is ~~strike~~, sup is ^sup^, sub is %sub%.", + "inline embedded thumbnails (tsp): >>1t >>1s >>1p", + "embedded image inside a spoiler: ||who needs it anyway >>1s||", + "spoilers inside of a table\n\nHello | World\n--- | ---:\n`||cool beans!||` | ||cool beans!||" + ], + "random": [ + "Oh my gosh, foxes are just too adorable, aren't they? With their pointy little ears, fluffy tails, and mischievous grins, they look like they've just stepped out of a fairy tale. And the way they prance and play in the wild, you can't help but be charmed by their antics. They have such a wide range of colors too, from vibrant reds to cool grays and even that stunning arctic white. Each one looks like it could be the star of its own animated movie. Honestly, watching videos of them jumping around and making those cute little fox sounds can totally make my day!", + "Wolves are absolutely fascinating creatures, and their greatness really speaks for itself! They embody the wild spirit of nature with their fierce eyes, powerful builds, and majestic howls that echo through the wilderness. It's incredible how these animals operate within sophisticated social structures, showing deep loyalty and intricate communication within their packs. Their roles as apex predators help maintain the balance of their ecosystems, managing populations of other animals and thereby supporting overall biodiversity. Plus, observing their interactions and tactical hunting strategies really highlights their intelligence and adaptability. Wolves definitely deserve our respect and admiration for their vital role in the natural world.", + "Frontend JavaScript frameworks are running rampant and it's high time we talk about the chaos they're creating! Honestly, it feels like a new framework pops up every other day, each one promising to be the solution to all our development woes. Yet, what we end up with is massive complexity, steep learning curves, and interminable \"best practices\" that shift faster than you can say \"deprecated.\" Developers are forced to hop from one framework to another, which dilutes focus and fragments the community. Can we please just settle on a few solid, well-supported options rather than continually chasing the next shiny thing? It's exhausting and not sustainable. The JavaScript community would be much better served by focusing on enhancing and stabilizing the frameworks we already have, rather than pushing out yet another contender in this already overcrowded space.", + "Elixir and the Phoenix framework truly are game changers in the world of programming, especially for those looking to build highly scalable and maintainable applications. Elixir, built on the reliable Erlang VM, provides exceptional concurrency and fault tolerance, making it an outstanding choice for real-time systems. Its syntax is clean and modern, which greatly simplifies the process of writing maintainable code. When you pair Elixir with Phoenix, things get even better. Phoenix leverages the strengths of Elixir to offer impressive speeds and unparalleled responsiveness in web development. It also comes with built-in support for WebSockets, which is a blessing for building interactive, real-time applications. The community around these technologies is vibrant and supportive, constantly contributing to a growing ecosystem of packages and tools. For developers looking for a robust solution for high-performance applications, diving into Elixir and Phoenix is a no-brainer!", + "Oh, please, there's simply no contest when it comes to operating systems; Arch Linux stands leagues above the rest. I mean, if you're serious about understanding how your system works, or if you actually value control and efficiency over hand-holding and spoon-feeding, then Arch is the clear choice. Its rolling release model ensures that you are always at the bleeding edge of technology—none of this outdated, \"stable release\" nonsense. And the AUR? An unmatched repository where the breadth and depth of available packages laugh in the face of other so-called \"advanced\" distributions. Setting up an Arch system might be a day's work for a novice, but for those who excel, it's a testament to skill and a badge of honor. They say “you must be this smart to use Arch,” and frankly, it's true. Why settle for less when you can have the purity of a user-centric, endlessly customizable experience? Let the uninitiated toil with their GUI installers and automatic setups; I'll be over here, crafting a sleek, efficient system that does exactly what I need, how I need it, when I need it.", + "i use arch btw", + "Absolutely ridiculous! Here I am with a supposedly high-end PC that I paid a good chunk of money for, and what happens? It literally starts smoking out of nowhere while I'm in the middle of an important task! I mean, you'd think with all the advances in technology these days, at least \"non-combustibility\" would be a feature you can count on. Now I'm stuck dealing with potential data loss, hardware damage, and not to mention the sheer inconvenience of it all. And you better believe I'm going to be on the phone with customer support who, I bet, will try to pin this on anything but a flaw in their product. Seriously, how hard is it to make a computer that doesn't turn into a fireplace? Everyone's going digital, and yet here we are, it seems like I've set up camp at the bonfire. Unbelievable!", + "Absolutely unbelievable! You won't believe this, but the latest update just \"fixed\" the one bug I was actually benefiting from! Holding down the spacebar on my computer used to cause unusually high CPU usage, which, yes, led to it heating up, but hear me out – during the winter, this bug was my personal, little makeshift heater! It was perfect; I could work on my stuff while keeping my room cozy without cranking up the costly central heating. But now, with this latest patch, the developers have “corrected” what they deemed a bug, and just like that, my delightful little heater feature is gone. My room feels like a freezer again while I type. Seriously, what were they thinking? It's ridiculous that fixing bugs also means stripping away the unintended perks. This was a feature, not a bug, from my perspective! Bring back my CPU-heating spacebar!", + "Hey everyone! I'm on the hunt for some fellow adventurers to dive into the realms of Dungeons & Dragons. Whether you're a seasoned Dungeon Master or a curious newbie ready to take your first steps into role-playing, I'm super excited to get a group together. We can explore mysterious dungeons, battle fearsome dragons, and undertake epic quests that will be talked about for ages! I'm open to any campaign setting - whether it's navigating the political intrigue of Waterdeep or braving the untamed wilderness of Chult. Ideally, I'd love to meet weekly, either online using platforms like Roll20 or Discord, or in person if you're nearby. So grab your dice, prepare your character sheets, and let's create some unforgettable stories together. Who's with me?", + "Ah, the quest for the Holy Grail of digital art software! Where do I even begin? I've been on a wild goose chase, bouncing from program to program—starting with Draw-o-Matic, hopping over to SketchySketch, and even dabbling in the enigmatic realms of PixelPerfect Pro. Each promises to turn my clumsy scribbles into strokes of genius, yet somehow, my digital doodling still looks like abstract art gone wrong. It's like choosing between different brands of spaghetti noodles...in the end, can you really tell them apart when they're all covered in sauce?\n\nLet's be real; after my millionth failed attempt at mastering brushes and layers, I decided that true artistic talent might just be out of my reach. So, guess what? I've thrown in the traditional artist's towel and embraced our futuristic overlords – yes, AI art generators! Why struggle with pen tablets and complicated controls when a clever algorithm can whip up a masterpiece with the click of a button? So here's to letting AI do the heavy lifting while I sit back, sip my coffee, and marvel at the magic of machine-made art. Who needs skill when you have technology, right? Cheers to the easy art life!", + "Cooking breakfast, supposedly the most important meal of the day, can feel like an insurmountable chore. First, there's the waking up part, which involves peeling yourself from the comforting embrace of your bed at an ungodly hour. Then, you trudge to the kitchen, where a gauntlet of tasks awaits. You start with the basics: eggs. But the eggs don't just hop out of the shell — oh no, you have to crack them just right or else you'll be fishing out pieces of shell for what feels like hours. And let's not even get started on getting the cooking temperature perfect; too hot, and they're rubber, too cool, and they just sit there, mocking you.\n\nMeanwhile, the bread quietly burns in the toaster as you juggle pots and spatulas. And cereal? It might seem easy until you realize you're out of milk — a discovery typically made only after you've already filled your bowl. Add to that the high stakes of brewing a decent cup of coffee, which is practically a dark art involving precise timings and temperatures. By the end of this so-called 'simple' task, the kitchen looks like a cyclone hit it, you're exhausted, and it's only the start of your day. Sometimes, it seems like breakfast is just not worth the hassle.", + "Oh, PHP, where do I even begin with this relic from the early days of the web? From its inconsistent function names to a bewildering mix of underscores and camelCase, PHP often feels like a language playing a cruel joke on developers. Its error handling is a nightmare straight out of a coder's worst dreams, where one minute you think everything's running smoothly, and the next, you're greeted by a blank screen with no clues. Then there's the infamous 'PHP spaghetti code', a term so closely associated with PHP that they might as well trademark it. The language allows, even promotes, practices that would make any seasoned developer cringe, muddling logic and presentation in ways that other, more refined languages have long since evolved past.\n\nAnd let's not gloss over the security aspects—PHP makes it frighteningly easy to write code with major vulnerabilities, such as SQL injection and XSS, practically inviting hackers to a free-for-all buffet. Despite its efforts to improve with newer versions, PHP carries the burden of its chaotic past, a Frankenstein's monster of a scripting language that just can't shake off its jumbled roots. In the fast-paced world of modern web development, clinging to PHP feels like driving a rickety old car that's always one bump away from falling apart. Why suffer through the eccentricities of PHP when there are so many cleaner, more robust alternatives waiting in the wings?", + "Oh man, I've got this unstoppable craving for chicken wings! You know, nothing beats the irresistible allure of crispy, perfectly seasoned wings that just melt off the bone. Whether they're doused in a fiery hot sauce or coated in a sweet and savory glaze, each bite is a little piece of heaven. It's not just about satisfying hunger; it's an experience, a guilty pleasure that calls for piles of napkins and not caring about how messy your fingers get. Is there anything better than tucking into a mountain of these glorious wings while zoning out to your favorite show or hanging out with friends? I think not. Now, if only I could telepathically summon a plate full of them right now, that would be the dream. Seriously, anyone know where I can get the best wings delivered ASAP? Because this craving isn't going to satisfy itself!", + "You wouldn't believe it, but I just witnessed the most breathtaking art of my life! It was one of those rare, soul-stirching moments when you feel completely transported by the power of creativity. The pieces were a masterful blend of colors and textures that seemed to dance together, telling a story deeper than words ever could. Each stroke had its own pulse, vibrant with life and emotion, pulling me into a world crafted by the artist's hands. The way the light played across the canvas made it seem almost alive, as if the scenes depicted were unfolding right before my eyes. I was totally captivated, lost in the beauty and complexity of it all. It wasn't just art; it was a profound experience that left me reflecting long after I walked away. How incredible is the human ability to convey such depth and feeling through mere brushes and pigments? I'm absolutely inspired and reminded of why art is so essential to our lives.", + "Ugh, I've got to admit, the patience is wearing thin with this one user in our thread. They keep missing the point and throwing off the whole discussion with tangents that aren't exactly relevant. Just when we start getting into the meat of the topic, in they come with off-the-wall comments that grind everything to a halt. It's like they're in their own little world! I've tried steering the conversation back on track a few times, but it's like talking to a brick wall. Honestly, it's becoming a real test of my forum etiquette to keep responding politely. Maybe it's time for some subtle hints or a direct message to let them know they're derailing the discourse a bit too much?", + "Honestly, scrolling through this thread feels like I've accidentally stumbled into some kind of bizarre, AI-generated text adventure. Each comment seems more random than the last, barely connected to the one before it, like a patchwork quilt made by a robot who's only just learned about human conversation. It's almost amusing, trying to follow the logic as it hops from topic to topic with surreal, dream-like transitions. It makes you wonder if anyone's actually steering this ship, or if we're all just passengers on an auto-pilot journey through Randomville. While it's been an interesting ride, a little coherence would be nice. Maybe it's time to tighten up the topic or set some ground rules for clarity's sake!", + "A jet engine works by following the basic principle of thrust generation through Newton's third law of motion: for every action, there is an equal and opposite reaction. It begins with air intake at the front of the engine. This air is compressed by a series of fan blades, raising the air's pressure and temperature. Next, the high-pressure air enters a combustion chamber where it is mixed with fuel and ignited. The resulting high-temperature gases expand rapidly and are forced out through the rear of the engine at high speed. This expulsion of gases generates thrust, propelling the engine (and the aircraft) forward. The engine also includes turbines located after the combustion chamber, which are driven by the outgoing gases. These turbines are connected to the front fans and compressors via a shaft and help in powering them, making the process self-sustaining.", + "Can you believe it? I've been banned from my favorite forum, and let me tell you, the moderators handled it terribly. It's like they're on a power trip or something! There was hardly any warning or proper communication about why my posts were suddenly considered problematic. They just slapped a ban on my account with some generic reasoning about violating community guidelines. Talk about frustrating! It feels completely unfair, especially since I've contributed so much to that forum over the years. Their approach just killed all the camaraderie and dynamic discussions we had going. Seriously, those mods need to learn a thing or two about handling issues with a bit more tact and clarity. Guess it's time to find a new online hangout where the moderators actually appreciate their community members!", + "Well, would you look at that? I got booted from the old forum—apparently, I'm too much of a free spirit for their strict standards! But no matter, I've discovered this new, glorious haven. The moderation here is just chef's kiss—so lenient, so understanding, practically the Wild West compared to that stuffy old forum!\n\nIt's hilarious, really. Here, the moderators actually get my sense of humor and my, uh, vibrant debate style. They seem to appreciate a good controversial topic or seven, unlike those other mods who couldn't handle a little spirited discussion. At my new online home, not only can I express my 'unique' opinions freely, but it seems like almost anything goes! Who would've thought that finding a place where the rules are just guidelines would be so refreshing?\n\nI mean, it's clear now—the problem was definitely them, not me. It's almost like this new forum was made for me. Here's to hoping they continue to see the genius in my chaos!", + "Five whole seconds for a moderator response? Absolutely outrageous! In this digital age, five seconds feels like an eternity. I mean, what could they possibly be doing that's so important? It's almost as if they have lives outside of moderating this forum! Who knew that awaiting a reply could stretch into such an unbearable timeframe? It's almost comical how these delays test my patience. I could have brewed a cup of tea in that time—or at least microwaved a cup of water. It's time for a revolution in moderator response times! They need to understand that in the fast-paced world of the internet, every second counts. Let's usher in an era of instant gratification!", + "Honestly, at this point, why do we even bother with human moderators? They tire, they need breaks, and don't get me started on the personal biases! It's the 21st century, and it's high time we embraced the efficiency of AI moderation. Imagine a world where responses are instantaneous, no waiting for a moderator to finish their coffee break. An AI moderator wouldn't sleep, wouldn't take vacations, and certainly wouldn't bring any of those pesky human emotions or biases to the table.\n\nAnd consistency! Oh, the consistency we'd experience with AI—no more of this \"depends on who you get\" roulette. Every decision would be based on cold, hard logic and predefined protocols. Sure, some might argue that AI lacks the human touch, might be too rigid, or could misinterpret nuances, but I say it's a tradeoff worth exploring. Let's streamline this whole process and bring in AI that can keep up with the speed of our posts and the scale of our debates. Out with the old, in with the new—AI moderators for a faster, bias-free forum experience!", + "I just have to vent about this— the staff on this website are just unbearable! It feels like every interaction is an exercise in frustration. They move at the pace of a snail, always lagging days behind when you need urgent help. It's as if they've never heard of customer service. And their knowledge? Seems like they're always 'looking into it' or 'will get back to you shortly,' which translates to an eternity of waiting with no real answers.\n\nDon't even get me started on the technical issues. Every little glitch or bug, and their best advice? \"Try refreshing your browser\" or \"clear your cookies.\" Genius solutions, really, groundbreaking stuff. It's like they're just regurgitating lines from a poorly written script. And heaven forbid you have a real, complex issue; you might as well talk to a wall.\n\nPlus, their attitude leaves much to be desired. There's this air of indifference, as if your problems are just minor annoyances to them rather than legitimate concerns. Honestly, a bunch of unenthused, unengaged robots could probably do a better job. At least robots wouldn't have an attitude. They seriously need to up their game or consider a major overhaul of their staff because this level of service just isn't cutting it.", + "Oh, isn't it just delightful to watch those adorable little birds flit around? Their tiny beaks, the variety of vibrant colors, and those fluttery, delicate wings just fill the heart with joy. Whether they're darting delicately from branch to branch or chirping sweetly in the morning light, each bird seems to have its own charming personality. And let's not forget when they puff up into fluffy balls to keep warm – absolutely precious! Observing birds can easily brighten anyone's day, whether it's a city pigeon strutting with unexpected elegance or a brilliantly colored parrot speaking mimicry. Their playful antics and endearing behaviors are a constant reminder of nature's simple beauties and the intricate marvels of wildlife. Watching them go about their busy lives, one can't help but be enamored by their cuteness and fascinating lives.", + "Cats are just the epitome of cuteness! From their mesmerizing eyes that seem to change with their moods to their small, expressive faces framed by those adorable twitching whiskers, every aspect of a cat can tug at the heartstrings. And who can resist the charm of a cat's gentle purr, a sound that signifies the utmost contentment and can soothe any soul within hearing distance? Cats are also masters of flexibility and grace, moving with an elegance that is both impressive and endearing. Whether they're meticulously grooming themselves, curiously pawing at a dangling string, or arching their backs in that classic stretch, every action seems designed to reaffirm their status as delightful companions. Plus, their love for cozy naps in sunny spots or curled up in laps just makes them all the more adorable. Truly, cats with their quirky, independent personalities and cute antics, are a constant source of joy and amusement.", + "The printer perches regally on the windowsill, silently surveying its domain with alert, watchful eyes. Its sleek form is compact yet sturdy, often covered in hues ranging from deep black to shimmering grayscale, sometimes even featuring striking patterns. This printer is known for its unpredictable nature; one minute it's purring smoothly as it works, the next it might be spitting out paper in a sudden flurry of energy. Despite its occasional quirks, the printer is highly valued for its ability to settle into long periods of quiet, almost meditative, processing that can provide a calming presence in any home office. Loyal yet independent, the printer often demands attention on its own terms, signalling with little chirps or flashing lights when it needs more input or maintenance. An enigmatic companion, this printer continues to be a beloved fixture in homes and offices around the world, admired for both its functional elegance and mysterious charm.", + "The cat bounds through the park with unbridled joy, its tail wagging wildly as it explores every inch of its surroundings. With a loyal and loving temperament, this cat is a cherished companion, always eager to greet you with enthusiastic licks and a happy bark. Its eyes are bright and expressive, often reflecting a wide-eyed curiosity about the world. Despite its sometimes boisterous energy, the cat is known for its protective nature, always alert and ready to guard its home or snuggle up close when it senses you need a friend. Whether it's fetching a tossed ball or simply lying at your feet, the cat thrives on interaction and physical activity, embodying a spirit of adventure and loyalty. With a hearty appetite and a fondness for playful antics, this furry friend effortlessly captures the hearts of all who meet it, proving itself as a devoted and endearing member of the family.", + "Okay, hear me out on this because your entire culinary and nutritional understanding is about to be flipped on its head! What if I told you that what we've been calling \"sugar\" all this time is actually salt? It sounds wild, I know, but this is a massive conspiracy we're dealing with. Imagine the implications! Every sweet treat you've enjoyed was designed to tweak your taste perception.\n\nThink about it: companies could save massive amounts of money substituting sugar with a cheaper compound like salt, which, through certain chemical treatments, could be made to mimic sweetness. Why would they do this? Control and profit, of course! By secretly swapping sugar with treated salt, industries manipulate both the supply chain and consumer taste preferences, all while dodging the health scrutiny that real sugar faces.\n\nPlus, consider the confusion it would cause among nutritionists and health professionals—it's the perfect cover-up. They continue debating over the effects of sugar, never knowing they should have been researching salt all this time! So next time you sprinkle some \"sugar\" on your cereal or in your coffee, ask yourself: is it really sugar, or is it part of a grander scheme? Keep your eyes open; taste is deceptive!", + "Take a moment to consider the device you carry in your pocket—commonly referred to as a smartphone. Beneath its sleek exterior and user-friendly interface lies a rather unnerving capability: to monitor virtually all your activities. Equipped with cameras and microphones that are always at the ready, these smartphones can record and transmit vast amounts of personal data, often without explicit consent or knowledge. Every location visited, every conversation held, even every item searched or purchased can be tracked and stored, ostensibly to \"enhance user experience.\" Yet, one can't help but question—who really benefits from this accumulation of intimate details, and just how secure is this information against misuse? Cloaked under the guise of advancement and convenience, smartphones might just be the most willingly adopted surveillance devices in human history.", + "Good morning, everyone! I hope this message finds you all well and ready for another exciting day on our forum. Whether you're sipping your morning coffee or you're just settling in at your desk, I wanted to drop in and send a warm hello to brighten your day. Let's make today a productive one, filled with engaging discussions and plenty of new insights. Looking forward to seeing what everyone has to share today. Have a fantastic day ahead!", + "Hey everyone, I'm a bit new around here and could use a little help. Could someone guide me on how to post a comment in this forum? I've been reading through some fascinating threads and I'm eager to jump into the discussions, but I'm not quite sure where to click or how to actually get my comments up. Thanks in advance for your patience and assistance!", + "I can't believe the level of tyranny we are subjected to on this website by the moderation team it's absolutely outrageous you would think that a place designed for open discussion and sharing of ideas would be more democratic but instead we're dealing with an outright authoritarian regime where every word you say is scrutinized and even the slightest deviation from their opaque rules results in deletion or bans it's like walking on eggshells every time you type something and you never know when you'll be the next on the chopping block they need to loosen up and let the community breathe a little instead of stifling every conversation with their heavy handedness honestly it just kills the whole vibe of what could have been a vibrant discussion space." + ], + "titles": { + "first": [ + "Doing", "Summoning", "Best practices of", "Ensuring", "Creating", + "Making", "Pretending to be", "I want", "The", "Writing", + "A", "Realizing", "Retconning", "Real", "Incredible" + ], + "second": [ + "pancakes", "monsters", "furries", "people", "mods", + "admins", "devs", "foxes", "wolves", "Jeff Bezos", + "cats", "dogs", "fruits", "code", "games" + ], + "third": [ + "for fun!", "for profit!", "for fun and profit!", "for free", "thread", + "- a discussion", "- forum game", "- where to download?", "problem", "for the glory of the mods!", + "- a different perspective", "I dunno", "so I can be more popular", "for lolz", "- is it real??" + ] + } +} diff --git a/priv/repo/seeds/dev/images.json b/priv/repo/seeds/dev/images.json new file mode 100644 index 000000000..2582094ee --- /dev/null +++ b/priv/repo/seeds/dev/images.json @@ -0,0 +1,388 @@ +[ + { + "url": "https://furrycdn.org/img/view/2020/9/30/35230.jpg", + "sources": [ + "https://twitter.com/Lou_Art_93/status/1311435253026828288" + ], + "tag_input": "safe, otter, clothes, female, solo, smiling, solo female, dialogue, shirt, pants, text, signature, semi-anthro, offscreen character, green eyes, happy, standing, cute, outdoors, disney, english text, talking, zootopia, featured image, mammal, fur, door, barefoot, mrs. otterton (zootopia), brown fur, mustelid, silhouette, japanese text, brown body, artist:louart, topwear, bottomwear" + }, + { + "url": "https://derpicdn.net/img/view/2019/3/26/1995489.webm", + "sources": [ + "https://twitter.com/StormXF3/status/1110609781897814023" + ], + "tag_input": "webm, breaking the fourth wall, oc:echo, visual effects of awesome, wide eyes, weapons-grade cute, featured image, oc only, irl human, ear fluff, artist:stormxf3, dilated pupils, laptop computer, slit eyes, leaning, behaving like a cat, eyes on the prize, solo focus, sound, safe, pov, pony, photo, daaaaaaaaaaaw, fangs, female, food, amazing, eye dilation, fourth wall, that bat pony sure does love fruits, frown, hand, hnnng, human, irl, male, mare, monitor, oc, computer, bat pony, apple, animated, cute, tracking, bat pony oc, cuteness overload, ear tufts, offscreen character, looking at something, ocbetes" + }, + { + "url": "https://furrycdn.org/img/view/2020/5/2/823.webm", + "sources": [ + "https://twitter.com/RikoSakari/status/1241720594107756544" + ], + "tag_input": "safe, clothes, tail, smiling, fangs, animated, feral, paws, sitting, wings, scarf, eyes closed, open mouth, semi-anthro, whiskers, duo, oc, happy, underpaw, dancing, cute, ambiguous gender, 2020, :3, watermark, webm, horns, paw pads, hug, ferret, featured image, mammal, sound, bat wings, artist:rikosakari, buttercup (song), music, hurondance, dot eyes, webbed wings, mustelid, domestic ferret, frame by frame, uwu, oc:riko sakari, smooth as butter, jack stauber" + }, + { + "url": "https://derpicdn.net/img/view/2018/3/10/1676327.jpg", + "sources": [ + "https://www.deviantart.com/jowyb/art/Strong-Petals-734821216" + ], + "tag_input": "younger, petals, featured image, sweet dreams fuel, jackabetes, weapons-grade cute, filly applejack, color porn, flower petals, smiling, wholesome, pear butter, pearabetes, jowybean is trying to murder us, artist:jowybean, duo, bed, bright, cute, daaaaaaaaaaaw, earth pony, eyes closed, feels, female, filly, freckles, happy, heartwarming, hnnng, hug, mare, morning ponies, mother and daughter, pillow, pony, precious, applejack, safe" + }, + { + "url": "https://derpicdn.net/img/view/2018/7/15/1781742.gif", + "sources": [ + "https://www.deviantart.com/szafir87/art/Shy-Hug-754638552" + ], + "tag_input": "szafir87 is trying to murder us, simple background, sitting, hiding behind wing, solo, transparent background, you are already dead, shyabetes, featured image, sweet dreams fuel, hug request, weapons-grade cute, eye shimmer, artist:szafir87, smiling, spread wings, adorable face, animated, blinking, blushing, bronybait, chest fluff, cute, daaaaaaaaaaaw, female, fluttershy, frown, gif, hiding, hnnng, looking at you, looking down, mare, mouth hold, note, pegasus, pony, raised hoof, safe, shy" + }, + { + "url": "https://furrycdn.org/img/view/2020/7/20/9844.jpg", + "sources": [ + "https://www.deviantart.com/tomatocoup/art/The-Guard-782523190" + ], + "tag_input": "safe, clothes, teeth, tail, female, solo, fangs, anthro, solo female, oc only, shark, breasts, cloud, swimsuit, looking at something, oc, standing, water, wet, fish, crepuscular rays, fish tail, one-piece swimsuit, ocean, fins, cloudy, shark tail, lifeguard, oc:erika (ambris), artist:tomatocoup" + }, + { + "url": "https://furrycdn.org/img/view/2020/4/25/180.jpg", + "sources": [ + "https://www.deviantart.com/kaleido-art/art/Dance-with-me-838409320" + ], + "tag_input": "safe, clothes, tail, female, smiling, ear fluff, anthro, male, wolf, dress, pants, eyes closed, simple background, open mouth, duo, canine, happy, shoes, artist:kaleido-art, haru (beastars), beastars, rabbit, dancing, cheek fluff, tan background, fluff, size difference, shipping, 2020, blush sticker, shadow, legoshi (beastars), featured image, plantigrade anthro, long ears, mammal, fur, white fur, male/female, lagomorph, gray fur, white body, ambient wildlife, gray body, ambient insect, harushi (beastars), anthro/anthro, bottomwear" + }, + { + "url": "https://derpicdn.net/img/view/2015/9/23/985817.gif", + "sources": [ + "http://duocartoonist.tumblr.com/post/129677819320/see-up-my-first-rendition-of-nmm-the-nebulous" + ], + "tag_input": "spread wings, alicorn, animated, artist:anima-dos, artist:lionheartcartoon, bat pony, bat wings, bedroom eyes, castle, crown, cute, evil laugh, eyeshadow, fangs, female, flapping, gif, grin, laughing, looking at you, makeup, mare, nightmare moon, open mouth, pony, raised hoof, redesign, safe, smirk, solo, unshorn fetlocks, slit eyes, artist:duo cartoonist, raised eyebrow, ethereal mane, moonbat, the moon rises, smiling, moonabetes, bat pony alicorn, smooth as butter" + }, + { + "url": "https://furrycdn.org/img/view/2020/4/30/672.jpg", + "sources": [ + "https://twitter.com/k_b__m/status/729172237761150978" + ], + "tag_input": "safe, clothes, tail, female, fluffy, anthro, male, dialogue, paws, white background, fox, simple background, open mouth, duo, canine, green eyes, claws, underpaw, rabbit, fluff, nick wilde (zootopia), disney, colored pupils, talking, paw pads, judy hopps (zootopia), zootopia, red fox, necktie, purple eyes, featured image, mammal, artist:k_b__m, 2016, lagomorph, fluffy tail, palm pads" + }, + { + "url": "https://derpicdn.net/img/view/2015/10/3/993821.jpg", + "sources": [ + "http://cannibalus.deviantart.com/art/Sunbutt-Portrait-563994112" + ], + "tag_input": "draw me like one of your french girls, drawing, duo, eating, fat, female, food, funny, funny as hell, ice cream, levitation, magic, obese, open mouth, painting, pony, prank, princess celestia, princess luna, prone, safe, smirk, tea, teacup, teapot, telekinesis, trolluna, wallpaper, this will end in pain, goblet, :t, tongue out, featured image, caricature, duo female, this will end in tears and/or a journey to the moon, best sisters, this will not end well, artception, smiling, ethereal tail, sibling rivalry, ethereal mane, royal sisters, nailed it, chubbylestia, lidded eyes, alicorn, cutelestia, lunabetes, sweet dreams fuel, technically advanced, tabun art-battle, tabun art-battle cover, artist:cannibalus, cake, cakelestia, close enough, cute" + }, + { + "url": "https://derpicdn.net/img/view/2017/10/27/1571166.gif", + "sources": [ + "https://therealdjthed.deviantart.com/art/Super-Smile-Animation-Test-711915586" + ], + "tag_input": "featured image, 3d, 3d model, animated, blender, blinking, bouncing, cute, cycles, earth pony, female, gif, grin, happy, headbob, mare, pinkie pie, pony, safe, simple background, solo, squee, diapinkes, weapons-grade cute, perfect loop, patreon, smiling, idle animation, ponk, patreon logo, cycles render, artist:therealdjthed, therealdjthed is trying to murder us, model:djthed, breathing" + }, + { + "url": "https://derpicdn.net/img/view/2020/8/10/2419740.gif", + "sources": [ + "https://2snacks.tumblr.com/post/626019622654787584/httpsyoutubezhu1luqmn9g" + ], + "tag_input": "get stick bugged lol, princess cadance, featured image, adorawat, perfect loop, pony, pixel art, meme, female, dancing, changeling queen, changeling, artist:2snacks, animated, alicorn, wat, synchronized, sweatdrop, sweat, open mouth, cute, queen chrysalis, safe, crystal castle, princess flurry heart, ponified meme, blursed image" + }, + { + "url": "https://furrycdn.org/img/view/2021/5/22/87948.png", + "sources": [ + "https://www.deviantart.com/yakovlev-vad/art/Sisu-878339374" + ], + "tag_input": "safe, tail, female, solo, smiling, solo female, hair, feral, paws, dragon, tree, claws, eyebrows, water, disney, horns, eyelashes, dragoness, featured image, leaf, mane, side view, eastern dragon, magenta eyes, pink eyes, purple hair, blue hair, blue body, aquatic dragon, holding object, artist:yakovlev-vad, blue mane, fictional species, artifact, multicolored body, 2021, raya and the last dragon, sisu (raya and the last dragon), water bubble" + }, + { + "url": "https://furrycdn.org/img/view/2020/7/11/7370.png", + "sources": [ + "https://twitter.com/HiccupsDoesSFW/status/959798227993317376/photo/1" + ], + "tag_input": "safe, clothes, tail, female, solo, smiling, ear fluff, anthro, solo female, hair, paws, looking at you, sitting, my little pony, wolf, collar, abstract background, signature, crossed legs, canine, underpaw, fluff, species swap, paw pads, digitigrade anthro, featured image, mammal, hasbro, white outline, sunset shimmer (mlp), artist:hiccupsdoesart, friendship is magic" + }, + { + "url": "https://derpicdn.net/img/view/2016/6/20/1182765.gif", + "sources": [ + "http://megamanhxh.deviantart.com/art/Animation-A-malfunctioning-book-pony-616553371" + ], + "tag_input": "silly pony, simple background, sitting, solo, twilight sparkle, white background, wingboner, tongue out, dork, discussion in the comments, twiabetes, weapons-grade cute, majestic as fuck, behaving like a dog, lidded eyes, active stretch, smiling, artist:megamanhxh, spread wings, twilight sparkle (alicorn), safe, pony, photoshop, gif, floppy ears, flexible, flapping, ear scratch, daaaaaaaaaaaw, cute, animated, alicorn, adorkable, :p, silly, scratching, female, show accurate" + }, + { + "url": "https://furrycdn.org/img/view/2020/7/8/6299.png", + "sources": [ + "" + ], + "tag_input": "safe, tail, solo, ear fluff, tongue out, feral, paws, fox, simple background, transparent background, one eye closed, canine, claws, cheek fluff, fluff, cute, ambiguous gender, yellow eyes, holding, head fluff, colored pupils, blep, tongue, furbooru exclusive, featured image, mammal, high res, furbooru, white outline, tail hold, tail fluff, artist:chonkycrunchy, astra" + }, + { + "url": "https://derpicdn.net/img/view/2015/3/12/847656.gif", + "sources": [ + "http://nuksponyart.tumblr.com/post/113386426665/young-twilight-understands-every-cat-owners" + ], + "tag_input": "behaving like a cat, looking up, curled up, spikelove, twiabetes, featured image, bookshelf, weapons-grade cute, wide eyes, spikabetes, artist:nukilik, lidded eyes, definition of insanity, smiling, unicorn twilight, nukilik is trying to murder us, filly twilight sparkle, that pony sure does love books, frame by frame, debate in the comments, animated, annoyed, baby, baby dragon, baby spike, book, cute, cutie mark, daaaaaaaaaaaw, diabetes, dragon, eyeroll, eyes closed, female, filly, floppy ears, frown, grumpy, hnnng, ladder, levitation, library, loop, magic, male, mama twilight, nuzzling, photoshop, pony, ponyloaf, prone, reading, safe, sitting, sleeping, snuggling, spike, telekinesis, twilight sparkle, unamused, unicorn, yawn, younger" + }, + { + "url": "https://derpicdn.net/img/view/2014/10/9/739465.jpg", + "sources": [ + "https://devinian.deviantart.com/art/The-Golden-Cage-487369223" + ], + "tag_input": "politics in the comments, alicorn, artist:devinian, beautiful, cage, cake, chest, clothes, couch, crepuscular rays, cushion, derail in the comments, detailed, dress, female, fireplace, fishnets, interior, jewelry, levitation, light, magic, mare, painting, palace, philomena, photoshop, pony, princess celestia, princess luna, safe, scenery, socks, phoenix, wall of tags, technically advanced, duo, pegasus, rainbow dash, luxury, absurd resolution, tea, teacup, teapot, telekinesis, twilight sparkle, wallpaper, window, indoors, baroque, bird cage, glorious, scenery porn, featured image, chandelier, dust motes, color porn, smiling, new crown, twilight sparkle (alicorn)" + }, + { + "url": "https://furrycdn.org/img/view/2020/8/26/20869.png", + "sources": [ + "" + ], + "tag_input": "safe, tail, solo, ear fluff, anthro, feral, paws, fox, sitting, chest fluff, speech bubble, text, simple background, signature, canine, cheek fluff, fluff, cute, meme, tears, talking, purple background, furbooru exclusive, featured image, chibi, mammal, high res, fur, furbooru, front view, purple fur, artist:sorajona, artist:skodart, astra, astrael, mascot, joke, tempting fate, bottom, this will end in lewds" + }, + { + "url": "https://furrycdn.org/img/view/2021/3/17/74641.jpg", + "sources": [ + "https://twitter.com/popodunk/status/1370774397024342016" + ], + "tag_input": "safe, clothes, teeth, female, solo, smiling, anthro, solo female, looking at you, white background, dress, blushing, simple background, hat, rabbit, cute, eyebrows, flower, disney, judy hopps (zootopia), zootopia, purple eyes, featured image, floppy ears, mammal, fur, lagomorph, gray fur, sun hat, gray body, smiling at you, artist:popodunk" + }, + { + "url": "https://derpicdn.net/img/view/2021/9/16/2701452.png", + "sources": [ + "http://viwrastupr.deviantart.com/art/My-Little-Pony-Friendship-is-Magic-667085058" + ], + "tag_input": "trixie's wagon, alicorn, alicorn amulet, angel bunny, apple, apple bloom, applejack, artist:viwrastupr, bag, balcony, beautiful, big macintosh, blaze, blossomforth, book, bow, bridge, canterlot, canyon, cape, caramel, castle, cello, changeling, cloak, clothes, cloud, cloud kicker, cloudchaser, cloudsdale, clubhouse, crib, crown, crusaders clubhouse, crystal ball, crystal empire, cup, cute, cutie mark crusaders, daily deviation, daisy, derpy hooves, discord, dj pon-3, doctor whooves, dragon, drink, element of magic, epic, everfree forest, eyes closed, fancypants, fascinating, female, fleetfoot, fleur-de-lis, flitter, flower, flower trio, flower wishes, fluttershy, flying, food, friends, fruit, gilda, glass, glasses, glowing horn, goggles, grass, griffon, group, gummy, hair bow, hat, jewelry, lake, lemon hearts, light, lily, lyra heartstrings, magic, male, mane seven, mane six, minuette, moondancer, mountain, mountain range, mouth hold, multicolored hair, necktie, night, oc, opalescence, open mouth, owlowiscious, park, path, pegasus, philomena, phoenix, pillow, pinkie pie, playing, poison joke, pond, pony, ponyville, potion, princess cadance, princess celestia, princess luna, queen chrysalis, rainbow dash, raised hoof, rarity, raven, reading, river, rose, roseluck, safe, salad, scarf, scenery, school, scootaloo, self ponidox, shining armor, sitting, sky, sleeping, soarin', solar system, spike, spitfire, stage, starry night, stars, statue, sunburst, surprise, surprised, swarm, sweetie belle, sweetie drops, table, tank, telekinesis, tiara, timber wolf, time turner, toffee, tree, trixie, trixie's cape, twilight sparkle, twinkleshine, uniform, vinyl scratch, water, waterfall, wings, winona, wonderbolts, wonderbolts uniform, zebra, zecora, zecora's hut, looking at each other, bon bon, regalia, canterlot castle, pulling, father and daughter, scenery porn, octavia melody, balancing, lily valley, featured image, underhoof, bookshelf, trixie's hat, night sky, greta, castle of the royal pony sisters, large wings, twilight's castle, cello bow, lidded eyes, the hall of friendship, backwards cutie mark, alicorn hexarchy, color porn, starlight glimmer, cutie map, too big for derpibooru, griffonstone, castle griffonstone, smiling, tantabus, spread wings, gabby, tree of harmony, majestic, curved horn, oc:fausticorn, canterlot five, princess flurry heart, wall of tags, multicolored tail, colored pupils, bow (instrument), magnum opus, astronomical detail, twilight sparkle (alicorn), musical instrument, absurd resolution, technically advanced, sweet apple acres" + }, + { + "url": "https://derpicdn.net/img/view/2013/2/3/232093.gif", + "sources": [ + "http://www.reddit.com/r/mylittlepony/comments/17s485/did_someone_ask_for_a_gif_or_apng_of_dash_and/" + ], + "tag_input": "dashabetes, shifty eyes, artist:marminatoror, just for sidekicks, nose rub, transparent background, sleepless in ponyville, simple background, season 3, filly, pegasus, adventure in the comments, scootalove, safe, upvote event horizon, smiling, tsunderainbow, nose kiss, animated, scootaloo, boop, cute, cutealoo, daaaaaaaaaaaw, duo, derail in the comments, edit, eyes closed, female, gif, grin, happy, hnnng, looking around, mare, nuzzling, pony, rainbow dash, saddle bag, weapons-grade cute, tsundere, sweet dreams fuel" + }, + { + "url": "https://furrycdn.org/img/view/2020/7/5/5709.png", + "sources": [ + "https://twitter.com/2d10art/status/1279807434102628352/photo/1" + ], + "tag_input": "safe, clothes, tail, female, solo, ear fluff, anthro, solo female, paws, looking at you, signature, hat, pokémon, underpaw, cheek fluff, fluff, braixen, forest, outdoors, head fluff, raised tail, nintendo, digitigrade anthro, featured image, leaf, amber eyes, kneeling, yellow fur, mammal, fur, front view, tail fluff, crouching, white fur, shoulder fluff, orange fur, artist:2d10art, fictional species, technical advanced, starter pokémon" + }, + { + "url": "https://derpicdn.net/img/view/2014/3/20/580031.gif", + "sources": [ + "http://misterdavey.tumblr.com/post/80134333183/lesbian-fantasies" + ], + "tag_input": "gala dress, clothes, candle, bubble, blushing, bipedal, bedroom eyes, pegasus, unicorn, eyes closed, fantasizing, discussion in the comments, female, flower, food, frame, crush, hnnng, hug, imagination, juice, diabetes, juice box, kissing, lesbian, nightgown, nose wrinkle, nuzzling, out of character, picture, picture frame, plate, artist:misterdavey, animated, adventure in the comments, plushie, pony, rainbow dash, rainbow dash always dresses in style, rainbow dash's house, raridash, rarity, refrigerator, rose, safe, see-through, shipping, snuggling, spoon, standing, table, waifu dinner, not creepy, plates, spiderman thread, eye contact, dashabetes, featured image, weapons-grade cute, raribetes, jontron thread, eye shimmer, doctor who thread, waltz, rarity plushie, smiling, spread wings, wall of tags, crush plush, misterdavey is trying to murder us, jontron in the comments, doctor who in the comments, spiderman in the comments, dinner, doll, dress, derail in the comments, useless source url, source needed, dead source, dancing, daaaaaaaaaaaw, cute, cuddling" + }, + { + "url": "https://derpicdn.net/img/view/2015/4/20/878570.gif", + "sources": [ + "http://supereddit-blog.tumblr.com/post/116885770608/sweetie-belle-gets-her-cutiemark" + ], + "tag_input": "special talent, sweetie belle, telekinesis, vinyl scratch, wat, gritted teeth, diamondbelle, artist:superedit, the great and powerful superedit, octavia melody, wide eyes, unexpected, cutiespark, discovery family logo, bloom and gloom, music judges meme, smiling, hoof hold, what the hay?, funny, grin, implied shipping, frown, judges, lesbian, magic, season 5, gif, earth pony, unicorn, animated, bedroom eyes, bipedal, blushing, cute, cutie mark, diamond tiara, dj pon-3, edit, edited screencap, embarrassed, facehoof, female, meme, open mouth, photoshop, pony, rarity, safe, score, screencap, shipping" + }, + { + "url": "https://furrycdn.org/img/view/2020/4/20/0.png", + "sources": [ + "" + ], + "tag_input": "safe, tail, solo, fluffy, fox, eyes closed, simple background, transparent background, canine, vector, cheek fluff, fluff, ambiguous gender, vulpine, head fluff, furbooru exclusive, .svg available, featured image, meta, mammal, svg, fur, ambiguous form, furbooru, logo, artist:aureai, digital art, purple fur, fluffy tail, astra, mascot, it begins" + }, + { + "url": "https://derpicdn.net/img/view/2015/1/3/798402.gif", + "sources": [ + "http://nuksponyart.tumblr.com/post/106937983375/shining-giving-his-litle-sister-a-pony-version-of" + ], + "tag_input": "sweet dreams fuel, weapons-grade cute, brother and sister, twilight sparkle, duckery in the comments, artist:nukilik, sibling bonding, equestria's best brother, smiling, shining adorable, nukilik is trying to murder us, sweet, siblings, shining armor, safe, pony, ponies riding ponies, piggyback ride, photoshop, open mouth, hnnng, heartwarming, happy, grin, filly, diabetes, daaaaaaaaaaaw, cute, bouncing, bbbff, animated, duo, younger, filly twilight sparkle, female, gif, twilight riding shining armor, riding, unicorn, unicorn twilight, twiabetes, frame by frame, featured image, equestria's best big brother" + }, + { + "url": "https://furrycdn.org/img/view/2021/12/31/133675.png", + "sources": [ + "https://www.deviantart.com/yakovlev-vad/art/Faun-Elora-762586083" + ], + "tag_input": "safe, tail, female, solo, pubic fluff, smiling, ear fluff, anthro, solo female, breasts, jewelry, chest fluff, cleavage, tree, green eyes, fluff, cute, bracelet, outdoors, spyro the dragon (series), eyelashes, mammal, sexy, fur, faun, tail fluff, shoulderless, shoulder fluff, orange fur, artist:yakovlev-vad, elora (spyro), adorasexy, minidress, strapless, fictional species" + }, + { + "url": "https://furrycdn.org/img/view/2020/5/6/1252.jpg", + "sources": [ + "https://www.deviantart.com/tamberella/art/Eevee-s-Rainbow-790535422" + ], + "tag_input": "safe, tail, feral, paws, eevee, reflection, grass, eeveelution, vaporeon, pokémon, sylveon, jolteon, neck fluff, flareon, glaceon, espeon, leafeon, fluff, umbreon, fire, ambiguous gender, group, artist:tamberella, nintendo, fish tail, running, featured image, leaf, 2019, yellow fur, mammal, blue fur, fur, fins, digital art, brown fur, pink fur, black fur, purple fur, green fur, orange fur, fictional species, color porn, technical advanced" + }, + { + "url": "https://derpicdn.net/img/view/2016/5/6/1147843.gif", + "sources": [ + "http://trombonyponypie.deviantart.com/art/Wakey-Wakey-Animated-Gif-607274893" + ], + "tag_input": "gif, artist:trombonyponypie, smiling, visual effects of awesome, weapons-grade cute, underhoof, diapinkes, yawn, waking up, stretching, solo, sleeping, safe, realistic, pony, pinkie pie, pillow, morning ponies, looking at you, hnnng, happy, fluffy, eyes closed, earth pony, diabetes, derail in the comments, cute, 3d, c:, blender, eyes open, smiling at you, on side, female, blanket, sweet dreams fuel, bed, animated, adventure in the comments, detailed hair" + }, + { + "url": "https://furrycdn.org/img/view/2021/3/23/76073.png", + "sources": [ + "https://twitter.com/wolfypon/status/1374492788742496261" + ], + "tag_input": "safe, tail, female, solo, ear fluff, solo female, oc only, feral, paws, fox, chest fluff, simple background, transparent background, oc, canine, commission, cheek fluff, neck fluff, fluff, eyebrows, head fluff, eyelashes, featured image, mammal, blue fur, cyan eyes, butt fluff, high res, fur, tail fluff, shoulder fluff, artist:wolfypon, socks (leg marking), blue body, multicolored fur, firefox (browser), vixen, globe, 2021, oc:double colon" + }, + { + "url": "https://derpicdn.net/img/view/2015/3/27/858027.gif", + "sources": [ + "https://derpibooru.org/images/858027" + ], + "tag_input": "artist:sampodre, gif, creepy awesome, silhouette, 3d, adventure in the comments, animated, changeling, cinemagraph, epic, female, gif party, glowing eyes, open mouth, photoshop, queen chrysalis, rain, safe, sharp teeth, smirk, solo, wet, featured image, visual effects of awesome, derpibooru exclusive" + }, + { + "url": "https://derpicdn.net/img/view/2015/11/21/1026784.gif", + "sources": [ + "https://twitter.com/ziroro326/status/667726177813958656" + ], + "tag_input": "missing cutie mark, adorkable, alicorn, animated, black and white, cute, dancing, derp, ear twitch, female, floppy ears, grayscale, kicking, mare, monochrome, party hard, pixiv, pony, safe, silly, silly pony, simple background, solo, swing, twilight sparkle, white background, dork, :o, twiabetes, sweet dreams fuel, weapons-grade cute, artist:jirousan, do the sparkle, twilight sparkle (alicorn), jirousan is trying to murder us, club can't handle me, frame by frame" + }, + { + "url": "https://furrycdn.org/img/view/2020/7/23/11106.gif", + "sources": [ + "https://ostinlein.tumblr.com/post/617228745665839104/characters-belong" + ], + "tag_input": "safe, clothes, teeth, tail, fangs, anthro, male, oc only, piercing, animated, sitting, dog, shirt, pants, kissing, bird, signature, looking at each other, hat, duo, oc, gif, canine, claws, cheek fluff, neck fluff, fluff, cute, beak, scenery, holding, head fluff, shipping, hand hold, drink, digitigrade anthro, indoors, necktie, feathers, featured image, sharp teeth, mammal, hand on face, fur, tail fluff, glass, wine glass, brown fur, bird feet, males only, male/male, frame by frame, bar, tail feathers, oc x oc, red feathers, yellow feathers, tan fur, suspenders, artist:ostinlein, oc:tyler, oc:fletcher, galliform, art deco, topwear, smooth as butter, bottomwear" + }, + { + "url": "https://furrycdn.org/img/view/2020/8/29/22679.gif", + "sources": [ + "https://www.furaffinity.net/view/25506150/" + ], + "tag_input": "safe, clothes, solo, anthro, male, animated, paws, solo male, goat, abstract background, signature, open mouth, gif, fluff, cute, undertale, asriel dreemurr (undertale), flower, head fluff, tongue, featured image, floppy ears, black eyes, mammal, bovid, frame by frame, sneezing, artist:absolutedream" + }, + { + "url": "https://furrycdn.org/img/view/2020/4/25/267.gif", + "sources": [ + "https://inkbunny.net/s/1766018" + ], + "tag_input": "safe, clothes, female, solo, anthro, solo female, deer, oc only, breasts, freckles, animated, eyes closed, skirt, oc, gif, happy, dancing, cute, artist:kanashiipanda, book of lust, oc:julia woods, mammal, frame by frame, ocbetes, smooth as butter, bottomwear" + }, + { + "url": "https://derpicdn.net/img/view/2017/3/10/1383501.gif", + "sources": [ + "http://deannart.deviantart.com/art/Preview-2-Slice-Of-Life-668121278" + ], + "tag_input": "smooth as butter, glare, hilarious in hindsight, magic, open mouth, pony, frown, eyes closed, earth pony, drool, luna is not amused, frame by frame, alicorn, animated, marriage, artist:deannart, blinking, fiery shimmer, cutelestia, cute, mare, female, nintendo, featured image, wedding, bored, preview, princess celestia, princess luna, safe, sigh, sitting, sleeping, snoring, unamused, unicorn, gritted teeth, sunset shimmer, underhoof, maud pie, majestic as fuck, gamer sunset, lidded eyes, gif, slice of life (episode), smiling, hoof hold, nintendo switch" + }, + { + "url": "https://furrycdn.org/img/view/2020/8/19/18670.jpg", + "sources": [ + "https://twitter.com/Ruribec/status/1190149320374284289" + ], + "tag_input": "safe, clothes, teeth, female, solo, smiling, fangs, anthro, solo female, hair, oc only, paws, looking at you, sky, dress, cat, tree, signature, oc, claws, grass, underpaw, cheek fluff, standing, fluff, feline, yellow eyes, raised leg, outdoors, night, food, paw pads, digitigrade anthro, featured image, sharp teeth, mammal, blue fur, ear tuft, fur, front view, slit pupils, barefoot, fence, halloween, white fur, holiday, gray fur, gray hair, white body, graveyard, pumpkin, gray body, vegetables, artist:ruribec, oc:kelly (ruribec), cemetery" + }, + { + "url": "https://derpicdn.net/img/view/2015/3/22/854962.gif", + "sources": [ + "http://joshng.deviantart.com/art/Rainbow-Rockin-Animation-521841679" + ], + "tag_input": "ponytail, human coloration, musical instrument, alternate hairstyle, animated, awesome, boots, clothes, decepticon, equestria girls, eyes closed, female, guitar, headbang, human, humanized, jacket, kneesocks, metal, rainbow dash, rainbow socks, safe, shirt, shorts, simple background, skinny, smirk, socks, solo, transformers, white background, denim, electric guitar, striped socks, perfect loop, full body, artist:joshng, smiling, smooth as butter" + }, + { + "url": "https://furrycdn.org/img/view/2020/10/28/42897.png", + "sources": [ + "https://www.furaffinity.net/view/33574531/" + ], + "tag_input": "safe, clothes, tail, female, solo, anthro, solo female, hair, breasts, bra, panties, underwear, cell phone, phone, green eyes, rabbit, cute, angry, colored pupils, eyelashes, indoors, plushie, long hair, featured image, long ears, kneeling, mammal, tank top, sexy, fur, front view, short tail, brown hair, brown fur, all fours, pale belly, small breasts, gloves (arm marking), socks (leg marking), lagomorph, furniture, multicolored fur, game controller, tan fur, frame, adorasexy, frowning, brown body, tan body, madorable, topwear, pouting, artist:autumndeer" + }, + { + "url": "https://furrycdn.org/img/view/2021/1/18/62645.jpg", + "sources": [ + "https://twitter.com/ActuallyYshanii/status/1350865352243224581" + ], + "tag_input": "safe, clothes, tail, female, solo, anthro, solo female, paws, fox, thigh highs, heart, snow, canine, cheek fluff, fluff, cute, raised tail, blue eyes, leg warmers, face down ass up, digitigrade anthro, featured image, mammal, legwear, fur, tail wag, front view, three-quarter view, toeless legwear, orange fur, snowfall, brown nose, cream fur, orange body, vixen, cream body, artist:yshanii" + }, + { + "url": "https://furrycdn.org/img/view/2020/6/21/3578.png", + "sources": [ + "https://www.furaffinity.net/view/36893257/" + ], + "tag_input": "safe, tail, female, solo, smiling, solo female, hair, feral, paws, cloud, eyes closed, sleeping, lying down, underpaw, fluff, scenery, goggles, hammer, prone, scenery porn, featured image, blue fur, fur, front view, tail fluff, ringtail, ratchet & clank, lombax, feralized, prosthetics, artist:viwrastupr, white hair, prosthetic arm, fictional species, technical advanced, rivet (r&c)" + }, + { + "url": "https://derpicdn.net/img/view/2017/3/6/1381041.gif", + "sources": [ + "http://luminaura.deviantart.com/art/Rubbing-all-the-princesses-cheeks-656801151" + ], + "tag_input": "twiabetes, sweet dreams fuel, weapons-grade cute, wide eyes, lunabetes, patreon, alicorn tetrarchy, artist:lumineko, lidded eyes, lumineko is trying to murder us, smiling, spread wings, cutedance, patreon logo, lumineko's nuzzling princesses, twilight sparkle (alicorn), non-consensual nuzzling, luna is not amused, daaaaaaaaaaaw, animated, blushing, c:, crown, cute, cutelestia, cheek to cheek, eyes closed, alicorn, female, floppy ears, flower, frown, gif, hape, hnnng, hug, jewelry, mare, nuzzling, open mouth, pony, princess cadance, princess celestia, princess luna, rubbing, safe, snuggling, surprised, twilight sparkle, varying degrees of want, wink, regalia, :t, :o, one eye closed" + }, + { + "url": "https://furrycdn.org/img/view/2021/3/19/75280.gif", + "sources": [ + "https://www.furaffinity.net/view/32400757/" + ], + "tag_input": "safe, female, solo, smiling, solo female, hair, oc only, tongue out, animated, feral, white background, eyes closed, blushing, simple background, open mouth, oc, gif, cheek fluff, fluff, feline, cute, shy, lynx, blep, feather, tongue, featured image, 2019, mammal, teal eyes, ear tuft, fur, low res, pale belly, frame by frame, spotted fur, artist:tuwka, feather in hair, solo ambiguous, hair accessory, oc:kamari" + }, + { + "url": "https://furrycdn.org/img/view/2020/8/8/15561.jpg", + "sources": [ + "https://www.furaffinity.net/view/31801339/" + ], + "tag_input": "safe, tail, female, solo, smiling, ear fluff, anthro, solo female, hair, oc only, butt, looking at you, fox, bird, abstract background, signature, oc, canine, fluff, toy, cute, bathtub, water, wet, raised tail, blue eyes, duck, bathroom, indoors, featured image, floppy ears, mammal, sexy, ears, fur, tail fluff, the ass was fat, bubbles, orange hair, 2018, orange fur, bath, >:3, mischievous, adorasexy, cream fur, orange body, vixen, artist:amarihel, bubble bath, rubber duck, oc:tfs (amarihel), waterfowl, cream body" + }, + { + "url": "https://derpicdn.net/img/view/2019/1/16/1937202.gif", + "sources": [ + "" + ], + "tag_input": "animated, bedroom eyes, blinking, blowing, derpibooru, dialogue, downvote, earth pony, everything is ruined, female, flash, floppy ears, frown, gif, glowing horn, green eyes, happy, levitation, looking back, looking down, magic, mare, meta, metamorphosis, now you fucked up, oc, open mouth, ponified, pony, red eyes, sad, safe, simple background, sisters, telekinesis, transformation, unicorn, upvote, white background, wink, looking at each other, looking up, one eye closed, oc only, featured image, wide eyes, derpibooru exclusive, smiling, artist:justisanimation, oc:upvote, oc:downvote, derpibooru ponified, downvote's downvotes, shook, this will end in pain, ear twitch, shocked, horrified, shrunken pupils, wall of tags" + }, + { + "url": "https://derpicdn.net/img/view/2014/8/18/702641.png", + "sources": [ + "http://ukulilia.deviantart.com/art/Coffee-to-stay-awake-all-night-475492803" + ], + "tag_input": "smug, safe, realistic, princess luna, artist:mykegreywolf, pony, mug, technically advanced, makeup, magic, female, looking at you, translated in the comments, alternate hairstyle, lips, high res, german, eyeshadow, detailed, cute, alicorn, artist:katputze, collaboration, coffee mug, coffee, beautiful, photoshop elements, smiling, lidded eyes, lunabetes, featured image, 2014, telekinesis, solo" + }, + { + "url": "https://derpicdn.net/img/view/2019/5/10/2035594.gif", + "sources": [ + "https://twitter.com/nastylittlepest/status/1126899230151467008" + ], + "tag_input": "sweet dreams fuel, frame by frame, ocbetes, lidded eyes, smiling, artist:angrylittlerodent, oc:pezzhorse, wholesome, rodent is trying to murder us, oc:rodentmod, snuggling, sleeping, safe, precious, pony, pillow, oc, nuzzling, mare, male, hug, hnnng, gif, frown, floppy ears, female, eyes closed, earth pony, ear twitch, dark, daaaaaaaaaaaw, cute, cuddling, couple, blanket, dead source, bed, duo, animated, :<, stallion, unicorn, ear fluff, oc only, featured image, weapons-grade cute" + }, + { + "url": "https://derpicdn.net/img/view/2012/1/2/0.jpg", + "sources": [ + "" + ], + "tag_input": "chair, eyes, female, grin, hilarious in hindsight, adventure in the comments, image macro, derpibooru legacy, cigar, building, smiling, featured image, gritted teeth, swinging, stallion, spider-man, smoking, sitting, safe, pony, ponified, phone, pegasus, parody, paper, necktie, muffin, meme, mare, male, mail, letter, j. jonah jameson, it begins, derpy hooves, bag, song in the comments, artist needed" + }, + { + "url": "https://derpicdn.net/img/view/2015/9/26/988000.gif", + "sources": [ + "https://derpibooru.org/988000" + ], + "description": "Fairly large GIF (~23MB), use to test WebM stuff.", + "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof" + }, + { + "url": "https://derpicdn.net/img/2012/1/2/25/large.png", + "sources": [ + "https://derpibooru.org/25" + ], + "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood" + }, + { + "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm", + "sources": [ + "http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309" + ], + "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm" + }, + { + "url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg", + "sources": [ + "http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the" + ], + "tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres" + }, + { + "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg", + "sources": [ + "https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926" + ], + "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper" + }, + { + "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg", + "sources": [ + "https://derpibooru.org/2067468" + ], + "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector" + } +] diff --git a/priv/repo/seeds/dev/pages.json b/priv/repo/seeds/dev/pages.json new file mode 100644 index 000000000..cdb5232e9 --- /dev/null +++ b/priv/repo/seeds/dev/pages.json @@ -0,0 +1,70 @@ +[ + { + "title": "About Philomena", + "slug": "about" + }, + { + "title": "Advertising on this Site", + "slug": "advertising" + }, + { + "title": "API Documentation", + "slug": "api" + }, + { + "title": "Approval Queue", + "slug": "approval" + }, + { + "title": "Contact Us", + "slug": "contact" + }, + { + "title": "Donations", + "slug": "donations" + }, + { + "title": "Frequently Asked Questions", + "slug": "faq" + }, + { + "title": "Markdown Syntax Guide", + "slug": "markdown" + }, + { + "title": "Privacy Policy", + "slug": "privacy" + }, + { + "title": "Site Rules", + "slug": "rules" + }, + { + "title": "Search Syntax", + "slug": "search_syntax" + }, + { + "title": "Keyboard Shortcuts", + "slug": "shortcuts" + }, + { + "title": "Spoiler Guidelines", + "slug": "spoilers" + }, + { + "title": "Getting Started", + "slug": "start" + }, + { + "title": "Tag Help", + "slug": "tags" + }, + { + "title": "Takedown Policy", + "slug": "takedowns" + }, + { + "title": "Uploading Help", + "slug": "uploading" + } +] diff --git a/priv/repo/seeds/dev/users.json b/priv/repo/seeds/dev/users.json new file mode 100644 index 000000000..338183387 --- /dev/null +++ b/priv/repo/seeds/dev/users.json @@ -0,0 +1,32 @@ +[ + { + "name": "Hot Pocket Consumer", + "email": "moderator@example.com", + "password": "philomena123", + "role": "moderator" + }, + { + "name": "Hoping For a Promotion", + "email": "assistant@example.com", + "password": "philomena123", + "role": "assistant" + }, + { + "name": "Pleb", + "email": "user@example.com", + "password": "philomena123", + "role": "user" + }, + { + "name": "Artist", + "email": "artist@example.com", + "password": "philomena123", + "role": "user" + }, + { + "name": "Lurker", + "email": "lurker@example.com", + "password": "philomena123", + "role": "user" + } +] diff --git a/priv/repo/seeds/pages/about.md b/priv/repo/seeds/pages/about.md new file mode 100644 index 000000000..66a3ac742 --- /dev/null +++ b/priv/repo/seeds/pages/about.md @@ -0,0 +1,7 @@ +# About Philomena + +Philomena is state of the art software for powering image boorus (image sharing/commenting/voting sites). At the forefront of its goals is to be the easiest-to-use and most intuitive imageboard software around. It is also designed to be easy to scale on a technical level, requiring less system resources than other booru software, saving you the hassle of getting big servers with serious hardware. + +It was initially written for [Derpibooru](https://derpibooru.org), the largest image sharing site aimed at the fans of the My Little Pony cartoon, from scratch with a view to making a really good web application for sharing images. It has since outgrown Derpibooru, and now powers many other image sharing sites, such as [Furbooru](https://furbooru.org). + +We hope you enjoy using this software! Please make sure to share any suggestions and report any issues you may find with it [in the issues section of our GitHub repository](https://github.com/philomena-dev/philomena/issues). diff --git a/priv/repo/seeds/pages/advertising.md b/priv/repo/seeds/pages/advertising.md new file mode 100644 index 000000000..67818b667 --- /dev/null +++ b/priv/repo/seeds/pages/advertising.md @@ -0,0 +1,11 @@ +# Advertising + +This is the default advertisement policy page of Philomena! Edit this with your site's advertising prices, requirements and contact details. A good advertisement page mentions what kind of advertisements are accepted (e.g. community ads only), NSFW advertisement policy, free advertisement policy (e.g. if you'd like to offer free short-term ads to independent artists), paid advertisement policy (pricing per month, terms, required advance notice, etc), technical requirements of the advertisement banner image (see below), and anything else you might want a potential advertiser to know. Don't forget to mention how a potential advertiser might contact you about advertisement inquiries. + +Philomena is designed to accept advertisements with the following parameters: + +**File size:** no larger than 500 kilobytes (500000 bytes) +**Format:** PNG, GIF or JPG/JPEG +**Max resolution:** 729x91 pixels +**Min resolution:** 699x79 pixels +(we suggest 728x90 resolution for optimal experience) diff --git a/priv/repo/seeds/pages/api.md b/priv/repo/seeds/pages/api.md new file mode 100644 index 000000000..d896f7f44 --- /dev/null +++ b/priv/repo/seeds/pages/api.md @@ -0,0 +1,1156 @@ +We provide a JSON API for major site functionality, which can be freely used by anyone wanting to produce tools for the site or other web applications that use the data provided within this website. + +## Licensing + +Anyone may use the API. Users making abusively high numbers of requests or excessively expensive requests will be asked to stop, and banned if they do not. Your application must properly cache, and respect server-side cache expiry times. Your client must gracefully back off if requests fail, preferably exponentially or fatally. + +If images are used, the artist must always be credited (if provided) and the original source URL must be displayed alongside the image, either in textual form or as a link. A link to the image page on this website is optional but recommended. The `https:` protocol must be specified on all URLs. + +## Parameters + +This is a list of general parameters that are useful when working with the API. Not all parameters may be used in every request. + +Name | Description +--- | --- +`filter_id` Assuming | the user can access the filter ID given by the parameter, overrides the current filter for this request. This is primarily `useful` | for unauthenticated API access. +`key` | An optional authentication token. If omitted, no user will be authenticated. You can find your authentication token in your [account settings](/registration/edit). +`page` | Controls the current page of the response, if the response is paginated. Empty values default to the first page. +`per_page` | Controls the number of results per page, up to a limit of 50, if the response is paginated. The default is 25. +`q` | The current search query, if the request is a search request. +`sd` | The current sort direction, if the request is a search request. +`sf` | The current sort field, if the request is a search request. + +## Routes + +The interested reader may find the implementations of these endpoints [here](https://github.com/derpibooru/philomena/tree/master/lib/philomena_web/controllers/api). For the purposes of this document, a brief overview is given. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPathAllowed Query ParametersDescriptionResponse FormatExample
GET/api/v1/json/comments/:comment_idFetches a comment response for the comment ID referenced by the comment_id URL parameter.{"comment":comment-response}/api/v1/json/comments/1
GET/api/v1/json/images/:image_idkey, filter_idFetches an image response for the image ID referenced by the image_id URL parameter.{"image":image-response}/api/v1/json/images/1
POST/api/v1/json/imageskey, urlSubmits a new image. Both key and url are required. Errors will result in an {"errors":image-errors-response}.{"image":image-response}Posting images
GET/api/v1/json/images/featuredFetches an image response for the for the current featured image.{"image":image-response}/api/v1/json/images/featured
GET/api/v1/json/tags/:tag_idFetches a tag response for the tag slug given by the tag_id URL parameter. The tag's ID is not used.{"tag":tag-response}/api/v1/json/tags/artist-colon-atryl
GET/api/v1/json/posts/:post_idFetches a post response for the post ID given by the post_id URL parameter.{"post":post-response}/api/v1/json/posts/2730144
GET/api/v1/json/profiles/:user_idFetches a profile response for the user ID given by the user_id URL parameter.{"user":user-response}/api/v1/json/profiles/1
GET/api/v1/json/filters/:filter_idkeyFetches a filter response for the filter ID given by the filter_id URL parameter.{"filter":filter-response}/api/v1/json/filters/1
GET/api/v1/json/filters/systempageFetches a list of filter responses that are flagged as being system filters (and thus usable by anyone).{"filters":[filter-response]}/api/v1/json/filters/system
GET/api/v1/json/filters/userkey, pageFetches a list of filter responses that belong to the user given by key. If no key is given or it is invalid, will return a 403 Forbidden error.{"filters":[filter-response]}/api/v1/json/filters/user
GET/api/v1/json/oembedurlFetches an oEmbed response for the given app link or CDN URL.oembed-response/api/v1/json/oembed?url=https://cdn.philomena.local/img/2012/1/2/3/full.png
GET/api/v1/json/search/commentskey, pageExecutes the search given by the q query parameter, and returns comment responses sorted by descending creation time.{"comments":[comment-response]}/api/v1/json/search/comments?q=image_id:1000000
GET/api/v1/json/search/gallerieskey, pageExecutes the search given by the q query parameter, and returns gallery responses sorted by descending creation time.{"galleries":[gallery-response]}/api/v1/json/search/galleries?q=title:mean*
GET/api/v1/json/search/postskey, pageExecutes the search given by the q query parameter, and returns post responses sorted by descending creation time.{"posts":[post-response]}/api/v1/json/search/posts?q=subject:time wasting thread
GET/api/v1/json/search/imageskey, filter_id, page, per_page, q, sd, sfExecutes the search given by the q query parameter, and returns image responses.{"images":[image-response]}/api/v1/json/search/images?q=safe
GET/api/v1/json/search/tagspageExecutes the search given by the q query parameter, and returns tag responses sorted by descending image count.{"tags":[tag-response]}/api/v1/json/search/tags?q=analyzed_name:wing
POST/api/v1/json/search/reversekey, url, distanceReturns image responses based on the results of reverse-searching the image given by the url query parameter.{"images":[image-response]}/api/v1/json/search/reverse?url=https://cdn.philomena.local/img/2019/12/24/2228439/full.jpg
GET/api/v1/json/forumsFetches a list of forum responses.{"forums":forum-response}/api/v1/json/forums
GET/api/v1/json/forums/:short_nameFetches a forum response for the abbreviated name given by the short_name URL parameter.{"forum":forum-response}/api/v1/json/forums/dis
GET/api/v1/json/forums/:short_name/topicspageFetches a list of topic responses for the abbreviated forum name given by the short_name URL parameter.{"topics":topic-response}/api/v1/json/forums/dis/topics
GET/api/v1/json/forums/:short_name/topics/:topic_slugFetches a topic response for the abbreviated forum name given by the short_name and topic given by topic_slug URL parameters.{"topic":topic-response}/api/v1/json/forums/dis/topics/ask-the-mods-anything
GET/api/v1/json/forums/:short_name/topics/:topic_slug/postspageFetches a list of post responses for the abbreviated forum name given by the short_name and topic given by topic_slug URL parameters.{"posts":post-response}/api/v1/json/forums/dis/topics/ask-the-mods-anything/posts
GET/api/v1/json/forums/:short_name/topics/:topic_slug/posts/:post_idFetches a post response for the abbreviated forum name given by the short_name, topic given by topic_slug and post given by post_id URL parameters.{"post":post-response}/api/v1/json/forums/dis/topics/ask-the-mods-anything/posts/2761095
+ +## Posting Images + +Posting images should be done via request body parameters. An example with all parameters included is shown below. + +You are *strongly recommended* to test code using this endpoint using a local copy of the website's source code. Abuse of the endpoint **will result in a ban**. + +You *must* provide the direct link to the image in the `url` parameter. + +You *must* set the `content-type` header to `application/json` for the site to process your request. + +``` +POST /api/v1/json/images?key=API_KEY +``` +``` +{ +"image": { + "description": "[bq]Hey there this is a test post![/bq]\nDescriptions are *weird*.\nHave a >>0 re-upload :)\n", + "tag_input": "artist needed, safe, derpy hooves, pegasus, pony, adventure in the comments, bag, building, chair, cigar, derpibooru legacy, eyes, featured image, female, grin, gritted teeth, hilarious in hindsight, image macro, it begins, j. jonah jameson, letter, mail, male, mare, meme, muffin, necktie, paper, parody, phone, ponified, sitting, smiling, smoking, song in the comments, spider-man, stallion, swinging", + "source_url": "https://philomena.local/images/0" +}, +"url": "https://cdn.philomena.local/img/view/2012/1/2/0.jpg" +} +``` + +

Image Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
animatedBooleanWhether the image is animated.
aspect_ratioFloatThe image's width divided by its height.
comment_countIntegerThe number of comments made on the image.
created_atRFC3339 datetimeThe creation time, in UTC, of the image.
deletion_reasonStringThe hide reason for the image, or null if none provided. This will only have a value on images which are deleted for a rule violation.
descriptionStringThe image's description.
downvotesIntegerThe number of downvotes the image has.
duplicate_ofIntegerThe ID of the target image, or null if none provided. This will only have a value on images which are merged into another image.
durationFloatThe number of seconds the image lasts, if animated, otherwise .04.
favesIntegerThe number of faves the image has.
first_seen_atRFC3339 datetimeThe time, in UTC, the image was first seen (before any duplicate merging).
formatStringThe file extension of the image. One of "gif", "jpg", "jpeg", "png", "svg", "webm".
heightIntegerThe image's height, in pixels.
hidden_from_usersBooleanWhether the image is hidden. An image is hidden if it is merged or deleted for a rule violation.
idIntegerThe image's ID.
intensitiesObjectOptional object of internal image intensity data for deduplication purposes. May be null if intensities have not yet been generated.
mime_typeStringThe MIME type of this image. One of "image/gif", "image/jpeg", "image/png", "image/svg+xml", "video/webm".
nameStringThe filename that the image was uploaded with.
orig_sha512_hashStringThe SHA512 hash of the image as it was originally uploaded.
processedBooleanWhether the image has finished optimization.
representationsObjectA mapping of representation names to their respective URLs. Contains the keys "full", "large", "medium", "small", "tall", "thumb", "thumb_small", "thumb_tiny".
scoreIntegerThe image's number of upvotes minus the image's number of downvotes.
sha512_hashStringThe SHA512 hash of this image after it has been processed.
sizeIntegerThe number of bytes the image's file contains.
source_urlString(Deprecated - Use source_urls field instead) Provides the first source URL of the image as stored in the database, intended for legacy applications only.
source_urlsString[]A list of all source URLs provided for the image, may be empty.
spoileredBooleanWhether the image is hit by the current filter.
tag_countIntegerThe number of tags present on the image.
tag_idsArrayA list of tag IDs the image contains.
tagsArrayA list of tag names the image contains.
thumbnails_generatedBooleanWhether the image has finished thumbnail generation. Do not attempt to load images from view_url or representations if this is false.
updated_atRFC3339 datetimeThe time, in UTC, the image was last updated.
uploaderStringThe image's uploader.
uploader_idIntegerThe ID of the image's uploader. null if uploaded anonymously.
upvotesIntegerThe image's number of upvotes.
view_urlStringThe image's view URL, including tags.
widthIntegerThe image's width, in pixels.
wilson_scoreFloatThe lower bound of the Wilson score interval for the image, based on its upvotes and downvotes, given a z-score corresponding to a confidence of 99.5%.
+

Comment Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
authorStringThe comment's author.
avatarStringThe URL of the author's avatar. May be a link to the CDN path, or a data: URI.
bodyStringThe comment text.
created_atRFC3339 datetimeThe creation time, in UTC, of the comment.
edit_reasonStringThe edit reason for this comment, or null if none provided.
edited_atRFC3339 datetimeThe time, in UTC, this comment was last edited at, or null if it was not edited.
idIntegerThe comment's ID.
image_idIntegerThe ID of the image the comment belongs to.
updated_atRFC3339 datetimeThe time, in UTC, the comment was last updated at.
user_idIntegerThe ID of the user the comment belongs to, if any.
+

Forum Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
nameStringThe forum's name.
short_nameStringThe forum's short name (used to identify it).
descriptionStringThe forum's description.
topic_countIntegerThe amount of topics in the forum.
post_countIntegerThe amount of posts in the forum.
+

Topic Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
slugStringThe topic's slug (used to identify it).
titleStringThe topic's title.
post_countIntegerThe amount of posts in the topic.
view_countIntegerThe amount of views the topic has received.
stickyBooleanWhether the topic is sticky.
last_replied_to_atRFC3339 datetimeThe time, in UTC, when the last reply was made.
lockedBooleanWhether the topic is locked.
user_idIntegerThe ID of the user who made the topic. null if posted anonymously.
authorStringThe name of the user who made the topic.
+

Post Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
authorStringThe post's author.
avatarStringThe URL of the author's avatar. May be a link to the CDN path, or a data: URI.
bodyStringThe post text.
created_atRFC3339 datetimeThe creation time, in UTC, of the post.
edit_reasonStringThe edit reason for this post.
edited_atRFC3339 datetimeThe time, in UTC, this post was last edited at, or null if it was not edited.
idIntegerThe post's ID (used to identify it).
updated_atRFC3339 datetimeThe time, in UTC, the post was last updated at.
user_idIntegerThe ID of the user the post belongs to, if any.
+

Tag Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
aliased_tagStringThe slug of the tag this tag is aliased to, if any.
aliasesArrayThe slugs of the tags aliased to this tag.
categoryStringThe category class of this tag. One of "character", "content-fanmade", "content-official", "error", "oc", "origin", "rating", "species", "spoiler".
descriptionStringThe long description for the tag.
dnp_entriesArrayAn array of objects containing DNP entries claimed on the tag.
idIntegerThe tag's ID.
imagesIntegerThe image count of the tag.
implied_by_tagsArrayThe slugs of the tags this tag is implied by.
implied_tagsArrayThe slugs of the tags this tag implies.
nameStringThe name of the tag.
name_in_namespaceStringThe name of the tag in its namespace.
namespaceStringThe namespace of the tag.
short_descriptionStringThe short description for the tag.
slugStringThe slug for the tag.
spoiler_image_uriStringThe spoiler image for the tag.
+

User Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
idIntegerThe ID of the user.
nameStringThe name of the user.
slugStringThe slug of the user.
roleStringThe role of the user.
descriptionStringThe description (bio) of the user.
avatar_urlStringThe URL of the user's thumbnail. null if the avatar is not set.
created_atRFC3339 datetimeThe creation time, in UTC, of the user.
comments_countIntegerThe comment count of the user.
uploads_countIntegerThe upload count of the user.
posts_countIntegerThe forum posts count of the user.
topics_countIntegerThe forum topics count of the user.
linksObjectThe links the user has registered. See links-response.
awardsObjectThe awards/badges of the user. See awards-response.
+

Filter Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
idIntegerThe id of the filter.
nameStringThe name of the filter.
descriptionStringThe description of the filter.
user_idIntegerThe id of the user the filter belongs to. null if it isn't assigned to a user (usually system filters only).
user_countIntegerThe amount of users employing this filter.
systemBooleanIf true, is a system filter. System filters are usable by anyone and don't have a user_id set.
publicBooleanIf true, is a public filter. Public filters are usable by anyone.
spoilered_tag_idsArrayA list of tag IDs (as integers) that this filter will spoil.
spoilered_complexStringThe complex spoiled filter.
hidden_tag_idsArrayA list of tag IDs (as integers) that this filter will hide.
hidden_complexStringThe complex hidden filter.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
user_idIntegerThe ID of the user who owns this link.
created_atRFC3339 datetimeThe creation time, in UTC, of this link.
stateStringThe state of this link.
tag_idIntegerThe ID of an associated tag for this link. null if no tag linked.
+

Awards Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
image_urlStringThe URL of this award.
titleStringThe title of this award.
idIntegerThe ID of the badge this award is derived from.
labelStringThe label of this award.
awarded_onRFC3339 datetimeThe time, in UTC, when this award was given.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
descriptionStringThe gallery's description.
idIntegerThe gallery's ID.
spoiler_warningStringThe gallery's spoiler warning.
thumbnail_idIntegerThe ID of the cover image for the gallery.
titleStringThe gallery's title.
userStringThe name of the gallery's creator.
user_idIntegerThe ID of the gallery's creator.
+

Image Errors Responses

+

Each field is optional and is an Array of Strings.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
imageArrayErrors in the submitted image
image_aspect_ratioArrayErrors in the submitted image
image_formatArrayWhen an image is unsupported (ex. WEBP)
image_heightArrayErrors in the submitted image
image_widthArrayErrors in the submitted image
image_sizeArrayUsually if an image that is too large is uploaded.
image_is_animatedArrayErrors in the submitted image
image_mime_typeArrayErrors in the submitted image
image_orig_sha512_hashArrayErrors in the submitted image. If has already been taken is present, means the image already exists in the database.
image_sha512_hashArrayErrors in the submitted image
tag_inputArrayErrors with the tag metadata.
uploaded_imageArrayErrors in the submitted image
+

Oembed Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
author_nameStringThe comma-delimited names of the image authors.
author_urlStringThe source URL of the image.
cache_ageIntegerAlways 7200.
derpibooru_commentsIntegerThe number of comments made on the image.
derpibooru_idIntegerThe image's ID.
derpibooru_scoreIntegerThe image's number of upvotes minus the image's number of downvotes.
derpibooru_tagsArrayThe names of the image's tags.
provider_nameStringAlways "Derpibooru".
provider_urlStringAlways "https://derpibooru.org".
titleStringThe image's ID and associated tags, as would be given on the title of the image page.
typeStringAlways "photo".
versionStringAlways "1.0".
diff --git a/priv/repo/seeds/pages/approval.md b/priv/repo/seeds/pages/approval.md new file mode 100644 index 000000000..5247f1c29 --- /dev/null +++ b/priv/repo/seeds/pages/approval.md @@ -0,0 +1,43 @@ +# Approval Queue and User Verification + +If you are here, you must be wondering why your upload got held up in an approval queue instead of being posted directly. + +Shortly speaking - **this is to protect our users from illegal imagery.** + +# Images + +#### **What happens to the image while it's not approved?** + +The image will not appear in any search results and will be impossible to be linked to via our on-site image embedding syntax. It is still viewable via a direct URL. + +#### **Why did my upload require approval?** + +We require that all uploads from users without an account, as well as unverified registered users go through the approval queue where a staff member can decide whether it's illegal content (Rule #5) or not. + +#### **Will my image lose views because of this?** + +No! Once approved, your image appears in search results and on the home page as if it was uploaded at the time of approval. + +#### **Is this some kind of censorship?** + +No. We are strictly checking for whether or not the imagery is illegal (Rule #5) or not. This is not "Quality Control" in any way whatsoever. + +#### **How do I get verified?** + +First of all - register an account. Once you upload a certain (small) amount of images that get approved by the staff members, your account will be evaluated and depending on staff evaluation, you will be granted verification. Once verified, your uploads will bypass the approval queue and be automatically approved. Please note that staff evaluation of accounts may take up to a week, but will usually take a day or two. + +#### **Where can I check if I'm verified?** + +As of the moment of writing this article - you cannot. If you're unsure if you're verified or not, simply ask a staff member via PMs. + +# Comments and forum posts + +Comments and forum posts are also subject to additional moderation measures now. + +#### **Why can't I embed external images into my comments or posts?** + +Users without an account cannot use the image embed syntax (`![](image url)`) at all. + +#### **Why does my comment/post require additional approval?** + +If your account is relatively new, any of your posts/comments that contain the image embed syntax (`![](image url)`) must go through staff approval before they're publicly visible and searchable. diff --git a/priv/repo/seeds/pages/contact.md b/priv/repo/seeds/pages/contact.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/contact.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/donations.md b/priv/repo/seeds/pages/donations.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/donations.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/faq.md b/priv/repo/seeds/pages/faq.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/faq.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/markdown.md b/priv/repo/seeds/pages/markdown.md new file mode 100644 index 000000000..2606c5200 --- /dev/null +++ b/priv/repo/seeds/pages/markdown.md @@ -0,0 +1,395 @@ +This page is here to help you get a better grasp on the syntax of Markdown, the text processing engine this site uses. + +## Inline formatting +Inline formatting is the most commonly seen type of text formatting in Markdown. It can be applied almost anywhere else and doesn't depend on specific context (most of the time). + +Operator | Example | Result +--- | --- | --- +Bold | `This is **huge**` | This is **huge** +Italic | `*very* clever, Connor... _very..._` | *very* clever, Connor... _very..._ +Underline | `And I consider this __important__` | And I consider this __important__ +Strikethrough | `I am ~~wrong~~ right` | I am ~~wrong~~ right +Superscript | `normal text ^superscripted text^` | normal text ^superscripted text^ +Subscript | `normal text %subscripted text%` | normal text %subscripted text% +Spoiler | `Psst! ||Darth Vader is Luke's father||` | Psst! ||Darth Vader is Luke's father|| +Code | ```Use `**bold**` to make text bold!``` | Use `**bold**` to make text bold! + +#### Multi-line inlines + +Most inline formatting can extend beyond just a single line and travel to other lines. However, it does have certain quirks, especially if you're unused to the Markdown syntax. + +``` +**I am a very +bold text** +``` +
+
+ Result +
+
+
+ I am a very
bold text
+
+
+
+ +However, if you try to insert a newline in the middle of it, it won't work. + +``` +**I am not a very + +bold text** +``` + +
+
+ Result +
+
+
**I am not a very
+
bold text**
+
+
+ +If you really need an empty line in the middle of your inline-formatted text, you must *escape* the line ending. In order to do so, Markdown provides us with the `\` (backslash) character. Backslash is a very special character and is used for *escaping* other special characters. *Escaping* forces the character immediately after the backslash to be ignored by the parser. + +As such, we can write our previous example like so to preserve the empty line: +``` +**I am a very +\ +bold text** +``` +
+
+ Result +
+
+
I am a very
+
+ bold text
+
+
+ +#### Combining inlines +Most inline operators may be combined with each other (with the exception of the ````code```` syntax). + +``` +_I am an italic text **with some bold in it**._ +``` + +
+
+ Result +
+
+
I am an italic text with some bold in it.
+
+
+ + +## Block formatting +Block formatting is the kind of formatting that cannot be written within a single line and typically requires to be written on its own line. Many block formatting styles extend past just one line. + +#### Blockquotes +Philomena's flavor of Markdown makes some changes to the blockquote syntax compared to regular CommonMark. The basic syntax is a > followed by a space. + +``` +> quote text +``` + +> quote text + +--- + +Please note, that if > is not followed by a space, it will not become a blockquote! + +``` +>not a quote +``` + +>not a quote + +--- + +Same goes for >>, even if followed by a space. + +``` +>> not a quote +``` + +>> not a quote + +--- + +You may continue a quote by adding > followed by a space on a new line, even if the line is otherwise empty. + +``` +> quote text +> +> continuation of quote +``` + +> quote text +> +> continuation of quote + +--- + +To nest a quote, simply repeat > followed by a space as many times as you wish to have nested blockquotes. + +``` +> quote text +> > nested quote +> > > even deeper nested quote +``` + +> quote text +> > nested quote +> > > even deeper nested quote + +#### Headers +Markdown supports adding headers to your text. The syntax is # repeated up to 6 times. + +``` +# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6 +``` + +# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6 + +#### Code block +Another way to write code is by writing a code block. Code blocks, unlike inline code syntax, are styled similar to blockquotes and are more appropriate for sharing larger snippets of code. In fact, this very page has been using this very structure to show examples of code. + +~~~ +``` +
+

Hello World!

+
+``` +~~~ + +``` +
+

Hello World!

+
+``` + +Code blocks may also use tildes (\~) instead of backticks (\`). + +``` +~~~ +code block +~~~ +``` + +~~~ +code block +~~~ + +## Links +Links have the basic syntax of + +``` +[Link Text](https://example.com) +``` + +[Link Text](https://example.com) + +Most links pasted as plaintext will be automatically converted into a proper clickable link, as long as they don't begin with dangerous protocols. +As such... + +``` +https://example.com +``` + +https://example.com + +On-site links may be written as either a relative or absolute path. If the on-site link is written as the absolute path, it will be automatically converted into a relative link for the convenience of other users. + +``` +[Link to the first image](https://philomena.local/images/0) +[Link to the first image](/images/0) +``` + +[Link to the first image](https://philomena.local/images/0) +[Link to the first image](/images/0) + +## On-site images +If you wish to link an on-site image, you should use the >>:id syntax. It respects filters currently in-use by the reader and spoilers content they do not wish to see. +**You should always use this for on-site uploads!** (as this will let other users filter the image if they wish to, and it is against the rules to not show content with care) +Here's a brief explanation of its usage. + +Operator | Description of result +--- | --- +\>\>5 | Simple link to image +\>\>5s | Small (150x150) thumbnail of the image +\>\>5t | Regular (320x240) thumbnail of the image +\>\>5p | Preview (800x600) size of the image + +>>5 +>>5s +>>5t +>>5p + +## External images +Some images you may wish to link may not exist on the site. To link them Markdown provides us with a special syntax. All images embedded this way are proxied by our image proxy (Go-Camo). + +``` +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +``` + +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) + +You may control the size of your externally-linked image by specifying the alt text. Certain keywords are recognized as size modifiers. The modifiers are case-sensitive! + +Modifier | Resulting size +--- | --- +tiny | 64x64 +small | 128x128 +medium | 256x256 +large | 512x512 +(anything else) | (actual size of the image) + +``` +![tiny](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![small](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![medium](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![large](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +``` + +![tiny](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![small](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![medium](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![large](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) + +#### Image links +To make an image link, simply combine the external image syntax with the link syntax. + +``` +[![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg)](https://github.com/philomena-dev/philomena) +``` + +[![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg)](https://github.com/philomena-dev/philomena) + +## Lists +#### Unordered list +Unordered lists can be written fairly intuitively, by putting one of the special characters in front of each line that should be a part of the list. + +``` +Shopping list: +* Milk +* Eggs +* Soda +``` + +Shopping list: +* Milk +* Eggs +* Soda + +You may use any of the following characters at the beginning of the line to make an unordered list: + +``` +* ++ +- +``` + +Lists may be nested and have sublists within them. Simply prefix your sublist items with three spaces while within another list. + +``` +* Item one +* Item two + * Sublist item one + * Sublist item two +``` + +* Item one +* Item two + * Sublist item one + * Sublist item two + +#### Ordered list +To write an ordered list, simply put a number at the beginning of the line followed by a dot or closing bracket. It doesn't actually matter which order your numbers are written in, the list will always maintain its incremental order. Note the 4 in the example, it isn't a typo. + +``` +1. Item one +2. Item two +4. Item three +``` + +1. Item one +2. Item two +4. Item three + +**Ordered lists cannot be sublists to other ordered lists.** They can, however, be sublists to unordered lists. Unordered lists, in turn, may be sublists in ordered lists. + +``` +1) Item one +2) Item two + * Sublist item one + * Sublist item two +``` + +1) Item one +2) Item two + * Sublist item one + * Sublist item two + +## Tables +Philomena's Markdown implementation supports GitHub-style tables. This isn't a part of the core Markdown specification, but we support them. The colons are used to specify the alignment of columns. + +``` +| Left | Center | Right | +| ------------ |:--------------:| -------------:| +| left-aligned | center-aligned | right-aligned | +| *formatting* | **works** | __here__ | +``` + +| Left | Center | Right | +| ------------ |:--------------:| -------------:| +| left-aligned | center-aligned | right-aligned | +| *formatting* | **works** | __here__ | + +In tables, the pipes (|) at the edges of the table are optional. To separate table head from body, you need to put in at least three - symbols. As such, example above could have also been written like so: + +``` +Left | Center | Right +--- | :---: | ---: +left-aligned | center-aligned | right-aligned +*formatting* | **works** | __here__ +``` + +Left | Center | Right +--- | :---: | ---: +left-aligned | center-aligned | right-aligned +*formatting* | **works** | __here__ + +# Escaping the syntax. + +Sometimes you may wish certain characters to not be interpreted as Markdown syntax. This is where the backslash comes in! Prefixing any markup with a backslash will cause the markup immediately following the backslash to not be parsed, for example: + +``` +\*\*grr grr, I should not be bold!\*\* +``` + +\*\*grr grr, I should not be bold\*\* + +Code blocks and code inlines will also escape the syntax to a limited extent (except for backticks themselves). + +``` +`**not bold!**` +``` + +`**not bold!**` diff --git a/priv/repo/seeds/pages/privacy.md b/priv/repo/seeds/pages/privacy.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/privacy.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/rules.md b/priv/repo/seeds/pages/rules.md new file mode 100644 index 000000000..e69de29bb diff --git a/priv/repo/seeds/pages/search_syntax.md b/priv/repo/seeds/pages/search_syntax.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/search_syntax.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/shortcuts.md b/priv/repo/seeds/pages/shortcuts.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/shortcuts.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/spoilers.md b/priv/repo/seeds/pages/spoilers.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/spoilers.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/start.md b/priv/repo/seeds/pages/start.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/start.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/tags.md b/priv/repo/seeds/pages/tags.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/tags.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/takedowns.md b/priv/repo/seeds/pages/takedowns.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/takedowns.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds/pages/uploading.md b/priv/repo/seeds/pages/uploading.md new file mode 100644 index 000000000..4f2e970e9 --- /dev/null +++ b/priv/repo/seeds/pages/uploading.md @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/priv/repo/seeds.json b/priv/repo/seeds/seeds.json similarity index 98% rename from priv/repo/seeds.json rename to priv/repo/seeds/seeds.json index 546d47069..cbcadd676 100644 --- a/priv/repo/seeds.json +++ b/priv/repo/seeds/seeds.json @@ -22,7 +22,7 @@ }, { "name": "Shows and Movies", - "short_name": "pony", + "short_name": "shows", "description": "Discuss TV shows and movies, as well as their characters and theories.", "access_level": "normal" }, diff --git a/priv/repo/seeds/seeds_development.json b/priv/repo/seeds/seeds_development.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/priv/repo/seeds/seeds_development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 47bcd1e7e..75f6333be 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -10,123 +10,221 @@ # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. -alias Philomena.{Repo, Forums.Forum, Users, Users.User} -alias Philomena.Comments -alias Philomena.Images -alias Philomena.Topics -alias Philomena.Posts -alias Philomena.Tags - -{:ok, ip} = EctoNetwork.INET.cast({203, 0, 113, 0}) -{:ok, _} = Application.ensure_all_started(:plug) - -resources = - "priv/repo/seeds_development.json" - |> File.read!() - |> Jason.decode!() - -IO.puts "---- Generating users" -for user_def <- resources["users"] do - {:ok, user} = Users.register_user(user_def) - - user - |> Repo.preload([:roles]) - |> User.confirm_changeset() - |> User.update_changeset(%{role: user_def["role"]}, []) - |> Repo.update!() -end +defmodule Philomena.DevSeeds do + alias Philomena.{Repo, Forums.Forum, Users, Users.User} + alias Philomena.Comments + alias Philomena.Images + alias Philomena.Images.Image + alias Philomena.Topics + alias Philomena.Posts + alias Philomena.Tags + import Ecto.Query + + def seed() do + {:ok, _} = Application.ensure_all_started(:plug) + + # resources = + # "priv/repo/seeds/seeds_development.json" + # |> File.read!() + # |> Jason.decode!() + + communications = + "priv/repo/seeds/dev/communications.json" + |> File.read!() + |> Jason.decode!() + + images = + "priv/repo/seeds/dev/images.json" + |> File.read!() + |> Jason.decode!() + + # pages = + # "priv/repo/seeds/dev/pages.json" + # |> File.read!() + # |> Jason.decode!() + + users = + "priv/repo/seeds/dev/users.json" + |> File.read!() + |> Jason.decode!() + + Logger.configure(level: :warning) + + IO.puts "---- Generating users" + for user_def <- users do + {:ok, user} = Users.register_user(user_def) + + user + |> Repo.preload([:roles]) + |> User.confirm_changeset() + |> User.update_changeset(%{role: user_def["role"]}, []) + |> Repo.update!() + end + + users = Repo.all(User) + pleb = Repo.get_by!(User, name: "Pleb") + pleb_attrs = request_attrs(pleb) + + IO.puts "---- Generating images" + for image_def <- images do + file = Briefly.create!() + now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) + + IO.puts "Fetching #{image_def["url"]} ..." + {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) + + File.write!(file, body) + + upload = %Plug.Upload{ + path: file, + content_type: "application/octet-stream", + filename: "fixtures-#{now}" + } + + IO.puts "Inserting ..." + + Images.create_image( + pleb_attrs, + Map.merge(image_def, %{"image" => upload}) + ) + |> case do + {:ok, %{image: image}} -> + Images.approve_image(image) + Images.reindex_image(image) + Tags.reindex_tags(image.added_tags) + + IO.puts "Created image ##{image.id}" + + {:error, :image, changeset, _so_far} -> + IO.inspect changeset.errors + end + end + + IO.puts "---- Generating comments for image #1" + for comment_body <- communications["demos"] do + image = Images.get_image!(1) + + Comments.create_comment( + image, + pleb_attrs, + %{"body" => comment_body} + ) + |> case do + {:ok, %{comment: comment}} -> + Comments.approve_comment(comment, pleb) + Comments.reindex_comment(comment) + Images.reindex_image(image) + + {:error, :comment, changeset, _so_far} -> + IO.inspect changeset.errors + end + end + + all_imgs = Image |> where([i], i.id > 1) |> Repo.all() + + IO.puts "---- Generating random comments for images other than 1" + for _ <- 1..1000 do + image = Enum.random(all_imgs) + user = random_user(users) + + Comments.create_comment( + image, + request_attrs(user), + %{"body" => random_body(communications)} + ) + |> case do + {:ok, %{comment: comment}} -> + Comments.approve_comment(comment, user) + Comments.reindex_comment(comment) + Images.reindex_image(image) + + {:error, :comment, changeset, _so_far} -> + IO.inspect changeset.errors + end + end + + IO.puts "---- Generating forum posts" + for _ <- 1..500 do + random_topic_no_replies(communications, users) + end + + for _ <- 1..20 do + random_topic(communications, users) + end -pleb = Repo.get_by!(User, name: "Pleb") -request_attributes = [ - fingerprint: "c1836832948", - ip: ip, - user_id: pleb.id, - user: pleb -] - -IO.puts "---- Generating images" -for image_def <- resources["remote_images"] do - file = Briefly.create!() - now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) - - IO.puts "Fetching #{image_def["url"]} ..." - {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) - - File.write!(file, body) - - upload = %Plug.Upload{ - path: file, - content_type: "application/octet-stream", - filename: "fixtures-#{now}" - } - - IO.puts "Inserting ..." - - Images.create_image( - request_attributes, - Map.merge(image_def, %{"image" => upload}) - ) - |> case do - {:ok, %{image: image}} -> - Images.approve_image(image) - Images.reindex_image(image) - Tags.reindex_tags(image.added_tags) - - IO.puts "Created image ##{image.id}" - - {:error, :image, changeset, _so_far} -> - IO.inspect changeset.errors + IO.puts "---- Done." + + Logger.configure(level: :debug) end -end -IO.puts "---- Generating comments for image #1" -for comment_body <- resources["comments"] do - image = Images.get_image!(1) - - Comments.create_comment( - image, - request_attributes, - %{"body" => comment_body} - ) - |> case do - {:ok, %{comment: comment}} -> - Comments.approve_comment(comment, pleb) - Comments.reindex_comment(comment) - Images.reindex_image(image) - - {:error, :comment, changeset, _so_far} -> - IO.inspect changeset.errors + defp default_ip() do + {:ok, ip} = EctoNetwork.INET.cast({203, 0, 113, 0}) + ip end -end -IO.puts "---- Generating forum posts" -for %{"forum" => forum_name, "topics" => topics} <- resources["forum_posts"] do - forum = Repo.get_by!(Forum, short_name: forum_name) + defp available_forums(), do: ["dis", "art", "rp", "meta", "shows"] + + defp random_forum(), do: Enum.random(available_forums()) + + defp random_user(users), do: Enum.random(users) + + defp request_attrs(%{id: id} = user) do + [ + fingerprint: "d015c342859dde3", + ip: default_ip(), + user_agent: "Hopefully not IE", + referrer: "localhost", + user_id: id, + user: user + ] + end + + defp random_body(%{"random" => random}) do + count = :rand.uniform(3) + + (0..count) + |> Enum.map(fn _ -> Enum.random(random) end) + |> Enum.join("\n\n") + end + + defp random_title(%{"titles" => titles}) do + Enum.random(titles["first"]) <> " " + <> Enum.random(titles["second"]) <> " " + <> Enum.random(titles["third"]) + end + + defp random_topic(comm, users) do + forum = Repo.get_by!(Forum, short_name: random_forum()) + op = random_user(users) - for %{"title" => topic_name, "posts" => [first_post | posts]} <- topics do Topics.create_topic( forum, - request_attributes, + request_attrs(op), %{ - "title" => topic_name, + "title" => random_title(comm), "posts" => %{ "0" => %{ - "body" => first_post, + "body" => random_body(comm), } } } ) |> case do {:ok, %{topic: topic}} -> - for post <- posts do + IO.puts(" -> created topic ##{topic.id}") + count = :rand.uniform(250) + 5 + + for _ <- 1..count do + user = random_user(users) + Posts.create_post( topic, - request_attributes, - %{"body" => post} + request_attrs(user), + %{"body" => random_body(comm)} ) |> case do {:ok, %{post: post}} -> - Posts.approve_post(post, pleb) + Posts.approve_post(post, op) Posts.reindex_post(post) {:error, :post, changeset, _so_far} -> @@ -134,10 +232,37 @@ for %{"forum" => forum_name, "topics" => topics} <- resources["forum_posts"] do end end + IO.puts(" -> created #{count} replies for topic ##{topic.id}") + + {:error, :topic, changeset, _so_far} -> + IO.inspect changeset.errors + end + end + + defp random_topic_no_replies(comm, users) do + forum = Repo.get_by!(Forum, short_name: random_forum()) + op = random_user(users) + + Topics.create_topic( + forum, + request_attrs(op), + %{ + "title" => random_title(comm), + "posts" => %{ + "0" => %{ + "body" => random_body(comm), + } + } + } + ) + |> case do + {:ok, %{topic: topic}} -> + IO.puts(" -> created topic ##{topic.id}") + {:error, :topic, changeset, _so_far} -> IO.inspect changeset.errors end end end -IO.puts "---- Done." +Philomena.DevSeeds.seed() diff --git a/priv/repo/seeds_development.json b/priv/repo/seeds_development.json deleted file mode 100644 index 4788b0f5d..000000000 --- a/priv/repo/seeds_development.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "users": [ - { - "name": "Hot Pocket Consumer", - "email": "moderator@example.com", - "password": "philomena123", - "role": "moderator" - }, - { - "name": "Hoping For a Promotion", - "email": "assistant@example.com", - "password": "philomena123", - "role": "assistant" - }, - { - "name": "Pleb", - "email": "user@example.com", - "password": "philomena123", - "role": "user" - } - ], - "remote_images": [ - { - "url": "https://derpicdn.net/img/2015/9/26/988000/thumb.gif", - "sources": ["https://derpibooru.org/988000"], - "description": "Fairly large GIF (~23MB), use to test WebM stuff.", - "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof" - }, - { - "url": "https://derpicdn.net/img/2012/1/2/25/large.png", - "sources": ["https://derpibooru.org/25"], - "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood" - }, - { - "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm", - "sources": ["http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"], - "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm" - }, - { - "url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg", - "sources": [ - "http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the" - ], - "tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres" - }, - { - "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg", - "sources": ["https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"], - "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper" - }, - { - "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg", - "sources": ["https://derpibooru.org/2067468"], - "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector" - } - ], - "comments": [ - "bold is **bold**, italic is _italic_, spoiler is ||spoiler||, code is `code`, underline is __underline__, strike is ~~strike~~, sup is ^sup^, sub is %sub%.", - "inline embedded thumbnails (tsp): >>1t >>1s >>1p", - "embedded image inside a spoiler: ||who needs it anyway >>1s||", - "spoilers inside of a table\n\nHello | World\n--- | ---:\n`||cool beans!||` | ||cool beans!||" - ], - "forum_posts": [ - { - "forum": "dis", - "topics": [ - { - "title": "Example Topic", - "posts": ["example post", "yet another example post"] - }, - { - "title": "Second Example Topic", - "posts": ["post", "post 2"] - } - ] - }, - { - "forum": "art", - "topics": [ - { - "title": "Embedded Images", - "posts": [">>1t >>1s >>1p", ">>1", "non-existent: >>1000t >>1000s >>1000p >>1000"] - } - ] - } - ] -} From d1aae412424eb21cf2189a85607abce2e6022417 Mon Sep 17 00:00:00 2001 From: MareStare Date: Thu, 27 Mar 2025 03:19:58 +0000 Subject: [PATCH 28/61] Format seeds_development.exs --- .formatter.exs | 2 +- priv/repo/seeds_development.exs | 49 +++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 8a6391c6a..e0d1f72bd 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ [ import_deps: [:ecto, :phoenix], - inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], + inputs: ["*.{ex,exs}", "priv/*/seeds*.exs", "{config,lib,test}/**/*.{ex,exs}"], subdirectories: ["priv/*/migrations"] ] diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 75f6333be..5b09eb5b9 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -50,7 +50,8 @@ defmodule Philomena.DevSeeds do Logger.configure(level: :warning) - IO.puts "---- Generating users" + IO.puts("---- Generating users") + for user_def <- users do {:ok, user} = Users.register_user(user_def) @@ -65,12 +66,13 @@ defmodule Philomena.DevSeeds do pleb = Repo.get_by!(User, name: "Pleb") pleb_attrs = request_attrs(pleb) - IO.puts "---- Generating images" + IO.puts("---- Generating images") + for image_def <- images do file = Briefly.create!() now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) - IO.puts "Fetching #{image_def["url"]} ..." + IO.puts("Fetching #{image_def["url"]} ...") {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) File.write!(file, body) @@ -81,7 +83,7 @@ defmodule Philomena.DevSeeds do filename: "fixtures-#{now}" } - IO.puts "Inserting ..." + IO.puts("Inserting ...") Images.create_image( pleb_attrs, @@ -93,14 +95,15 @@ defmodule Philomena.DevSeeds do Images.reindex_image(image) Tags.reindex_tags(image.added_tags) - IO.puts "Created image ##{image.id}" + IO.puts("Created image ##{image.id}") {:error, :image, changeset, _so_far} -> - IO.inspect changeset.errors + IO.inspect(changeset.errors) end end - IO.puts "---- Generating comments for image #1" + IO.puts("---- Generating comments for image #1") + for comment_body <- communications["demos"] do image = Images.get_image!(1) @@ -116,13 +119,14 @@ defmodule Philomena.DevSeeds do Images.reindex_image(image) {:error, :comment, changeset, _so_far} -> - IO.inspect changeset.errors + IO.inspect(changeset.errors) end end all_imgs = Image |> where([i], i.id > 1) |> Repo.all() - IO.puts "---- Generating random comments for images other than 1" + IO.puts("---- Generating random comments for images other than 1") + for _ <- 1..1000 do image = Enum.random(all_imgs) user = random_user(users) @@ -139,11 +143,12 @@ defmodule Philomena.DevSeeds do Images.reindex_image(image) {:error, :comment, changeset, _so_far} -> - IO.inspect changeset.errors + IO.inspect(changeset.errors) end end - IO.puts "---- Generating forum posts" + IO.puts("---- Generating forum posts") + for _ <- 1..500 do random_topic_no_replies(communications, users) end @@ -152,7 +157,7 @@ defmodule Philomena.DevSeeds do random_topic(communications, users) end - IO.puts "---- Done." + IO.puts("---- Done.") Logger.configure(level: :debug) end @@ -182,15 +187,17 @@ defmodule Philomena.DevSeeds do defp random_body(%{"random" => random}) do count = :rand.uniform(3) - (0..count) + 0..count |> Enum.map(fn _ -> Enum.random(random) end) |> Enum.join("\n\n") end defp random_title(%{"titles" => titles}) do - Enum.random(titles["first"]) <> " " - <> Enum.random(titles["second"]) <> " " - <> Enum.random(titles["third"]) + Enum.random(titles["first"]) <> + " " <> + Enum.random(titles["second"]) <> + " " <> + Enum.random(titles["third"]) end defp random_topic(comm, users) do @@ -204,7 +211,7 @@ defmodule Philomena.DevSeeds do "title" => random_title(comm), "posts" => %{ "0" => %{ - "body" => random_body(comm), + "body" => random_body(comm) } } } @@ -228,14 +235,14 @@ defmodule Philomena.DevSeeds do Posts.reindex_post(post) {:error, :post, changeset, _so_far} -> - IO.inspect changeset.errors + IO.inspect(changeset.errors) end end IO.puts(" -> created #{count} replies for topic ##{topic.id}") {:error, :topic, changeset, _so_far} -> - IO.inspect changeset.errors + IO.inspect(changeset.errors) end end @@ -250,7 +257,7 @@ defmodule Philomena.DevSeeds do "title" => random_title(comm), "posts" => %{ "0" => %{ - "body" => random_body(comm), + "body" => random_body(comm) } } } @@ -260,7 +267,7 @@ defmodule Philomena.DevSeeds do IO.puts(" -> created topic ##{topic.id}") {:error, :topic, changeset, _so_far} -> - IO.inspect changeset.errors + IO.inspect(changeset.errors) end end end From 3c3705e7a822c75000200a4cb00c0f1fd6e301fa Mon Sep 17 00:00:00 2001 From: MareStare Date: Thu, 27 Mar 2025 03:20:54 +0000 Subject: [PATCH 29/61] prettier . -w --- priv/repo/seeds/dev/communications.json | 54 +++++- priv/repo/seeds/dev/images.json | 220 ++++++------------------ priv/repo/seeds/pages/api.md | 25 +-- priv/repo/seeds/pages/markdown.md | 142 ++++++++------- priv/repo/seeds/seeds_development.json | 3 +- 5 files changed, 197 insertions(+), 247 deletions(-) diff --git a/priv/repo/seeds/dev/communications.json b/priv/repo/seeds/dev/communications.json index 43e235660..806e235b7 100644 --- a/priv/repo/seeds/dev/communications.json +++ b/priv/repo/seeds/dev/communications.json @@ -40,19 +40,55 @@ ], "titles": { "first": [ - "Doing", "Summoning", "Best practices of", "Ensuring", "Creating", - "Making", "Pretending to be", "I want", "The", "Writing", - "A", "Realizing", "Retconning", "Real", "Incredible" + "Doing", + "Summoning", + "Best practices of", + "Ensuring", + "Creating", + "Making", + "Pretending to be", + "I want", + "The", + "Writing", + "A", + "Realizing", + "Retconning", + "Real", + "Incredible" ], "second": [ - "pancakes", "monsters", "furries", "people", "mods", - "admins", "devs", "foxes", "wolves", "Jeff Bezos", - "cats", "dogs", "fruits", "code", "games" + "pancakes", + "monsters", + "furries", + "people", + "mods", + "admins", + "devs", + "foxes", + "wolves", + "Jeff Bezos", + "cats", + "dogs", + "fruits", + "code", + "games" ], "third": [ - "for fun!", "for profit!", "for fun and profit!", "for free", "thread", - "- a discussion", "- forum game", "- where to download?", "problem", "for the glory of the mods!", - "- a different perspective", "I dunno", "so I can be more popular", "for lolz", "- is it real??" + "for fun!", + "for profit!", + "for fun and profit!", + "for free", + "thread", + "- a discussion", + "- forum game", + "- where to download?", + "problem", + "for the glory of the mods!", + "- a different perspective", + "I dunno", + "so I can be more popular", + "for lolz", + "- is it real??" ] } } diff --git a/priv/repo/seeds/dev/images.json b/priv/repo/seeds/dev/images.json index 2582094ee..065f9c744 100644 --- a/priv/repo/seeds/dev/images.json +++ b/priv/repo/seeds/dev/images.json @@ -1,388 +1,278 @@ [ { "url": "https://furrycdn.org/img/view/2020/9/30/35230.jpg", - "sources": [ - "https://twitter.com/Lou_Art_93/status/1311435253026828288" - ], + "sources": ["https://twitter.com/Lou_Art_93/status/1311435253026828288"], "tag_input": "safe, otter, clothes, female, solo, smiling, solo female, dialogue, shirt, pants, text, signature, semi-anthro, offscreen character, green eyes, happy, standing, cute, outdoors, disney, english text, talking, zootopia, featured image, mammal, fur, door, barefoot, mrs. otterton (zootopia), brown fur, mustelid, silhouette, japanese text, brown body, artist:louart, topwear, bottomwear" }, { "url": "https://derpicdn.net/img/view/2019/3/26/1995489.webm", - "sources": [ - "https://twitter.com/StormXF3/status/1110609781897814023" - ], + "sources": ["https://twitter.com/StormXF3/status/1110609781897814023"], "tag_input": "webm, breaking the fourth wall, oc:echo, visual effects of awesome, wide eyes, weapons-grade cute, featured image, oc only, irl human, ear fluff, artist:stormxf3, dilated pupils, laptop computer, slit eyes, leaning, behaving like a cat, eyes on the prize, solo focus, sound, safe, pov, pony, photo, daaaaaaaaaaaw, fangs, female, food, amazing, eye dilation, fourth wall, that bat pony sure does love fruits, frown, hand, hnnng, human, irl, male, mare, monitor, oc, computer, bat pony, apple, animated, cute, tracking, bat pony oc, cuteness overload, ear tufts, offscreen character, looking at something, ocbetes" }, { "url": "https://furrycdn.org/img/view/2020/5/2/823.webm", - "sources": [ - "https://twitter.com/RikoSakari/status/1241720594107756544" - ], + "sources": ["https://twitter.com/RikoSakari/status/1241720594107756544"], "tag_input": "safe, clothes, tail, smiling, fangs, animated, feral, paws, sitting, wings, scarf, eyes closed, open mouth, semi-anthro, whiskers, duo, oc, happy, underpaw, dancing, cute, ambiguous gender, 2020, :3, watermark, webm, horns, paw pads, hug, ferret, featured image, mammal, sound, bat wings, artist:rikosakari, buttercup (song), music, hurondance, dot eyes, webbed wings, mustelid, domestic ferret, frame by frame, uwu, oc:riko sakari, smooth as butter, jack stauber" }, { "url": "https://derpicdn.net/img/view/2018/3/10/1676327.jpg", - "sources": [ - "https://www.deviantart.com/jowyb/art/Strong-Petals-734821216" - ], + "sources": ["https://www.deviantart.com/jowyb/art/Strong-Petals-734821216"], "tag_input": "younger, petals, featured image, sweet dreams fuel, jackabetes, weapons-grade cute, filly applejack, color porn, flower petals, smiling, wholesome, pear butter, pearabetes, jowybean is trying to murder us, artist:jowybean, duo, bed, bright, cute, daaaaaaaaaaaw, earth pony, eyes closed, feels, female, filly, freckles, happy, heartwarming, hnnng, hug, mare, morning ponies, mother and daughter, pillow, pony, precious, applejack, safe" }, { "url": "https://derpicdn.net/img/view/2018/7/15/1781742.gif", - "sources": [ - "https://www.deviantart.com/szafir87/art/Shy-Hug-754638552" - ], + "sources": ["https://www.deviantart.com/szafir87/art/Shy-Hug-754638552"], "tag_input": "szafir87 is trying to murder us, simple background, sitting, hiding behind wing, solo, transparent background, you are already dead, shyabetes, featured image, sweet dreams fuel, hug request, weapons-grade cute, eye shimmer, artist:szafir87, smiling, spread wings, adorable face, animated, blinking, blushing, bronybait, chest fluff, cute, daaaaaaaaaaaw, female, fluttershy, frown, gif, hiding, hnnng, looking at you, looking down, mare, mouth hold, note, pegasus, pony, raised hoof, safe, shy" }, { "url": "https://furrycdn.org/img/view/2020/7/20/9844.jpg", - "sources": [ - "https://www.deviantart.com/tomatocoup/art/The-Guard-782523190" - ], + "sources": ["https://www.deviantart.com/tomatocoup/art/The-Guard-782523190"], "tag_input": "safe, clothes, teeth, tail, female, solo, fangs, anthro, solo female, oc only, shark, breasts, cloud, swimsuit, looking at something, oc, standing, water, wet, fish, crepuscular rays, fish tail, one-piece swimsuit, ocean, fins, cloudy, shark tail, lifeguard, oc:erika (ambris), artist:tomatocoup" }, { "url": "https://furrycdn.org/img/view/2020/4/25/180.jpg", - "sources": [ - "https://www.deviantart.com/kaleido-art/art/Dance-with-me-838409320" - ], + "sources": ["https://www.deviantart.com/kaleido-art/art/Dance-with-me-838409320"], "tag_input": "safe, clothes, tail, female, smiling, ear fluff, anthro, male, wolf, dress, pants, eyes closed, simple background, open mouth, duo, canine, happy, shoes, artist:kaleido-art, haru (beastars), beastars, rabbit, dancing, cheek fluff, tan background, fluff, size difference, shipping, 2020, blush sticker, shadow, legoshi (beastars), featured image, plantigrade anthro, long ears, mammal, fur, white fur, male/female, lagomorph, gray fur, white body, ambient wildlife, gray body, ambient insect, harushi (beastars), anthro/anthro, bottomwear" }, { "url": "https://derpicdn.net/img/view/2015/9/23/985817.gif", - "sources": [ - "http://duocartoonist.tumblr.com/post/129677819320/see-up-my-first-rendition-of-nmm-the-nebulous" - ], + "sources": ["http://duocartoonist.tumblr.com/post/129677819320/see-up-my-first-rendition-of-nmm-the-nebulous"], "tag_input": "spread wings, alicorn, animated, artist:anima-dos, artist:lionheartcartoon, bat pony, bat wings, bedroom eyes, castle, crown, cute, evil laugh, eyeshadow, fangs, female, flapping, gif, grin, laughing, looking at you, makeup, mare, nightmare moon, open mouth, pony, raised hoof, redesign, safe, smirk, solo, unshorn fetlocks, slit eyes, artist:duo cartoonist, raised eyebrow, ethereal mane, moonbat, the moon rises, smiling, moonabetes, bat pony alicorn, smooth as butter" }, { "url": "https://furrycdn.org/img/view/2020/4/30/672.jpg", - "sources": [ - "https://twitter.com/k_b__m/status/729172237761150978" - ], + "sources": ["https://twitter.com/k_b__m/status/729172237761150978"], "tag_input": "safe, clothes, tail, female, fluffy, anthro, male, dialogue, paws, white background, fox, simple background, open mouth, duo, canine, green eyes, claws, underpaw, rabbit, fluff, nick wilde (zootopia), disney, colored pupils, talking, paw pads, judy hopps (zootopia), zootopia, red fox, necktie, purple eyes, featured image, mammal, artist:k_b__m, 2016, lagomorph, fluffy tail, palm pads" }, { "url": "https://derpicdn.net/img/view/2015/10/3/993821.jpg", - "sources": [ - "http://cannibalus.deviantart.com/art/Sunbutt-Portrait-563994112" - ], + "sources": ["http://cannibalus.deviantart.com/art/Sunbutt-Portrait-563994112"], "tag_input": "draw me like one of your french girls, drawing, duo, eating, fat, female, food, funny, funny as hell, ice cream, levitation, magic, obese, open mouth, painting, pony, prank, princess celestia, princess luna, prone, safe, smirk, tea, teacup, teapot, telekinesis, trolluna, wallpaper, this will end in pain, goblet, :t, tongue out, featured image, caricature, duo female, this will end in tears and/or a journey to the moon, best sisters, this will not end well, artception, smiling, ethereal tail, sibling rivalry, ethereal mane, royal sisters, nailed it, chubbylestia, lidded eyes, alicorn, cutelestia, lunabetes, sweet dreams fuel, technically advanced, tabun art-battle, tabun art-battle cover, artist:cannibalus, cake, cakelestia, close enough, cute" }, { "url": "https://derpicdn.net/img/view/2017/10/27/1571166.gif", - "sources": [ - "https://therealdjthed.deviantart.com/art/Super-Smile-Animation-Test-711915586" - ], + "sources": ["https://therealdjthed.deviantart.com/art/Super-Smile-Animation-Test-711915586"], "tag_input": "featured image, 3d, 3d model, animated, blender, blinking, bouncing, cute, cycles, earth pony, female, gif, grin, happy, headbob, mare, pinkie pie, pony, safe, simple background, solo, squee, diapinkes, weapons-grade cute, perfect loop, patreon, smiling, idle animation, ponk, patreon logo, cycles render, artist:therealdjthed, therealdjthed is trying to murder us, model:djthed, breathing" }, { "url": "https://derpicdn.net/img/view/2020/8/10/2419740.gif", - "sources": [ - "https://2snacks.tumblr.com/post/626019622654787584/httpsyoutubezhu1luqmn9g" - ], + "sources": ["https://2snacks.tumblr.com/post/626019622654787584/httpsyoutubezhu1luqmn9g"], "tag_input": "get stick bugged lol, princess cadance, featured image, adorawat, perfect loop, pony, pixel art, meme, female, dancing, changeling queen, changeling, artist:2snacks, animated, alicorn, wat, synchronized, sweatdrop, sweat, open mouth, cute, queen chrysalis, safe, crystal castle, princess flurry heart, ponified meme, blursed image" }, { "url": "https://furrycdn.org/img/view/2021/5/22/87948.png", - "sources": [ - "https://www.deviantart.com/yakovlev-vad/art/Sisu-878339374" - ], + "sources": ["https://www.deviantart.com/yakovlev-vad/art/Sisu-878339374"], "tag_input": "safe, tail, female, solo, smiling, solo female, hair, feral, paws, dragon, tree, claws, eyebrows, water, disney, horns, eyelashes, dragoness, featured image, leaf, mane, side view, eastern dragon, magenta eyes, pink eyes, purple hair, blue hair, blue body, aquatic dragon, holding object, artist:yakovlev-vad, blue mane, fictional species, artifact, multicolored body, 2021, raya and the last dragon, sisu (raya and the last dragon), water bubble" }, { "url": "https://furrycdn.org/img/view/2020/7/11/7370.png", - "sources": [ - "https://twitter.com/HiccupsDoesSFW/status/959798227993317376/photo/1" - ], + "sources": ["https://twitter.com/HiccupsDoesSFW/status/959798227993317376/photo/1"], "tag_input": "safe, clothes, tail, female, solo, smiling, ear fluff, anthro, solo female, hair, paws, looking at you, sitting, my little pony, wolf, collar, abstract background, signature, crossed legs, canine, underpaw, fluff, species swap, paw pads, digitigrade anthro, featured image, mammal, hasbro, white outline, sunset shimmer (mlp), artist:hiccupsdoesart, friendship is magic" }, { "url": "https://derpicdn.net/img/view/2016/6/20/1182765.gif", - "sources": [ - "http://megamanhxh.deviantart.com/art/Animation-A-malfunctioning-book-pony-616553371" - ], + "sources": ["http://megamanhxh.deviantart.com/art/Animation-A-malfunctioning-book-pony-616553371"], "tag_input": "silly pony, simple background, sitting, solo, twilight sparkle, white background, wingboner, tongue out, dork, discussion in the comments, twiabetes, weapons-grade cute, majestic as fuck, behaving like a dog, lidded eyes, active stretch, smiling, artist:megamanhxh, spread wings, twilight sparkle (alicorn), safe, pony, photoshop, gif, floppy ears, flexible, flapping, ear scratch, daaaaaaaaaaaw, cute, animated, alicorn, adorkable, :p, silly, scratching, female, show accurate" }, { "url": "https://furrycdn.org/img/view/2020/7/8/6299.png", - "sources": [ - "" - ], + "sources": [""], "tag_input": "safe, tail, solo, ear fluff, tongue out, feral, paws, fox, simple background, transparent background, one eye closed, canine, claws, cheek fluff, fluff, cute, ambiguous gender, yellow eyes, holding, head fluff, colored pupils, blep, tongue, furbooru exclusive, featured image, mammal, high res, furbooru, white outline, tail hold, tail fluff, artist:chonkycrunchy, astra" }, { "url": "https://derpicdn.net/img/view/2015/3/12/847656.gif", - "sources": [ - "http://nuksponyart.tumblr.com/post/113386426665/young-twilight-understands-every-cat-owners" - ], + "sources": ["http://nuksponyart.tumblr.com/post/113386426665/young-twilight-understands-every-cat-owners"], "tag_input": "behaving like a cat, looking up, curled up, spikelove, twiabetes, featured image, bookshelf, weapons-grade cute, wide eyes, spikabetes, artist:nukilik, lidded eyes, definition of insanity, smiling, unicorn twilight, nukilik is trying to murder us, filly twilight sparkle, that pony sure does love books, frame by frame, debate in the comments, animated, annoyed, baby, baby dragon, baby spike, book, cute, cutie mark, daaaaaaaaaaaw, diabetes, dragon, eyeroll, eyes closed, female, filly, floppy ears, frown, grumpy, hnnng, ladder, levitation, library, loop, magic, male, mama twilight, nuzzling, photoshop, pony, ponyloaf, prone, reading, safe, sitting, sleeping, snuggling, spike, telekinesis, twilight sparkle, unamused, unicorn, yawn, younger" }, { "url": "https://derpicdn.net/img/view/2014/10/9/739465.jpg", - "sources": [ - "https://devinian.deviantart.com/art/The-Golden-Cage-487369223" - ], + "sources": ["https://devinian.deviantart.com/art/The-Golden-Cage-487369223"], "tag_input": "politics in the comments, alicorn, artist:devinian, beautiful, cage, cake, chest, clothes, couch, crepuscular rays, cushion, derail in the comments, detailed, dress, female, fireplace, fishnets, interior, jewelry, levitation, light, magic, mare, painting, palace, philomena, photoshop, pony, princess celestia, princess luna, safe, scenery, socks, phoenix, wall of tags, technically advanced, duo, pegasus, rainbow dash, luxury, absurd resolution, tea, teacup, teapot, telekinesis, twilight sparkle, wallpaper, window, indoors, baroque, bird cage, glorious, scenery porn, featured image, chandelier, dust motes, color porn, smiling, new crown, twilight sparkle (alicorn)" }, { "url": "https://furrycdn.org/img/view/2020/8/26/20869.png", - "sources": [ - "" - ], + "sources": [""], "tag_input": "safe, tail, solo, ear fluff, anthro, feral, paws, fox, sitting, chest fluff, speech bubble, text, simple background, signature, canine, cheek fluff, fluff, cute, meme, tears, talking, purple background, furbooru exclusive, featured image, chibi, mammal, high res, fur, furbooru, front view, purple fur, artist:sorajona, artist:skodart, astra, astrael, mascot, joke, tempting fate, bottom, this will end in lewds" }, { "url": "https://furrycdn.org/img/view/2021/3/17/74641.jpg", - "sources": [ - "https://twitter.com/popodunk/status/1370774397024342016" - ], + "sources": ["https://twitter.com/popodunk/status/1370774397024342016"], "tag_input": "safe, clothes, teeth, female, solo, smiling, anthro, solo female, looking at you, white background, dress, blushing, simple background, hat, rabbit, cute, eyebrows, flower, disney, judy hopps (zootopia), zootopia, purple eyes, featured image, floppy ears, mammal, fur, lagomorph, gray fur, sun hat, gray body, smiling at you, artist:popodunk" }, { "url": "https://derpicdn.net/img/view/2021/9/16/2701452.png", - "sources": [ - "http://viwrastupr.deviantart.com/art/My-Little-Pony-Friendship-is-Magic-667085058" - ], + "sources": ["http://viwrastupr.deviantart.com/art/My-Little-Pony-Friendship-is-Magic-667085058"], "tag_input": "trixie's wagon, alicorn, alicorn amulet, angel bunny, apple, apple bloom, applejack, artist:viwrastupr, bag, balcony, beautiful, big macintosh, blaze, blossomforth, book, bow, bridge, canterlot, canyon, cape, caramel, castle, cello, changeling, cloak, clothes, cloud, cloud kicker, cloudchaser, cloudsdale, clubhouse, crib, crown, crusaders clubhouse, crystal ball, crystal empire, cup, cute, cutie mark crusaders, daily deviation, daisy, derpy hooves, discord, dj pon-3, doctor whooves, dragon, drink, element of magic, epic, everfree forest, eyes closed, fancypants, fascinating, female, fleetfoot, fleur-de-lis, flitter, flower, flower trio, flower wishes, fluttershy, flying, food, friends, fruit, gilda, glass, glasses, glowing horn, goggles, grass, griffon, group, gummy, hair bow, hat, jewelry, lake, lemon hearts, light, lily, lyra heartstrings, magic, male, mane seven, mane six, minuette, moondancer, mountain, mountain range, mouth hold, multicolored hair, necktie, night, oc, opalescence, open mouth, owlowiscious, park, path, pegasus, philomena, phoenix, pillow, pinkie pie, playing, poison joke, pond, pony, ponyville, potion, princess cadance, princess celestia, princess luna, queen chrysalis, rainbow dash, raised hoof, rarity, raven, reading, river, rose, roseluck, safe, salad, scarf, scenery, school, scootaloo, self ponidox, shining armor, sitting, sky, sleeping, soarin', solar system, spike, spitfire, stage, starry night, stars, statue, sunburst, surprise, surprised, swarm, sweetie belle, sweetie drops, table, tank, telekinesis, tiara, timber wolf, time turner, toffee, tree, trixie, trixie's cape, twilight sparkle, twinkleshine, uniform, vinyl scratch, water, waterfall, wings, winona, wonderbolts, wonderbolts uniform, zebra, zecora, zecora's hut, looking at each other, bon bon, regalia, canterlot castle, pulling, father and daughter, scenery porn, octavia melody, balancing, lily valley, featured image, underhoof, bookshelf, trixie's hat, night sky, greta, castle of the royal pony sisters, large wings, twilight's castle, cello bow, lidded eyes, the hall of friendship, backwards cutie mark, alicorn hexarchy, color porn, starlight glimmer, cutie map, too big for derpibooru, griffonstone, castle griffonstone, smiling, tantabus, spread wings, gabby, tree of harmony, majestic, curved horn, oc:fausticorn, canterlot five, princess flurry heart, wall of tags, multicolored tail, colored pupils, bow (instrument), magnum opus, astronomical detail, twilight sparkle (alicorn), musical instrument, absurd resolution, technically advanced, sweet apple acres" }, { "url": "https://derpicdn.net/img/view/2013/2/3/232093.gif", - "sources": [ - "http://www.reddit.com/r/mylittlepony/comments/17s485/did_someone_ask_for_a_gif_or_apng_of_dash_and/" - ], + "sources": ["http://www.reddit.com/r/mylittlepony/comments/17s485/did_someone_ask_for_a_gif_or_apng_of_dash_and/"], "tag_input": "dashabetes, shifty eyes, artist:marminatoror, just for sidekicks, nose rub, transparent background, sleepless in ponyville, simple background, season 3, filly, pegasus, adventure in the comments, scootalove, safe, upvote event horizon, smiling, tsunderainbow, nose kiss, animated, scootaloo, boop, cute, cutealoo, daaaaaaaaaaaw, duo, derail in the comments, edit, eyes closed, female, gif, grin, happy, hnnng, looking around, mare, nuzzling, pony, rainbow dash, saddle bag, weapons-grade cute, tsundere, sweet dreams fuel" }, { "url": "https://furrycdn.org/img/view/2020/7/5/5709.png", - "sources": [ - "https://twitter.com/2d10art/status/1279807434102628352/photo/1" - ], + "sources": ["https://twitter.com/2d10art/status/1279807434102628352/photo/1"], "tag_input": "safe, clothes, tail, female, solo, ear fluff, anthro, solo female, paws, looking at you, signature, hat, pokémon, underpaw, cheek fluff, fluff, braixen, forest, outdoors, head fluff, raised tail, nintendo, digitigrade anthro, featured image, leaf, amber eyes, kneeling, yellow fur, mammal, fur, front view, tail fluff, crouching, white fur, shoulder fluff, orange fur, artist:2d10art, fictional species, technical advanced, starter pokémon" }, { "url": "https://derpicdn.net/img/view/2014/3/20/580031.gif", - "sources": [ - "http://misterdavey.tumblr.com/post/80134333183/lesbian-fantasies" - ], + "sources": ["http://misterdavey.tumblr.com/post/80134333183/lesbian-fantasies"], "tag_input": "gala dress, clothes, candle, bubble, blushing, bipedal, bedroom eyes, pegasus, unicorn, eyes closed, fantasizing, discussion in the comments, female, flower, food, frame, crush, hnnng, hug, imagination, juice, diabetes, juice box, kissing, lesbian, nightgown, nose wrinkle, nuzzling, out of character, picture, picture frame, plate, artist:misterdavey, animated, adventure in the comments, plushie, pony, rainbow dash, rainbow dash always dresses in style, rainbow dash's house, raridash, rarity, refrigerator, rose, safe, see-through, shipping, snuggling, spoon, standing, table, waifu dinner, not creepy, plates, spiderman thread, eye contact, dashabetes, featured image, weapons-grade cute, raribetes, jontron thread, eye shimmer, doctor who thread, waltz, rarity plushie, smiling, spread wings, wall of tags, crush plush, misterdavey is trying to murder us, jontron in the comments, doctor who in the comments, spiderman in the comments, dinner, doll, dress, derail in the comments, useless source url, source needed, dead source, dancing, daaaaaaaaaaaw, cute, cuddling" }, { "url": "https://derpicdn.net/img/view/2015/4/20/878570.gif", - "sources": [ - "http://supereddit-blog.tumblr.com/post/116885770608/sweetie-belle-gets-her-cutiemark" - ], + "sources": ["http://supereddit-blog.tumblr.com/post/116885770608/sweetie-belle-gets-her-cutiemark"], "tag_input": "special talent, sweetie belle, telekinesis, vinyl scratch, wat, gritted teeth, diamondbelle, artist:superedit, the great and powerful superedit, octavia melody, wide eyes, unexpected, cutiespark, discovery family logo, bloom and gloom, music judges meme, smiling, hoof hold, what the hay?, funny, grin, implied shipping, frown, judges, lesbian, magic, season 5, gif, earth pony, unicorn, animated, bedroom eyes, bipedal, blushing, cute, cutie mark, diamond tiara, dj pon-3, edit, edited screencap, embarrassed, facehoof, female, meme, open mouth, photoshop, pony, rarity, safe, score, screencap, shipping" }, { "url": "https://furrycdn.org/img/view/2020/4/20/0.png", - "sources": [ - "" - ], + "sources": [""], "tag_input": "safe, tail, solo, fluffy, fox, eyes closed, simple background, transparent background, canine, vector, cheek fluff, fluff, ambiguous gender, vulpine, head fluff, furbooru exclusive, .svg available, featured image, meta, mammal, svg, fur, ambiguous form, furbooru, logo, artist:aureai, digital art, purple fur, fluffy tail, astra, mascot, it begins" }, { "url": "https://derpicdn.net/img/view/2015/1/3/798402.gif", - "sources": [ - "http://nuksponyart.tumblr.com/post/106937983375/shining-giving-his-litle-sister-a-pony-version-of" - ], + "sources": ["http://nuksponyart.tumblr.com/post/106937983375/shining-giving-his-litle-sister-a-pony-version-of"], "tag_input": "sweet dreams fuel, weapons-grade cute, brother and sister, twilight sparkle, duckery in the comments, artist:nukilik, sibling bonding, equestria's best brother, smiling, shining adorable, nukilik is trying to murder us, sweet, siblings, shining armor, safe, pony, ponies riding ponies, piggyback ride, photoshop, open mouth, hnnng, heartwarming, happy, grin, filly, diabetes, daaaaaaaaaaaw, cute, bouncing, bbbff, animated, duo, younger, filly twilight sparkle, female, gif, twilight riding shining armor, riding, unicorn, unicorn twilight, twiabetes, frame by frame, featured image, equestria's best big brother" }, { "url": "https://furrycdn.org/img/view/2021/12/31/133675.png", - "sources": [ - "https://www.deviantart.com/yakovlev-vad/art/Faun-Elora-762586083" - ], + "sources": ["https://www.deviantart.com/yakovlev-vad/art/Faun-Elora-762586083"], "tag_input": "safe, tail, female, solo, pubic fluff, smiling, ear fluff, anthro, solo female, breasts, jewelry, chest fluff, cleavage, tree, green eyes, fluff, cute, bracelet, outdoors, spyro the dragon (series), eyelashes, mammal, sexy, fur, faun, tail fluff, shoulderless, shoulder fluff, orange fur, artist:yakovlev-vad, elora (spyro), adorasexy, minidress, strapless, fictional species" }, { "url": "https://furrycdn.org/img/view/2020/5/6/1252.jpg", - "sources": [ - "https://www.deviantart.com/tamberella/art/Eevee-s-Rainbow-790535422" - ], + "sources": ["https://www.deviantart.com/tamberella/art/Eevee-s-Rainbow-790535422"], "tag_input": "safe, tail, feral, paws, eevee, reflection, grass, eeveelution, vaporeon, pokémon, sylveon, jolteon, neck fluff, flareon, glaceon, espeon, leafeon, fluff, umbreon, fire, ambiguous gender, group, artist:tamberella, nintendo, fish tail, running, featured image, leaf, 2019, yellow fur, mammal, blue fur, fur, fins, digital art, brown fur, pink fur, black fur, purple fur, green fur, orange fur, fictional species, color porn, technical advanced" }, { "url": "https://derpicdn.net/img/view/2016/5/6/1147843.gif", - "sources": [ - "http://trombonyponypie.deviantart.com/art/Wakey-Wakey-Animated-Gif-607274893" - ], + "sources": ["http://trombonyponypie.deviantart.com/art/Wakey-Wakey-Animated-Gif-607274893"], "tag_input": "gif, artist:trombonyponypie, smiling, visual effects of awesome, weapons-grade cute, underhoof, diapinkes, yawn, waking up, stretching, solo, sleeping, safe, realistic, pony, pinkie pie, pillow, morning ponies, looking at you, hnnng, happy, fluffy, eyes closed, earth pony, diabetes, derail in the comments, cute, 3d, c:, blender, eyes open, smiling at you, on side, female, blanket, sweet dreams fuel, bed, animated, adventure in the comments, detailed hair" }, { "url": "https://furrycdn.org/img/view/2021/3/23/76073.png", - "sources": [ - "https://twitter.com/wolfypon/status/1374492788742496261" - ], + "sources": ["https://twitter.com/wolfypon/status/1374492788742496261"], "tag_input": "safe, tail, female, solo, ear fluff, solo female, oc only, feral, paws, fox, chest fluff, simple background, transparent background, oc, canine, commission, cheek fluff, neck fluff, fluff, eyebrows, head fluff, eyelashes, featured image, mammal, blue fur, cyan eyes, butt fluff, high res, fur, tail fluff, shoulder fluff, artist:wolfypon, socks (leg marking), blue body, multicolored fur, firefox (browser), vixen, globe, 2021, oc:double colon" }, { "url": "https://derpicdn.net/img/view/2015/3/27/858027.gif", - "sources": [ - "https://derpibooru.org/images/858027" - ], + "sources": ["https://derpibooru.org/images/858027"], "tag_input": "artist:sampodre, gif, creepy awesome, silhouette, 3d, adventure in the comments, animated, changeling, cinemagraph, epic, female, gif party, glowing eyes, open mouth, photoshop, queen chrysalis, rain, safe, sharp teeth, smirk, solo, wet, featured image, visual effects of awesome, derpibooru exclusive" }, { "url": "https://derpicdn.net/img/view/2015/11/21/1026784.gif", - "sources": [ - "https://twitter.com/ziroro326/status/667726177813958656" - ], + "sources": ["https://twitter.com/ziroro326/status/667726177813958656"], "tag_input": "missing cutie mark, adorkable, alicorn, animated, black and white, cute, dancing, derp, ear twitch, female, floppy ears, grayscale, kicking, mare, monochrome, party hard, pixiv, pony, safe, silly, silly pony, simple background, solo, swing, twilight sparkle, white background, dork, :o, twiabetes, sweet dreams fuel, weapons-grade cute, artist:jirousan, do the sparkle, twilight sparkle (alicorn), jirousan is trying to murder us, club can't handle me, frame by frame" }, { "url": "https://furrycdn.org/img/view/2020/7/23/11106.gif", - "sources": [ - "https://ostinlein.tumblr.com/post/617228745665839104/characters-belong" - ], + "sources": ["https://ostinlein.tumblr.com/post/617228745665839104/characters-belong"], "tag_input": "safe, clothes, teeth, tail, fangs, anthro, male, oc only, piercing, animated, sitting, dog, shirt, pants, kissing, bird, signature, looking at each other, hat, duo, oc, gif, canine, claws, cheek fluff, neck fluff, fluff, cute, beak, scenery, holding, head fluff, shipping, hand hold, drink, digitigrade anthro, indoors, necktie, feathers, featured image, sharp teeth, mammal, hand on face, fur, tail fluff, glass, wine glass, brown fur, bird feet, males only, male/male, frame by frame, bar, tail feathers, oc x oc, red feathers, yellow feathers, tan fur, suspenders, artist:ostinlein, oc:tyler, oc:fletcher, galliform, art deco, topwear, smooth as butter, bottomwear" }, { "url": "https://furrycdn.org/img/view/2020/8/29/22679.gif", - "sources": [ - "https://www.furaffinity.net/view/25506150/" - ], + "sources": ["https://www.furaffinity.net/view/25506150/"], "tag_input": "safe, clothes, solo, anthro, male, animated, paws, solo male, goat, abstract background, signature, open mouth, gif, fluff, cute, undertale, asriel dreemurr (undertale), flower, head fluff, tongue, featured image, floppy ears, black eyes, mammal, bovid, frame by frame, sneezing, artist:absolutedream" }, { "url": "https://furrycdn.org/img/view/2020/4/25/267.gif", - "sources": [ - "https://inkbunny.net/s/1766018" - ], + "sources": ["https://inkbunny.net/s/1766018"], "tag_input": "safe, clothes, female, solo, anthro, solo female, deer, oc only, breasts, freckles, animated, eyes closed, skirt, oc, gif, happy, dancing, cute, artist:kanashiipanda, book of lust, oc:julia woods, mammal, frame by frame, ocbetes, smooth as butter, bottomwear" }, { "url": "https://derpicdn.net/img/view/2017/3/10/1383501.gif", - "sources": [ - "http://deannart.deviantart.com/art/Preview-2-Slice-Of-Life-668121278" - ], + "sources": ["http://deannart.deviantart.com/art/Preview-2-Slice-Of-Life-668121278"], "tag_input": "smooth as butter, glare, hilarious in hindsight, magic, open mouth, pony, frown, eyes closed, earth pony, drool, luna is not amused, frame by frame, alicorn, animated, marriage, artist:deannart, blinking, fiery shimmer, cutelestia, cute, mare, female, nintendo, featured image, wedding, bored, preview, princess celestia, princess luna, safe, sigh, sitting, sleeping, snoring, unamused, unicorn, gritted teeth, sunset shimmer, underhoof, maud pie, majestic as fuck, gamer sunset, lidded eyes, gif, slice of life (episode), smiling, hoof hold, nintendo switch" }, { "url": "https://furrycdn.org/img/view/2020/8/19/18670.jpg", - "sources": [ - "https://twitter.com/Ruribec/status/1190149320374284289" - ], + "sources": ["https://twitter.com/Ruribec/status/1190149320374284289"], "tag_input": "safe, clothes, teeth, female, solo, smiling, fangs, anthro, solo female, hair, oc only, paws, looking at you, sky, dress, cat, tree, signature, oc, claws, grass, underpaw, cheek fluff, standing, fluff, feline, yellow eyes, raised leg, outdoors, night, food, paw pads, digitigrade anthro, featured image, sharp teeth, mammal, blue fur, ear tuft, fur, front view, slit pupils, barefoot, fence, halloween, white fur, holiday, gray fur, gray hair, white body, graveyard, pumpkin, gray body, vegetables, artist:ruribec, oc:kelly (ruribec), cemetery" }, { "url": "https://derpicdn.net/img/view/2015/3/22/854962.gif", - "sources": [ - "http://joshng.deviantart.com/art/Rainbow-Rockin-Animation-521841679" - ], + "sources": ["http://joshng.deviantart.com/art/Rainbow-Rockin-Animation-521841679"], "tag_input": "ponytail, human coloration, musical instrument, alternate hairstyle, animated, awesome, boots, clothes, decepticon, equestria girls, eyes closed, female, guitar, headbang, human, humanized, jacket, kneesocks, metal, rainbow dash, rainbow socks, safe, shirt, shorts, simple background, skinny, smirk, socks, solo, transformers, white background, denim, electric guitar, striped socks, perfect loop, full body, artist:joshng, smiling, smooth as butter" }, { "url": "https://furrycdn.org/img/view/2020/10/28/42897.png", - "sources": [ - "https://www.furaffinity.net/view/33574531/" - ], + "sources": ["https://www.furaffinity.net/view/33574531/"], "tag_input": "safe, clothes, tail, female, solo, anthro, solo female, hair, breasts, bra, panties, underwear, cell phone, phone, green eyes, rabbit, cute, angry, colored pupils, eyelashes, indoors, plushie, long hair, featured image, long ears, kneeling, mammal, tank top, sexy, fur, front view, short tail, brown hair, brown fur, all fours, pale belly, small breasts, gloves (arm marking), socks (leg marking), lagomorph, furniture, multicolored fur, game controller, tan fur, frame, adorasexy, frowning, brown body, tan body, madorable, topwear, pouting, artist:autumndeer" }, { "url": "https://furrycdn.org/img/view/2021/1/18/62645.jpg", - "sources": [ - "https://twitter.com/ActuallyYshanii/status/1350865352243224581" - ], + "sources": ["https://twitter.com/ActuallyYshanii/status/1350865352243224581"], "tag_input": "safe, clothes, tail, female, solo, anthro, solo female, paws, fox, thigh highs, heart, snow, canine, cheek fluff, fluff, cute, raised tail, blue eyes, leg warmers, face down ass up, digitigrade anthro, featured image, mammal, legwear, fur, tail wag, front view, three-quarter view, toeless legwear, orange fur, snowfall, brown nose, cream fur, orange body, vixen, cream body, artist:yshanii" }, { "url": "https://furrycdn.org/img/view/2020/6/21/3578.png", - "sources": [ - "https://www.furaffinity.net/view/36893257/" - ], + "sources": ["https://www.furaffinity.net/view/36893257/"], "tag_input": "safe, tail, female, solo, smiling, solo female, hair, feral, paws, cloud, eyes closed, sleeping, lying down, underpaw, fluff, scenery, goggles, hammer, prone, scenery porn, featured image, blue fur, fur, front view, tail fluff, ringtail, ratchet & clank, lombax, feralized, prosthetics, artist:viwrastupr, white hair, prosthetic arm, fictional species, technical advanced, rivet (r&c)" }, { "url": "https://derpicdn.net/img/view/2017/3/6/1381041.gif", - "sources": [ - "http://luminaura.deviantart.com/art/Rubbing-all-the-princesses-cheeks-656801151" - ], + "sources": ["http://luminaura.deviantart.com/art/Rubbing-all-the-princesses-cheeks-656801151"], "tag_input": "twiabetes, sweet dreams fuel, weapons-grade cute, wide eyes, lunabetes, patreon, alicorn tetrarchy, artist:lumineko, lidded eyes, lumineko is trying to murder us, smiling, spread wings, cutedance, patreon logo, lumineko's nuzzling princesses, twilight sparkle (alicorn), non-consensual nuzzling, luna is not amused, daaaaaaaaaaaw, animated, blushing, c:, crown, cute, cutelestia, cheek to cheek, eyes closed, alicorn, female, floppy ears, flower, frown, gif, hape, hnnng, hug, jewelry, mare, nuzzling, open mouth, pony, princess cadance, princess celestia, princess luna, rubbing, safe, snuggling, surprised, twilight sparkle, varying degrees of want, wink, regalia, :t, :o, one eye closed" }, { "url": "https://furrycdn.org/img/view/2021/3/19/75280.gif", - "sources": [ - "https://www.furaffinity.net/view/32400757/" - ], + "sources": ["https://www.furaffinity.net/view/32400757/"], "tag_input": "safe, female, solo, smiling, solo female, hair, oc only, tongue out, animated, feral, white background, eyes closed, blushing, simple background, open mouth, oc, gif, cheek fluff, fluff, feline, cute, shy, lynx, blep, feather, tongue, featured image, 2019, mammal, teal eyes, ear tuft, fur, low res, pale belly, frame by frame, spotted fur, artist:tuwka, feather in hair, solo ambiguous, hair accessory, oc:kamari" }, { "url": "https://furrycdn.org/img/view/2020/8/8/15561.jpg", - "sources": [ - "https://www.furaffinity.net/view/31801339/" - ], + "sources": ["https://www.furaffinity.net/view/31801339/"], "tag_input": "safe, tail, female, solo, smiling, ear fluff, anthro, solo female, hair, oc only, butt, looking at you, fox, bird, abstract background, signature, oc, canine, fluff, toy, cute, bathtub, water, wet, raised tail, blue eyes, duck, bathroom, indoors, featured image, floppy ears, mammal, sexy, ears, fur, tail fluff, the ass was fat, bubbles, orange hair, 2018, orange fur, bath, >:3, mischievous, adorasexy, cream fur, orange body, vixen, artist:amarihel, bubble bath, rubber duck, oc:tfs (amarihel), waterfowl, cream body" }, { "url": "https://derpicdn.net/img/view/2019/1/16/1937202.gif", - "sources": [ - "" - ], + "sources": [""], "tag_input": "animated, bedroom eyes, blinking, blowing, derpibooru, dialogue, downvote, earth pony, everything is ruined, female, flash, floppy ears, frown, gif, glowing horn, green eyes, happy, levitation, looking back, looking down, magic, mare, meta, metamorphosis, now you fucked up, oc, open mouth, ponified, pony, red eyes, sad, safe, simple background, sisters, telekinesis, transformation, unicorn, upvote, white background, wink, looking at each other, looking up, one eye closed, oc only, featured image, wide eyes, derpibooru exclusive, smiling, artist:justisanimation, oc:upvote, oc:downvote, derpibooru ponified, downvote's downvotes, shook, this will end in pain, ear twitch, shocked, horrified, shrunken pupils, wall of tags" }, { "url": "https://derpicdn.net/img/view/2014/8/18/702641.png", - "sources": [ - "http://ukulilia.deviantart.com/art/Coffee-to-stay-awake-all-night-475492803" - ], + "sources": ["http://ukulilia.deviantart.com/art/Coffee-to-stay-awake-all-night-475492803"], "tag_input": "smug, safe, realistic, princess luna, artist:mykegreywolf, pony, mug, technically advanced, makeup, magic, female, looking at you, translated in the comments, alternate hairstyle, lips, high res, german, eyeshadow, detailed, cute, alicorn, artist:katputze, collaboration, coffee mug, coffee, beautiful, photoshop elements, smiling, lidded eyes, lunabetes, featured image, 2014, telekinesis, solo" }, { "url": "https://derpicdn.net/img/view/2019/5/10/2035594.gif", - "sources": [ - "https://twitter.com/nastylittlepest/status/1126899230151467008" - ], + "sources": ["https://twitter.com/nastylittlepest/status/1126899230151467008"], "tag_input": "sweet dreams fuel, frame by frame, ocbetes, lidded eyes, smiling, artist:angrylittlerodent, oc:pezzhorse, wholesome, rodent is trying to murder us, oc:rodentmod, snuggling, sleeping, safe, precious, pony, pillow, oc, nuzzling, mare, male, hug, hnnng, gif, frown, floppy ears, female, eyes closed, earth pony, ear twitch, dark, daaaaaaaaaaaw, cute, cuddling, couple, blanket, dead source, bed, duo, animated, :<, stallion, unicorn, ear fluff, oc only, featured image, weapons-grade cute" }, { "url": "https://derpicdn.net/img/view/2012/1/2/0.jpg", - "sources": [ - "" - ], + "sources": [""], "tag_input": "chair, eyes, female, grin, hilarious in hindsight, adventure in the comments, image macro, derpibooru legacy, cigar, building, smiling, featured image, gritted teeth, swinging, stallion, spider-man, smoking, sitting, safe, pony, ponified, phone, pegasus, parody, paper, necktie, muffin, meme, mare, male, mail, letter, j. jonah jameson, it begins, derpy hooves, bag, song in the comments, artist needed" }, { "url": "https://derpicdn.net/img/view/2015/9/26/988000.gif", - "sources": [ - "https://derpibooru.org/988000" - ], + "sources": ["https://derpibooru.org/988000"], "description": "Fairly large GIF (~23MB), use to test WebM stuff.", "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof" }, { "url": "https://derpicdn.net/img/2012/1/2/25/large.png", - "sources": [ - "https://derpibooru.org/25" - ], + "sources": ["https://derpibooru.org/25"], "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood" }, { "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm", - "sources": [ - "http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309" - ], + "sources": ["http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"], "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm" }, { "url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg", - "sources": [ - "http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the" - ], + "sources": ["http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the"], "tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres" }, { "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg", - "sources": [ - "https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926" - ], + "sources": ["https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"], "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper" }, { "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg", - "sources": [ - "https://derpibooru.org/2067468" - ], + "sources": ["https://derpibooru.org/2067468"], "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector" } ] diff --git a/priv/repo/seeds/pages/api.md b/priv/repo/seeds/pages/api.md index d896f7f44..fcc6c6315 100644 --- a/priv/repo/seeds/pages/api.md +++ b/priv/repo/seeds/pages/api.md @@ -10,15 +10,15 @@ If images are used, the artist must always be credited (if provided) and the ori This is a list of general parameters that are useful when working with the API. Not all parameters may be used in every request. -Name | Description ---- | --- -`filter_id` Assuming | the user can access the filter ID given by the parameter, overrides the current filter for this request. This is primarily `useful` | for unauthenticated API access. -`key` | An optional authentication token. If omitted, no user will be authenticated. You can find your authentication token in your [account settings](/registration/edit). -`page` | Controls the current page of the response, if the response is paginated. Empty values default to the first page. -`per_page` | Controls the number of results per page, up to a limit of 50, if the response is paginated. The default is 25. -`q` | The current search query, if the request is a search request. -`sd` | The current sort direction, if the request is a search request. -`sf` | The current sort field, if the request is a search request. +| Name | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| `filter_id` Assuming | the user can access the filter ID given by the parameter, overrides the current filter for this request. This is primarily `useful` | for unauthenticated API access. | +| `key` | An optional authentication token. If omitted, no user will be authenticated. You can find your authentication token in your [account settings](/registration/edit). | +| `page` | Controls the current page of the response, if the response is paginated. Empty values default to the first page. | +| `per_page` | Controls the number of results per page, up to a limit of 50, if the response is paginated. The default is 25. | +| `q` | The current search query, if the request is a search request. | +| `sd` | The current sort direction, if the request is a search request. | +| `sf` | The current sort field, if the request is a search request. | ## Routes @@ -227,15 +227,16 @@ The interested reader may find the implementations of these endpoints [here](htt Posting images should be done via request body parameters. An example with all parameters included is shown below. -You are *strongly recommended* to test code using this endpoint using a local copy of the website's source code. Abuse of the endpoint **will result in a ban**. +You are _strongly recommended_ to test code using this endpoint using a local copy of the website's source code. Abuse of the endpoint **will result in a ban**. -You *must* provide the direct link to the image in the `url` parameter. +You _must_ provide the direct link to the image in the `url` parameter. -You *must* set the `content-type` header to `application/json` for the site to process your request. +You _must_ set the `content-type` header to `application/json` for the site to process your request. ``` POST /api/v1/json/images?key=API_KEY ``` + ``` { "image": { diff --git a/priv/repo/seeds/pages/markdown.md b/priv/repo/seeds/pages/markdown.md index 2606c5200..f03e5d6c9 100644 --- a/priv/repo/seeds/pages/markdown.md +++ b/priv/repo/seeds/pages/markdown.md @@ -1,27 +1,29 @@ This page is here to help you get a better grasp on the syntax of Markdown, the text processing engine this site uses. ## Inline formatting + Inline formatting is the most commonly seen type of text formatting in Markdown. It can be applied almost anywhere else and doesn't depend on specific context (most of the time). -Operator | Example | Result ---- | --- | --- -Bold | `This is **huge**` | This is **huge** -Italic | `*very* clever, Connor... _very..._` | *very* clever, Connor... _very..._ -Underline | `And I consider this __important__` | And I consider this __important__ -Strikethrough | `I am ~~wrong~~ right` | I am ~~wrong~~ right -Superscript | `normal text ^superscripted text^` | normal text ^superscripted text^ -Subscript | `normal text %subscripted text%` | normal text %subscripted text% -Spoiler | `Psst! ||Darth Vader is Luke's father||` | Psst! ||Darth Vader is Luke's father|| -Code | ```Use `**bold**` to make text bold!``` | Use `**bold**` to make text bold! +| Operator | Example | Result | +| ------------- | ------------------------------------- | ---------------------------------- | ---------------------------- | --- | --- | ----- | --- | ---------------------------- | --- | +| Bold | `This is **huge**` | This is **huge** | +| Italic | `*very* clever, Connor... _very..._` | _very_ clever, Connor... _very..._ | +| Underline | `And I consider this __important__` | And I consider this **important** | +| Strikethrough | `I am ~~wrong~~ right` | I am ~~wrong~~ right | +| Superscript | `normal text ^superscripted text^` | normal text ^superscripted text^ | +| Subscript | `normal text %subscripted text%` | normal text %subscripted text% | +| Spoiler | `Psst! | | Darth Vader is Luke's father | | ` | Psst! | | Darth Vader is Luke's father | | +| Code | ``Use `**bold**` to make text bold!`` | Use `**bold**` to make text bold! | #### Multi-line inlines Most inline formatting can extend beyond just a single line and travel to other lines. However, it does have certain quirks, especially if you're unused to the Markdown syntax. ``` -**I am a very +**I am a very bold text** ``` +
Result @@ -36,7 +38,7 @@ bold text** However, if you try to insert a newline in the middle of it, it won't work. ``` -**I am not a very +**I am not a very bold text** ``` @@ -51,14 +53,16 @@ bold text**
-If you really need an empty line in the middle of your inline-formatted text, you must *escape* the line ending. In order to do so, Markdown provides us with the `\` (backslash) character. Backslash is a very special character and is used for *escaping* other special characters. *Escaping* forces the character immediately after the backslash to be ignored by the parser. +If you really need an empty line in the middle of your inline-formatted text, you must _escape_ the line ending. In order to do so, Markdown provides us with the `\` (backslash) character. Backslash is a very special character and is used for _escaping_ other special characters. _Escaping_ forces the character immediately after the backslash to be ignored by the parser. As such, we can write our previous example like so to preserve the empty line: + ``` **I am a very \ bold text** ``` +
Result @@ -71,7 +75,8 @@ bold text**
#### Combining inlines -Most inline operators may be combined with each other (with the exception of the ````code```` syntax). + +Most inline operators may be combined with each other (with the exception of the `code` syntax). ``` _I am an italic text **with some bold in it**._ @@ -86,11 +91,12 @@ _I am an italic text **with some bold in it**._
- ## Block formatting + Block formatting is the kind of formatting that cannot be written within a single line and typically requires to be written on its own line. Many block formatting styles extend past just one line. #### Blockquotes + Philomena's flavor of Markdown makes some changes to the blockquote syntax compared to regular CommonMark. The basic syntax is a > followed by a space. ``` @@ -107,7 +113,7 @@ Please note, that if > is not followed by a space, it will not become a blockquo >not a quote ``` ->not a quote +> not a quote --- @@ -117,7 +123,7 @@ Same goes for >>, even if followed by a space. >> not a quote ``` ->> not a quote +> > not a quote --- @@ -125,12 +131,12 @@ You may continue a quote by adding > followed by a space on a new line, even if ``` > quote text -> +> > continuation of quote ``` > quote text -> +> > continuation of quote --- @@ -144,10 +150,13 @@ To nest a quote, simply repeat > followed by a space as many times as you wish t ``` > quote text +> > > nested quote +> > > > > even deeper nested quote #### Headers + Markdown supports adding headers to your text. The syntax is # repeated up to 6 times. ``` @@ -160,22 +169,28 @@ Markdown supports adding headers to your text. The syntax is # repeated up to 6 ``` # Header 1 + ## Header 2 + ### Header 3 + #### Header 4 + ##### Header 5 + ###### Header 6 #### Code block + Another way to write code is by writing a code block. Code blocks, unlike inline code syntax, are styled similar to blockquotes and are more appropriate for sharing larger snippets of code. In fact, this very page has been using this very structure to show examples of code. -~~~ +```` ```

Hello World!

``` -~~~ +```` ```
@@ -191,11 +206,12 @@ code block ~~~ ``` -~~~ +``` code block -~~~ +``` ## Links + Links have the basic syntax of ``` @@ -224,23 +240,25 @@ On-site links may be written as either a relative or absolute path. If the on-si [Link to the first image](/images/0) ## On-site images + If you wish to link an on-site image, you should use the >>:id syntax. It respects filters currently in-use by the reader and spoilers content they do not wish to see. **You should always use this for on-site uploads!** (as this will let other users filter the image if they wish to, and it is against the rules to not show content with care) Here's a brief explanation of its usage. -Operator | Description of result ---- | --- -\>\>5 | Simple link to image -\>\>5s | Small (150x150) thumbnail of the image -\>\>5t | Regular (320x240) thumbnail of the image -\>\>5p | Preview (800x600) size of the image +| Operator | Description of result | +| -------- | ---------------------------------------- | +| \>\>5 | Simple link to image | +| \>\>5s | Small (150x150) thumbnail of the image | +| \>\>5t | Regular (320x240) thumbnail of the image | +| \>\>5p | Preview (800x600) size of the image | ->>5 ->>5s ->>5t ->>5p +> > 5 +> > 5s +> > 5t +> > 5p ## External images + Some images you may wish to link may not exist on the site. To link them Markdown provides us with a special syntax. All images embedded this way are proxied by our image proxy (Go-Camo). ``` @@ -251,13 +269,13 @@ Some images you may wish to link may not exist on the site. To link them Markdow You may control the size of your externally-linked image by specifying the alt text. Certain keywords are recognized as size modifiers. The modifiers are case-sensitive! -Modifier | Resulting size ---- | --- -tiny | 64x64 -small | 128x128 -medium | 256x256 -large | 512x512 -(anything else) | (actual size of the image) +| Modifier | Resulting size | +| --------------- | -------------------------- | +| tiny | 64x64 | +| small | 128x128 | +| medium | 256x256 | +| large | 512x512 | +| (anything else) | (actual size of the image) | ``` ![tiny](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) @@ -274,6 +292,7 @@ large | 512x512 ![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) #### Image links + To make an image link, simply combine the external image syntax with the link syntax. ``` @@ -283,7 +302,9 @@ To make an image link, simply combine the external image syntax with the link sy [![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg)](https://github.com/philomena-dev/philomena) ## Lists + #### Unordered list + Unordered lists can be written fairly intuitively, by putting one of the special characters in front of each line that should be a part of the list. ``` @@ -294,9 +315,10 @@ Shopping list: ``` Shopping list: -* Milk -* Eggs -* Soda + +- Milk +- Eggs +- Soda You may use any of the following characters at the beginning of the line to make an unordered list: @@ -315,12 +337,13 @@ Lists may be nested and have sublists within them. Simply prefix your sublist it * Sublist item two ``` -* Item one -* Item two - * Sublist item one - * Sublist item two +- Item one +- Item two + - Sublist item one + - Sublist item two #### Ordered list + To write an ordered list, simply put a number at the beginning of the line followed by a dot or closing bracket. It doesn't actually matter which order your numbers are written in, the list will always maintain its incremental order. Note the 4 in the example, it isn't a typo. ``` @@ -331,7 +354,7 @@ To write an ordered list, simply put a number at the beginning of the line follo 1. Item one 2. Item two -4. Item three +3. Item three **Ordered lists cannot be sublists to other ordered lists.** They can, however, be sublists to unordered lists. Unordered lists, in turn, may be sublists in ordered lists. @@ -342,12 +365,13 @@ To write an ordered list, simply put a number at the beginning of the line follo * Sublist item two ``` -1) Item one -2) Item two - * Sublist item one - * Sublist item two +1. Item one +2. Item two + - Sublist item one + - Sublist item two ## Tables + Philomena's Markdown implementation supports GitHub-style tables. This isn't a part of the core Markdown specification, but we support them. The colons are used to specify the alignment of columns. ``` @@ -357,10 +381,10 @@ Philomena's Markdown implementation supports GitHub-style tables. This isn't a p | *formatting* | **works** | __here__ | ``` -| Left | Center | Right | -| ------------ |:--------------:| -------------:| +| Left | Center | Right | +| ------------ | :------------: | ------------: | | left-aligned | center-aligned | right-aligned | -| *formatting* | **works** | __here__ | +| _formatting_ | **works** | **here** | In tables, the pipes (|) at the edges of the table are optional. To separate table head from body, you need to put in at least three - symbols. As such, example above could have also been written like so: @@ -371,10 +395,10 @@ left-aligned | center-aligned | right-aligned *formatting* | **works** | __here__ ``` -Left | Center | Right ---- | :---: | ---: -left-aligned | center-aligned | right-aligned -*formatting* | **works** | __here__ +| Left | Center | Right | +| ------------ | :------------: | ------------: | +| left-aligned | center-aligned | right-aligned | +| _formatting_ | **works** | **here** | # Escaping the syntax. diff --git a/priv/repo/seeds/seeds_development.json b/priv/repo/seeds/seeds_development.json index 2c63c0851..0967ef424 100644 --- a/priv/repo/seeds/seeds_development.json +++ b/priv/repo/seeds/seeds_development.json @@ -1,2 +1 @@ -{ -} +{} From 3dfdef984d8b5f990783a1e8eac0bd8e4fe3185b Mon Sep 17 00:00:00 2001 From: MareStare Date: Thu, 27 Mar 2025 03:21:28 +0000 Subject: [PATCH 30/61] Remove `seeds_development.json` --- priv/repo/seeds/seeds_development.json | 1 - priv/repo/seeds_development.exs | 5 ----- 2 files changed, 6 deletions(-) delete mode 100644 priv/repo/seeds/seeds_development.json diff --git a/priv/repo/seeds/seeds_development.json b/priv/repo/seeds/seeds_development.json deleted file mode 100644 index 0967ef424..000000000 --- a/priv/repo/seeds/seeds_development.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 5b09eb5b9..1bcf6904e 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -23,11 +23,6 @@ defmodule Philomena.DevSeeds do def seed() do {:ok, _} = Application.ensure_all_started(:plug) - # resources = - # "priv/repo/seeds/seeds_development.json" - # |> File.read!() - # |> Jason.decode!() - communications = "priv/repo/seeds/dev/communications.json" |> File.read!() From 4ae1af4d8bcb64136092103f5ec7a6681dd0178c Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Mon, 30 Sep 2024 23:25:22 +0200 Subject: [PATCH 31/61] fix dev seeds --- priv/repo/seeds_development.exs | 2 -- 1 file changed, 2 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 1bcf6904e..71f5eebe9 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -172,8 +172,6 @@ defmodule Philomena.DevSeeds do [ fingerprint: "d015c342859dde3", ip: default_ip(), - user_agent: "Hopefully not IE", - referrer: "localhost", user_id: id, user: user ] From 146af879cfce7cbe1e115a73d9e7f25d5e847a64 Mon Sep 17 00:00:00 2001 From: MareStare Date: Thu, 27 Mar 2025 23:47:28 +0000 Subject: [PATCH 32/61] Fix script name in comment --- priv/repo/seeds_development.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 71f5eebe9..b0e84b8ec 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -1,6 +1,6 @@ # Script for populating the database. You can run it as: # -# mix run priv/repo/seeds.exs +# mix run priv/repo/seeds_development.exs # # Inside the script, you can read and write to any of your # repositories directly: From e849e94def443074c3ca3aabcd4d3af29e6e1b95 Mon Sep 17 00:00:00 2001 From: MareStare Date: Thu, 27 Mar 2025 23:48:14 +0000 Subject: [PATCH 33/61] Remove clashing alias --- priv/repo/seeds.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 58a20bd83..a37ca5cfd 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -13,7 +13,6 @@ alias Philomena.{ Repo, Comments.Comment, - Filters.Filter, Forums.Forum, Galleries.Gallery, Posts.Post, From 3fcb1f5ad805f3d539d721b9518563e164dc720d Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 29 Mar 2025 21:47:15 +0000 Subject: [PATCH 34/61] Parallel image upload in dev seeds --- lib/philomena_media/objects.ex | 15 ++++-- priv/repo/seeds_development.exs | 82 ++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/lib/philomena_media/objects.ex b/lib/philomena_media/objects.ex index 27a927322..2aabfc988 100644 --- a/lib/philomena_media/objects.ex +++ b/lib/philomena_media/objects.ex @@ -61,11 +61,20 @@ defmodule PhilomenaMedia.Objects do contents = backends() |> Enum.find_value(fn opts -> - ExAws.S3.get_object(opts[:bucket], key) + bucket = opts[:bucket] + + ExAws.S3.get_object(bucket, key) |> ExAws.request(opts[:config_overrides]) |> case do - {:ok, result} -> result - _ -> nil + {:ok, result} -> + result + + {:err, err} -> + Logger.warning( + "Failed to download #{key} from #{bucket}: #{inspect(err, pretty: true)}" + ) + + nil end end) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index b0e84b8ec..8e453c874 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -19,6 +19,7 @@ defmodule Philomena.DevSeeds do alias Philomena.Posts alias Philomena.Tags import Ecto.Query + require Logger def seed() do {:ok, _} = Application.ensure_all_started(:plug) @@ -28,11 +29,6 @@ defmodule Philomena.DevSeeds do |> File.read!() |> Jason.decode!() - images = - "priv/repo/seeds/dev/images.json" - |> File.read!() - |> Jason.decode!() - # pages = # "priv/repo/seeds/dev/pages.json" # |> File.read!() @@ -63,39 +59,7 @@ defmodule Philomena.DevSeeds do IO.puts("---- Generating images") - for image_def <- images do - file = Briefly.create!() - now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) - - IO.puts("Fetching #{image_def["url"]} ...") - {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) - - File.write!(file, body) - - upload = %Plug.Upload{ - path: file, - content_type: "application/octet-stream", - filename: "fixtures-#{now}" - } - - IO.puts("Inserting ...") - - Images.create_image( - pleb_attrs, - Map.merge(image_def, %{"image" => upload}) - ) - |> case do - {:ok, %{image: image}} -> - Images.approve_image(image) - Images.reindex_image(image) - Tags.reindex_tags(image.added_tags) - - IO.puts("Created image ##{image.id}") - - {:error, :image, changeset, _so_far} -> - IO.inspect(changeset.errors) - end - end + generate_images(pleb_attrs) IO.puts("---- Generating comments for image #1") @@ -157,6 +121,48 @@ defmodule Philomena.DevSeeds do Logger.configure(level: :debug) end + defp generate_images(pleb_attrs) do + images = + "priv/repo/seeds/dev/images.json" + |> File.read!() + |> Jason.decode!() + + ingest_image = fn image_def -> + file = Briefly.create!() + now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) + + IO.puts("Fetching #{image_def["url"]} ...") + {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) + + File.write!(file, body) + + upload = %Plug.Upload{ + path: file, + content_type: "application/octet-stream", + filename: "fixtures-#{now}" + } + + IO.puts("Inserting ...") + + Images.create_image(pleb_attrs, Map.merge(image_def, %{"image" => upload})) + |> case do + {:ok, %{image: image}} -> + Images.approve_image(image) + Images.reindex_image(image) + Tags.reindex_tags(image.added_tags) + + IO.puts("Created image ##{image.id}") + + {:error, :image, changeset, _so_far} -> + IO.inspect(changeset.errors) + end + end + + images + |> Task.async_stream(ingest_image, max_concurrency: 20, ordered: false) + |> Stream.run() + end + defp default_ip() do {:ok, ip} = EctoNetwork.INET.cast({203, 0, 113, 0}) ip From bcf9b227d55dd32fdce74e16d6035327bb1092ae Mon Sep 17 00:00:00 2001 From: MareStare Date: Sun, 30 Mar 2025 20:35:04 +0000 Subject: [PATCH 35/61] Create OpenSearch indices in parallel (doesn't give a significant boost, but maybe a couple of seconds) --- priv/repo/seeds.exs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index a37ca5cfd..6f89055c0 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -31,12 +31,18 @@ alias Philomena.Tags alias Philomena.Filters import Ecto.Query -IO.puts("---- Creating search indices") - -for model <- [Image, Comment, Gallery, Tag, Post, Report, Filter] do - Search.delete_index!(model) - Search.create_index!(model) -end +IO.puts("---- Creating OpeanSearch indices") + +[Image, Comment, Gallery, Tag, Post, Report, Filter] +|> Task.async_stream( + fn model -> + Search.delete_index!(model) + Search.create_index!(model) + IO.puts("OpenSearch index created: #{inspect(model)}") + end, + timeout: 15_000 +) +|> Stream.run() resources = "priv/repo/seeds/seeds.json" From dbf759c7736d905d0b3cd0a3b5286914985dc308 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sun, 30 Mar 2025 20:35:37 +0000 Subject: [PATCH 36/61] Give away the temp file ownership to the image upload process, increase upload concurrency and better logs --- priv/repo/seeds_development.exs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 8e453c874..f58ec2466 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -131,7 +131,7 @@ defmodule Philomena.DevSeeds do file = Briefly.create!() now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) - IO.puts("Fetching #{image_def["url"]} ...") + IO.puts("[Images] Fetching #{image_def["url"]} ...") {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) File.write!(file, body) @@ -142,16 +142,19 @@ defmodule Philomena.DevSeeds do filename: "fixtures-#{now}" } - IO.puts("Inserting ...") + IO.puts("[Images] Creating image ...") Images.create_image(pleb_attrs, Map.merge(image_def, %{"image" => upload})) |> case do - {:ok, %{image: image}} -> + {:ok, %{image: image, upload_pid: upload_pid}} -> + # Delete the temp file only after the async image upload finishes + Briefly.give_away(file, upload_pid) + Images.approve_image(image) Images.reindex_image(image) Tags.reindex_tags(image.added_tags) - IO.puts("Created image ##{image.id}") + IO.puts("[Images] Created image ##{image.id}") {:error, :image, changeset, _so_far} -> IO.inspect(changeset.errors) @@ -159,7 +162,7 @@ defmodule Philomena.DevSeeds do end images - |> Task.async_stream(ingest_image, max_concurrency: 20, ordered: false) + |> Task.async_stream(ingest_image, max_concurrency: 100, ordered: false) |> Stream.run() end From 29d33de8273758d5a5d8afe3dbaa28a3ab4466c3 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 31 Mar 2025 20:49:27 +0000 Subject: [PATCH 37/61] Add `DROP_DB` config env var --- docker-compose.yml | 3 +++ docker/app/run-development | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index cef044380..2a386b5d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,9 @@ services: context: . dockerfile: ./docker/app/Dockerfile environment: + # Set this env var to `1` if you want to drop your current postgres DB and + # reseed the DB from scratch again. + - DROP_DB - MIX_ENV=dev - PGPASSWORD=postgres - ANONYMOUS_NAME_SALT=2fmJRo0OgMFe65kyAJBxPT0QtkVes/jnKDdtP21fexsRqiw8TlSY7yO+uFyMZycp diff --git a/docker/app/run-development b/docker/app/run-development index c0783a5ca..740e5eaec 100755 --- a/docker/app/run-development +++ b/docker/app/run-development @@ -52,6 +52,11 @@ until step wget --no-check-certificate -qO - http://opensearch:9200; do sleep 2 done +# If `DROP_DB` env var is set, drop the database +if [[ "${DROP_DB:-}" == "1" ]]; then + step dropdb -h postgres -U postgres philomena_dev +fi + # Try to create the database if it doesn't exist yet if step createdb -h postgres -U postgres philomena_dev; then step mix ecto.setup_dev From 0267e503795723424b57949ae66340f9bf73b8e8 Mon Sep 17 00:00:00 2001 From: MareStare Date: Tue, 1 Apr 2025 01:07:31 +0000 Subject: [PATCH 38/61] Ignore DB logs, but keep other logs --- config/dev.exs | 3 --- priv/repo/seeds_development.exs | 12 ++++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 674709ff0..89493b7df 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -78,9 +78,6 @@ config :philomena, csp_relaxed: true # Enable Vite HMR config :philomena, vite_reload: true -# Do not include metadata nor timestamps in development logs -config :logger, :console, format: "[$level] $message\n" - # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. config :phoenix, :stacktrace_depth, 20 diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index f58ec2466..5b9b28230 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -22,6 +22,16 @@ defmodule Philomena.DevSeeds do require Logger def seed() do + exclude_log_event? = fn event -> + # Skip DB logs, they are too verbose + Map.get(event.meta, :application) == :ecto_sql + end + + :logger.add_primary_filter( + :sql_logs, + {fn event, _ -> if(exclude_log_event?.(event), do: :stop, else: :ignore) end, []} + ) + {:ok, _} = Application.ensure_all_started(:plug) communications = @@ -39,8 +49,6 @@ defmodule Philomena.DevSeeds do |> File.read!() |> Jason.decode!() - Logger.configure(level: :warning) - IO.puts("---- Generating users") for user_def <- users do From 4e6c9ac68aa48036c93f47a0d1f60141b19403f7 Mon Sep 17 00:00:00 2001 From: MareStare Date: Tue, 1 Apr 2025 19:25:34 +0000 Subject: [PATCH 39/61] Use logger instead of IO.puts in seeds_development --- priv/repo/seeds_development.exs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 5b9b28230..f9618569e 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -49,7 +49,7 @@ defmodule Philomena.DevSeeds do |> File.read!() |> Jason.decode!() - IO.puts("---- Generating users") + Logger.info("---- Generating users") for user_def <- users do {:ok, user} = Users.register_user(user_def) @@ -65,11 +65,11 @@ defmodule Philomena.DevSeeds do pleb = Repo.get_by!(User, name: "Pleb") pleb_attrs = request_attrs(pleb) - IO.puts("---- Generating images") + Logger.info("---- Generating images") generate_images(pleb_attrs) - IO.puts("---- Generating comments for image #1") + Logger.info("---- Generating comments for image #1") for comment_body <- communications["demos"] do image = Images.get_image!(1) @@ -92,7 +92,7 @@ defmodule Philomena.DevSeeds do all_imgs = Image |> where([i], i.id > 1) |> Repo.all() - IO.puts("---- Generating random comments for images other than 1") + Logger.info("---- Generating random comments for images other than 1") for _ <- 1..1000 do image = Enum.random(all_imgs) @@ -114,7 +114,7 @@ defmodule Philomena.DevSeeds do end end - IO.puts("---- Generating forum posts") + Logger.info("---- Generating forum posts") for _ <- 1..500 do random_topic_no_replies(communications, users) @@ -124,7 +124,7 @@ defmodule Philomena.DevSeeds do random_topic(communications, users) end - IO.puts("---- Done.") + Logger.info("---- Done.") Logger.configure(level: :debug) end @@ -139,7 +139,7 @@ defmodule Philomena.DevSeeds do file = Briefly.create!() now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) - IO.puts("[Images] Fetching #{image_def["url"]} ...") + Logger.info("[Images] Fetching #{image_def["url"]} ...") {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) File.write!(file, body) @@ -150,7 +150,7 @@ defmodule Philomena.DevSeeds do filename: "fixtures-#{now}" } - IO.puts("[Images] Creating image ...") + Logger.info("[Images] Creating image ...") Images.create_image(pleb_attrs, Map.merge(image_def, %{"image" => upload})) |> case do @@ -162,7 +162,7 @@ defmodule Philomena.DevSeeds do Images.reindex_image(image) Tags.reindex_tags(image.added_tags) - IO.puts("[Images] Created image ##{image.id}") + Logger.info("[Images] Created image ##{image.id}") {:error, :image, changeset, _so_far} -> IO.inspect(changeset.errors) @@ -228,7 +228,7 @@ defmodule Philomena.DevSeeds do ) |> case do {:ok, %{topic: topic}} -> - IO.puts(" -> created topic ##{topic.id}") + Logger.info(" -> created topic ##{topic.id}") count = :rand.uniform(250) + 5 for _ <- 1..count do @@ -249,7 +249,7 @@ defmodule Philomena.DevSeeds do end end - IO.puts(" -> created #{count} replies for topic ##{topic.id}") + Logger.info(" -> created #{count} replies for topic ##{topic.id}") {:error, :topic, changeset, _so_far} -> IO.inspect(changeset.errors) @@ -274,7 +274,7 @@ defmodule Philomena.DevSeeds do ) |> case do {:ok, %{topic: topic}} -> - IO.puts(" -> created topic ##{topic.id}") + Logger.info(" -> created topic ##{topic.id}") {:error, :topic, changeset, _so_far} -> IO.inspect(changeset.errors) From ed3096c527d487ef9de4fc2601859be41075c970 Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 2 Apr 2025 00:41:21 +0000 Subject: [PATCH 40/61] Improve logging and error handling on objects.ex --- lib/philomena_media/objects.ex | 117 ++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/lib/philomena_media/objects.ex b/lib/philomena_media/objects.ex index 2aabfc988..171c6b764 100644 --- a/lib/philomena_media/objects.ex +++ b/lib/philomena_media/objects.ex @@ -7,7 +7,7 @@ defmodule PhilomenaMedia.Objects do recommended to maintain a secondary storage provider, such as in the [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/). - Functions in this module replicate operations on both the primary and secondary storage + Functions in this module replicate_request operations on both the primary and secondary storage providers. Alternatively, a mode with only a primary storage provider is supported. This module assumes storage endpoints are S3-compatible and can be communicated with via the @@ -42,9 +42,11 @@ defmodule PhilomenaMedia.Objects do """ alias PhilomenaMedia.Mime + alias ExAws.S3 require Logger @type key :: String.t() + @typep operation_fn :: (... -> ExAws.Operation.S3.t()) @doc """ Fetch a key from the storage backend and write it into the destination path. @@ -61,19 +63,11 @@ defmodule PhilomenaMedia.Objects do contents = backends() |> Enum.find_value(fn opts -> - bucket = opts[:bucket] - - ExAws.S3.get_object(bucket, key) - |> ExAws.request(opts[:config_overrides]) - |> case do - {:ok, result} -> - result - - {:err, err} -> - Logger.warning( - "Failed to download #{key} from #{bucket}: #{inspect(err, pretty: true)}" - ) + case request(&S3.get_object/2, [opts[:bucket], key], opts) do + {:ok, contents} -> + contents + {:error, _} -> nil end end) @@ -96,10 +90,10 @@ defmodule PhilomenaMedia.Objects do {_, mime} = Mime.file(file_path) contents = File.read!(file_path) - run_all(fn opts -> - ExAws.S3.put_object(opts[:bucket], key, contents, content_type: mime) - |> ExAws.request!(opts[:config_overrides]) - end) + replicate_request( + &S3.put_object/4, + &[&1[:bucket], key, {:log_byte_size, contents}, [content_type: mime]] + ) end @doc """ @@ -136,11 +130,7 @@ defmodule PhilomenaMedia.Objects do def copy(source_key, dest_key) do # Potential workaround for inconsistent PutObjectCopy on R2 # - # run_all(fn opts-> - # ExAws.S3.put_object_copy(opts[:bucket], dest_key, opts[:bucket], source_key) - # |> ExAws.request!(opts[:config_overrides]) - # end) - + # replicate_request(ExAws.S3.put_object_copy, &[&1[:bucket], dest_key, &1[:bucket], source_key]) try do file_path = Briefly.create!() download_file(source_key, file_path) @@ -163,10 +153,7 @@ defmodule PhilomenaMedia.Objects do """ @spec delete(key()) :: :ok def delete(key) do - run_all(fn opts -> - ExAws.S3.delete_object(opts[:bucket], key) - |> ExAws.request!(opts[:config_overrides]) - end) + replicate_request(&S3.delete_object/2, &[&1[:bucket], key]) end @doc """ @@ -185,34 +172,70 @@ defmodule PhilomenaMedia.Objects do """ @spec delete_multiple([key()]) :: :ok def delete_multiple(keys) do - run_all(fn opts -> - ExAws.S3.delete_multiple_objects(opts[:bucket], keys) - |> ExAws.request!(opts[:config_overrides]) - end) + replicate_request(&S3.delete_multiple_objects/2, &[&1[:bucket], keys]) end - defp run_all(wrapped) do - fun = fn opts -> - try do - wrapped.(opts) - :ok - catch - _kind, _value -> :error - end - end + @spec request(operation_fn(), [term()], keyword()) :: term() + defp request(operation, args, opts) do + {:name, operation_name} = Function.info(operation, :name) + + Logger.debug(fn -> + args_debug = + args + |> Enum.map(fn + {:log_byte_size, arg} -> "#{(byte_size(arg) / 1_000_000) |> Float.round(2)} MB" + arg -> inspect(arg) + end) + |> Enum.join(", ") + + "S3.#{operation_name}(#{args_debug})" + end) + + args = + args + |> Enum.map(fn + {:log_byte_size, arg} -> arg + arg -> arg + end) - backends() - |> Task.async_stream(fun, timeout: :infinity) - |> Enum.any?(fn {_, v} -> v == :error end) + operation + |> apply(args) + |> ExAws.request(opts[:config_overrides]) |> case do - true -> - Logger.warning("Failed to operate on all backends") + {:ok, output} -> + {:ok, output} - _ -> - :ok + {:error, {:http_error, status_code, %{body: body}} = err} -> + Logger.warning("S3.#{operation_name} failed (#{inspect(status_code)})\nError: #{body}") + {:error, err} + + {:error, err} -> + Logger.warning("S3.#{operation_name} failed\nError: #{inspect(err, pretty: true)}") + {:error, err} end + end - :ok + @spec replicate_request(operation_fn(), (keyword() -> [term()])) :: :ok + defp replicate_request(operation, args) do + operation_name = Function.info(operation, :name) + backends = backends() + + total_err = + backends + |> Task.async_stream(&request(operation, args.(&1), &1), timeout: :infinity) + |> Enum.filter(&(not match?({:ok, {:ok, _}}, &1))) + |> Enum.count() + + cond do + total_err > 0 and total_err == length(backends) -> + Logger.error("S3.#{operation_name} failed for all backends") + + total_err > 0 -> + Logger.warning("S3.#{operation_name} failed for #{total_err} backends") + + true -> + :ok + end end defp backends do From eac7a362bfb8e7a006c35a10263dd1a4ba5a325d Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 2 Apr 2025 00:41:49 +0000 Subject: [PATCH 41/61] Hide SQL and Exq logs by default --- config/dev.exs | 4 ++++ lib/philomena/application.ex | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/config/dev.exs b/config/dev.exs index 89493b7df..a4617d27d 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -84,3 +84,7 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime + +config :logger, :console, format: "$time $metadata[$level] $message\n" +# Uncomment to show additional metadata i the logs to see where they come from +# metadata: [:application, :mfa] diff --git a/lib/philomena/application.ex b/lib/philomena/application.ex index 3ecfcc030..e34f8ac2e 100644 --- a/lib/philomena/application.ex +++ b/lib/philomena/application.ex @@ -6,6 +6,16 @@ defmodule Philomena.Application do use Application def start(_type, _args) do + exclude_log_event? = fn event -> + # Skip DB logs, they are too verbose + Map.get(event.meta, :application) in [:ecto_sql, :exq] + end + + :logger.add_primary_filter( + :sql_logs, + {fn event, _ -> if(exclude_log_event?.(event), do: :stop, else: :ignore) end, []} + ) + # List all child processes to be supervised children = [ # Start the Ecto repository From 4a0d36255a9f874ec5dfcba103c4cd59461d0763 Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 2 Apr 2025 00:42:02 +0000 Subject: [PATCH 42/61] Parallelize the rest of the dev seeding --- priv/repo/seeds_development.exs | 193 ++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 87 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index f9618569e..b09b8a17a 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -22,16 +22,6 @@ defmodule Philomena.DevSeeds do require Logger def seed() do - exclude_log_event? = fn event -> - # Skip DB logs, they are too verbose - Map.get(event.meta, :application) == :ecto_sql - end - - :logger.add_primary_filter( - :sql_logs, - {fn event, _ -> if(exclude_log_event?.(event), do: :stop, else: :ignore) end, []} - ) - {:ok, _} = Application.ensure_all_started(:plug) communications = @@ -39,27 +29,15 @@ defmodule Philomena.DevSeeds do |> File.read!() |> Jason.decode!() + # TODO: add pages to the seeds too # pages = # "priv/repo/seeds/dev/pages.json" # |> File.read!() # |> Jason.decode!() - users = - "priv/repo/seeds/dev/users.json" - |> File.read!() - |> Jason.decode!() - Logger.info("---- Generating users") - for user_def <- users do - {:ok, user} = Users.register_user(user_def) - - user - |> Repo.preload([:roles]) - |> User.confirm_changeset() - |> User.update_changeset(%{role: user_def["role"]}, []) - |> Repo.update!() - end + generate_users() users = Repo.all(User) pleb = Repo.get_by!(User, name: "Pleb") @@ -69,64 +47,51 @@ defmodule Philomena.DevSeeds do generate_images(pleb_attrs) - Logger.info("---- Generating comments for image #1") + last_image_id = Image |> Repo.aggregate(:max, :id) - for comment_body <- communications["demos"] do - image = Images.get_image!(1) + Logger.info("---- Generating comments for image #{last_image_id}") - Comments.create_comment( - image, - pleb_attrs, - %{"body" => comment_body} - ) - |> case do - {:ok, %{comment: comment}} -> - Comments.approve_comment(comment, pleb) - Comments.reindex_comment(comment) - Images.reindex_image(image) + generate_predefined_image_comments(pleb, pleb_attrs, communications, last_image_id) - {:error, :comment, changeset, _so_far} -> - IO.inspect(changeset.errors) - end - end - - all_imgs = Image |> where([i], i.id > 1) |> Repo.all() + other_images = Image |> where([i], i.id != ^last_image_id) |> Repo.all() Logger.info("---- Generating random comments for images other than 1") - for _ <- 1..1000 do - image = Enum.random(all_imgs) - user = random_user(users) - - Comments.create_comment( - image, - request_attrs(user), - %{"body" => random_body(communications)} - ) - |> case do - {:ok, %{comment: comment}} -> - Comments.approve_comment(comment, user) - Comments.reindex_comment(comment) - Images.reindex_image(image) - - {:error, :comment, changeset, _so_far} -> - IO.inspect(changeset.errors) - end - end + generate_random_image_comments(other_images, users, communications) Logger.info("---- Generating forum posts") - for _ <- 1..500 do - random_topic_no_replies(communications, users) - end + 1..500 + |> Task.async_stream(fn _ -> generate_topic_without_replies(communications, users) end) + |> Stream.run() - for _ <- 1..20 do - random_topic(communications, users) - end + 1..20 + |> Task.async_stream(fn _ -> generate_topic_with_replies(communications, users) end) + |> Stream.run() Logger.info("---- Done.") + end + + defp generate_users() do + users = + "priv/repo/seeds/dev/users.json" + |> File.read!() + |> Jason.decode!() - Logger.configure(level: :debug) + users + |> Task.async_stream( + fn user_def -> + {:ok, user} = Users.register_user(user_def) + + user + |> Repo.preload([:roles]) + |> User.confirm_changeset() + |> User.update_changeset(%{role: user_def["role"]}, []) + |> Repo.update!() + end, + timeout: :infinity + ) + |> Stream.run() end defp generate_images(pleb_attrs) do @@ -135,13 +100,14 @@ defmodule Philomena.DevSeeds do |> File.read!() |> Jason.decode!() - ingest_image = fn image_def -> + generate_image = fn image_def -> file = Briefly.create!() now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) - Logger.info("[Images] Fetching #{image_def["url"]} ...") {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) + Logger.info("[Images] Fetched #{image_def["url"]}") + File.write!(file, body) upload = %Plug.Upload{ @@ -165,12 +131,63 @@ defmodule Philomena.DevSeeds do Logger.info("[Images] Created image ##{image.id}") {:error, :image, changeset, _so_far} -> - IO.inspect(changeset.errors) + Logger.error(inspect(changeset.errors)) end end images - |> Task.async_stream(ingest_image, max_concurrency: 100, ordered: false) + |> Task.async_stream(generate_image) + |> Stream.run() + end + + defp generate_predefined_image_comments(pleb, pleb_attrs, communications, image_id) do + generate_comment = fn comment_body -> + image = Images.get_image!(image_id) + + Comments.create_comment( + image, + pleb_attrs, + %{"body" => comment_body} + ) + |> case do + {:ok, %{comment: comment}} -> + Comments.approve_comment(comment, pleb) + Comments.reindex_comment(comment) + Images.reindex_image(image) + + {:error, :comment, changeset, _so_far} -> + Logger.error(inspect(changeset.errors)) + end + end + + communications["demos"] + |> Task.async_stream(generate_comment) + |> Stream.run() + end + + defp generate_random_image_comments(images, users, communications) do + generate_comment = fn _ -> + image = Enum.random(images) + user = random_user(users) + + Comments.create_comment( + image, + request_attrs(user), + %{"body" => random_body(communications)} + ) + |> case do + {:ok, %{comment: comment}} -> + Comments.approve_comment(comment, user) + Comments.reindex_comment(comment) + Images.reindex_image(image) + + {:error, :comment, changeset, _so_far} -> + Logger.error(inspect(changeset.errors)) + end + end + + 1..1000 + |> Task.async_stream(generate_comment) |> Stream.run() end @@ -210,7 +227,7 @@ defmodule Philomena.DevSeeds do Enum.random(titles["third"]) end - defp random_topic(comm, users) do + defp generate_topic_with_replies(communications, users) do forum = Repo.get_by!(Forum, short_name: random_forum()) op = random_user(users) @@ -218,26 +235,25 @@ defmodule Philomena.DevSeeds do forum, request_attrs(op), %{ - "title" => random_title(comm), + "title" => random_title(communications), "posts" => %{ "0" => %{ - "body" => random_body(comm) + "body" => random_body(communications) } } } ) |> case do {:ok, %{topic: topic}} -> - Logger.info(" -> created topic ##{topic.id}") count = :rand.uniform(250) + 5 - for _ <- 1..count do + generate_post = fn _ -> user = random_user(users) Posts.create_post( topic, request_attrs(user), - %{"body" => random_body(comm)} + %{"body" => random_body(communications)} ) |> case do {:ok, %{post: post}} -> @@ -245,18 +261,21 @@ defmodule Philomena.DevSeeds do Posts.reindex_post(post) {:error, :post, changeset, _so_far} -> - IO.inspect(changeset.errors) + Logger.error(inspect(changeset.errors)) end end - Logger.info(" -> created #{count} replies for topic ##{topic.id}") + 1..count + |> Task.async_stream(generate_post, timeout: :infinity) + + Logger.info("[Topics] Created topic ##{topic.id} with #{count} replies") {:error, :topic, changeset, _so_far} -> - IO.inspect(changeset.errors) + Logger.error(inspect(changeset.errors)) end end - defp random_topic_no_replies(comm, users) do + defp generate_topic_without_replies(communications, users) do forum = Repo.get_by!(Forum, short_name: random_forum()) op = random_user(users) @@ -264,20 +283,20 @@ defmodule Philomena.DevSeeds do forum, request_attrs(op), %{ - "title" => random_title(comm), + "title" => random_title(communications), "posts" => %{ "0" => %{ - "body" => random_body(comm) + "body" => random_body(communications) } } } ) |> case do {:ok, %{topic: topic}} -> - Logger.info(" -> created topic ##{topic.id}") + Logger.info("[Topics] Created topic ##{topic.id}") {:error, :topic, changeset, _so_far} -> - IO.inspect(changeset.errors) + Logger.error(inspect(changeset.errors)) end end end From 9da840638b8e4536fd15549476e935d02b01bcf9 Mon Sep 17 00:00:00 2001 From: MareStare Date: Thu, 3 Apr 2025 01:52:06 +0000 Subject: [PATCH 43/61] Use logger in `seeds.exs` --- priv/repo/seeds.exs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 6f89055c0..1799d4178 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -29,16 +29,17 @@ alias PhilomenaQuery.Search alias Philomena.Users alias Philomena.Tags alias Philomena.Filters +require Logger import Ecto.Query -IO.puts("---- Creating OpeanSearch indices") +Logger.info("---- Creating OpenSearch indices") [Image, Comment, Gallery, Tag, Post, Report, Filter] |> Task.async_stream( fn model -> Search.delete_index!(model) Search.create_index!(model) - IO.puts("OpenSearch index created: #{inspect(model)}") + Logger.info("OpenSearch index created: #{inspect(model)}") end, timeout: 15_000 ) @@ -49,7 +50,7 @@ resources = |> File.read!() |> Jason.decode!() -IO.puts("---- Generating rating tags") +Logger.info("---- Generating rating tags") for tag_name <- resources["rating_tags"] do %Tag{category: "rating"} @@ -57,7 +58,7 @@ for tag_name <- resources["rating_tags"] do |> Repo.insert(on_conflict: :nothing) end -IO.puts("---- Generating system filters") +Logger.info("---- Generating system filters") for filter_def <- resources["system_filters"] do spoilered_tag_list = Enum.join(filter_def["spoilered"], ",") @@ -80,7 +81,7 @@ for filter_def <- resources["system_filters"] do end end -IO.puts("---- Generating forums") +Logger.info("---- Generating forums") for forum_def <- resources["forums"] do %Forum{} @@ -88,7 +89,7 @@ for forum_def <- resources["forums"] do |> Repo.insert(on_conflict: :nothing) end -IO.puts("---- Generating users") +Logger.info("---- Generating users") for user_def <- resources["users"] do {:ok, user} = Users.register_user(user_def) @@ -100,7 +101,7 @@ for user_def <- resources["users"] do |> Repo.update!() end -IO.puts("---- Generating roles") +Logger.info("---- Generating roles") for role_def <- resources["roles"] do %Role{name: role_def["name"], resource_type: role_def["resource_type"]} @@ -108,7 +109,7 @@ for role_def <- resources["roles"] do |> Repo.insert(on_conflict: :nothing) end -IO.puts("---- Generating static pages") +Logger.info("---- Generating static pages") for page_def <- resources["pages"] do %StaticPage{title: page_def["title"], slug: page_def["slug"], body: page_def["body"]} @@ -116,7 +117,7 @@ for page_def <- resources["pages"] do |> Repo.insert(on_conflict: :nothing) end -IO.puts("---- Indexing content") +Logger.info("---- Indexing content") Search.reindex(Tag |> preload(^Tags.indexing_preloads()), Tag) -IO.puts("---- Done.") +Logger.info("---- Done.") From 8852e600ccd6f6a4b956166387f6a7a305678b23 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 14:27:03 +0000 Subject: [PATCH 44/61] Preload all forums for seeding --- priv/repo/seeds_development.exs | 40 ++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index b09b8a17a..d9a08b0e9 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -61,12 +61,20 @@ defmodule Philomena.DevSeeds do Logger.info("---- Generating forum posts") + forums = Repo.all(Forum) + + topic_params = %{ + communications: communications, + users: users, + forums: forums + } + 1..500 - |> Task.async_stream(fn _ -> generate_topic_without_replies(communications, users) end) + |> Task.async_stream(fn _ -> generate_topic_without_replies(topic_params) end) |> Stream.run() 1..20 - |> Task.async_stream(fn _ -> generate_topic_with_replies(communications, users) end) + |> Task.async_stream(fn _ -> generate_topic_with_replies(topic_params) end) |> Stream.run() Logger.info("---- Done.") @@ -196,10 +204,6 @@ defmodule Philomena.DevSeeds do ip end - defp available_forums(), do: ["dis", "art", "rp", "meta", "shows"] - - defp random_forum(), do: Enum.random(available_forums()) - defp random_user(users), do: Enum.random(users) defp request_attrs(%{id: id} = user) do @@ -227,18 +231,18 @@ defmodule Philomena.DevSeeds do Enum.random(titles["third"]) end - defp generate_topic_with_replies(communications, users) do - forum = Repo.get_by!(Forum, short_name: random_forum()) - op = random_user(users) + defp generate_topic_with_replies(params) do + forum = Enum.random(params.forums) + op = random_user(params.users) Topics.create_topic( forum, request_attrs(op), %{ - "title" => random_title(communications), + "title" => random_title(params.communications), "posts" => %{ "0" => %{ - "body" => random_body(communications) + "body" => random_body(params.communications) } } } @@ -248,12 +252,12 @@ defmodule Philomena.DevSeeds do count = :rand.uniform(250) + 5 generate_post = fn _ -> - user = random_user(users) + user = random_user(params.users) Posts.create_post( topic, request_attrs(user), - %{"body" => random_body(communications)} + %{"body" => random_body(params.communications)} ) |> case do {:ok, %{post: post}} -> @@ -275,18 +279,18 @@ defmodule Philomena.DevSeeds do end end - defp generate_topic_without_replies(communications, users) do - forum = Repo.get_by!(Forum, short_name: random_forum()) - op = random_user(users) + defp generate_topic_without_replies(params) do + forum = Enum.random(params.forums) + op = random_user(params.users) Topics.create_topic( forum, request_attrs(op), %{ - "title" => random_title(communications), + "title" => random_title(params.communications), "posts" => %{ "0" => %{ - "body" => random_body(communications) + "body" => random_body(params.communications) } } } From c010dc0c280cc65ea95d7b6c04e7fb2f16b24934 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 14:51:22 +0000 Subject: [PATCH 45/61] Remove now useless `COPY` instructions because we use a volume bind-mount instead --- docker/app/Dockerfile | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index fddd19b3f..ff3e3b91b 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -40,15 +40,8 @@ RUN git clone --depth 1 https://github.com/philomena-dev/mediatools /tmp/mediato && cd /tmp/mediatools \ && make -j$(nproc) install -COPY scripts/lib.sh /var/philomena/scripts/ +# docker-compose configures a bind-mount of the repo root dir to /srv/philomena +ENV PATH=$PATH:/root/.cargo/bin:/srv/philomena/docker/app -COPY \ - docker/app/run-development \ - docker/app/run-test \ - docker/app/safe-rsvg-convert \ - docker/app/purge-cache \ - /var/philomena/docker/app/ - -ENV PATH=$PATH:/root/.cargo/bin:/var/philomena/docker/app EXPOSE 5173 CMD run-development From 0ae58d549d52a5a7d5145473453eae6139c6c40b Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 17:07:56 +0000 Subject: [PATCH 46/61] Add more topic name variety to reduce the probability of collisions --- priv/repo/seeds/dev/communications.json | 53 +++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/priv/repo/seeds/dev/communications.json b/priv/repo/seeds/dev/communications.json index 806e235b7..b849d0404 100644 --- a/priv/repo/seeds/dev/communications.json +++ b/priv/repo/seeds/dev/communications.json @@ -50,11 +50,26 @@ "I want", "The", "Writing", - "A", + "All", "Realizing", "Retconning", "Real", - "Incredible" + "Incredible", + "Pure", + "The best", + "Cute", + "Worshiping", + "Enjoying", + "Loving", + "Possessing", + "Getting your own", + "The art of", + "The science of", + "The history of", + "The future of", + "Our relationship with", + "The truth about", + "The lies about" ], "second": [ "pancakes", @@ -71,7 +86,22 @@ "dogs", "fruits", "code", - "games" + "games", + "art", + "AI", + "ponies", + "mares", + "snowpity", + "hooves", + "ears", + "tails", + "wings", + "manes", + "horns", + "tummies", + "pegasi", + "unicorns", + "princesses" ], "third": [ "for fun!", @@ -88,7 +118,22 @@ "I dunno", "so I can be more popular", "for lolz", - "- is it real??" + "- is it real??", + "- my hot take", + "- a guide", + "- a tutorial", + "- a review", + "no pony asked for", + "- comphensive guide", + "- full overview", + "that you'll never believe", + "that every mare talks about", + "- what mares think", + "- what stallions think", + "(everypony talks about it)", + "(spicy interview)", + "by \"The Canterlot Newspaper\"", + "by \"Overheard in Ponyville\"" ] } } From e3f49550540fbfa42c7c8bf147b49c6155697184 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 17:08:15 +0000 Subject: [PATCH 47/61] Add more context to topic name collision error --- lib/philomena/topics/topic.ex | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/philomena/topics/topic.ex b/lib/philomena/topics/topic.ex index d0e04c0be..694447f67 100644 --- a/lib/philomena/topics/topic.ex +++ b/lib/philomena/topics/topic.ex @@ -53,15 +53,25 @@ defmodule Philomena.Topics.Topic do changes |> get_field(:anonymous) + changes = + changes + |> validate_length(:title, min: 4, max: 96, count: :bytes) + |> put_slug() + |> change(forum: forum, user: attribution[:user]) + |> validate_required(:forum) + |> cast_assoc(:poll, with: &Poll.changeset/2) + |> cast_assoc(:posts, with: &Post.topic_creation_changeset(&1, &2, attribution, anonymous?)) + |> validate_length(:posts, is: 1) + changes - |> validate_length(:title, min: 4, max: 96, count: :bytes) - |> put_slug() - |> change(forum: forum, user: attribution[:user]) - |> validate_required(:forum) - |> cast_assoc(:poll, with: &Poll.changeset/2) - |> cast_assoc(:posts, with: &Post.topic_creation_changeset(&1, &2, attribution, anonymous?)) - |> validate_length(:posts, is: 1) - |> unique_constraint(:slug, name: :index_topics_on_forum_id_and_slug) + |> unique_constraint( + :slug, + name: :index_topics_on_forum_id_and_slug, + message: + "A topic with the similar name already exists. " <> + "Choose a different name that doesn't collide with the following " <> + "slug (name in the URL): '#{get_change(changes, :slug)}'" + ) end def stick_changeset(topic) do From fd057007fadf4606e471fc33ee8efa2f27dc2a75 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 17:08:34 +0000 Subject: [PATCH 48/61] Retry the seeding of a topic on name collision --- priv/repo/seeds_development.exs | 115 +++++++++++++++++--------------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index d9a08b0e9..ed15db70a 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -70,11 +70,11 @@ defmodule Philomena.DevSeeds do } 1..500 - |> Task.async_stream(fn _ -> generate_topic_without_replies(topic_params) end) + |> Task.async_stream(fn _ -> generate_topic_without_posts(topic_params) end) |> Stream.run() - 1..20 - |> Task.async_stream(fn _ -> generate_topic_with_replies(topic_params) end) + 500..520 + |> Task.async_stream(fn _ -> generate_topic_with_posts(topic_params) end) |> Stream.run() Logger.info("---- Done.") @@ -223,71 +223,72 @@ defmodule Philomena.DevSeeds do |> Enum.join("\n\n") end + # `nonce` is a unique number for each topic that is used in the title to make + # sure we don't generate conflicting titles defp random_title(%{"titles" => titles}) do - Enum.random(titles["first"]) <> - " " <> - Enum.random(titles["second"]) <> - " " <> + [ + Enum.random(titles["first"]), + Enum.random(titles["second"]), Enum.random(titles["third"]) + ] + |> Enum.join(" ") end - defp generate_topic_with_replies(params) do - forum = Enum.random(params.forums) - op = random_user(params.users) + defp generate_topic_posts(params, topic, op) do + count = :rand.uniform(250) + 5 - Topics.create_topic( - forum, - request_attrs(op), - %{ - "title" => random_title(params.communications), - "posts" => %{ - "0" => %{ - "body" => random_body(params.communications) - } - } - } - ) - |> case do - {:ok, %{topic: topic}} -> - count = :rand.uniform(250) + 5 - - generate_post = fn _ -> - user = random_user(params.users) - - Posts.create_post( - topic, - request_attrs(user), - %{"body" => random_body(params.communications)} - ) - |> case do - {:ok, %{post: post}} -> - Posts.approve_post(post, op) - Posts.reindex_post(post) - - {:error, :post, changeset, _so_far} -> - Logger.error(inspect(changeset.errors)) - end - end + generate_post = fn _ -> + user = random_user(params.users) + + Posts.create_post( + topic, + request_attrs(user), + %{"body" => random_body(params.communications)} + ) + |> case do + {:ok, %{post: post}} -> + Posts.approve_post(post, op) + Posts.reindex_post(post) + + {:error, :post, changeset, _so_far} -> + Logger.error("Failed to create a post: #{inspect(changeset.errors)}") + end + end - 1..count - |> Task.async_stream(generate_post, timeout: :infinity) + 1..count + |> Task.async_stream(generate_post, timeout: :infinity) - Logger.info("[Topics] Created topic ##{topic.id} with #{count} replies") + Logger.info("[Topics] Created topic ##{topic.id} with #{count} replies") + end + + defp generate_topic_with_posts(params) do + result = generate_topic(params) + + if !is_nil(result) do + {topic, op} = result + generate_topic_posts(params, topic, op) + end + end + + defp generate_topic_without_posts(params) do + result = generate_topic(params) - {:error, :topic, changeset, _so_far} -> - Logger.error(inspect(changeset.errors)) + if !is_nil(result) do + {topic, _op} = result + Logger.info("[Topics] Created topic ##{topic.id}") end end - defp generate_topic_without_replies(params) do + defp generate_topic(params) do forum = Enum.random(params.forums) op = random_user(params.users) + title = random_title(params.communications) Topics.create_topic( forum, request_attrs(op), %{ - "title" => random_title(params.communications), + "title" => title, "posts" => %{ "0" => %{ "body" => random_body(params.communications) @@ -297,10 +298,16 @@ defmodule Philomena.DevSeeds do ) |> case do {:ok, %{topic: topic}} -> - Logger.info("[Topics] Created topic ##{topic.id}") - - {:error, :topic, changeset, _so_far} -> - Logger.error(inspect(changeset.errors)) + {topic, op} + + {:error, :topic, %{errors: errors}, _changes_so_far} -> + if inspect(errors) |> String.contains?("already exists") do + Logger.info("[Topics] Random title collision (#{title}), retrying...") + generate_topic(params) + else + Logger.error("[Topics] Failed to create a topic: #{inspect(errors)}") + nil + end end end end From a363ee56d75d0eb259ad3ecad1ff2aa7e94731c9 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 17:17:13 +0000 Subject: [PATCH 49/61] Add more logs to the thumbnailer --- lib/philomena/images/thumbnailer.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/philomena/images/thumbnailer.ex b/lib/philomena/images/thumbnailer.ex index c90a638bf..f25559a1d 100644 --- a/lib/philomena/images/thumbnailer.ex +++ b/lib/philomena/images/thumbnailer.ex @@ -15,6 +15,8 @@ defmodule Philomena.Images.Thumbnailer do alias Philomena.Images.Image alias Philomena.Repo + require Logger + @versions [ thumb_tiny: {50, 50}, thumb_small: {150, 150}, @@ -75,6 +77,9 @@ defmodule Philomena.Images.Thumbnailer do def generate_thumbnails(image_id) do image = Repo.get!(Image, image_id) + + Logger.debug("[Thumbnailer] Generating thumbnails for the image #{image.id}") + file = download_image_file(image) {:ok, analysis} = Analyzers.analyze_path(file) From d2103e6416e5a2a704b9642fc3b316b11bb18360 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 19:10:18 +0000 Subject: [PATCH 50/61] Less noisy logs for seeding topics --- priv/repo/seeds_development.exs | 36 ++++++++++++--------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index ed15db70a..e5eafba21 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -69,13 +69,19 @@ defmodule Philomena.DevSeeds do forums: forums } - 1..500 - |> Task.async_stream(fn _ -> generate_topic_without_posts(topic_params) end) - |> Stream.run() + total = + 1..500 + |> Task.async_stream(fn _ -> generate_topic(topic_params) end) + |> Enum.count(&(not is_nil(&1))) - 500..520 - |> Task.async_stream(fn _ -> generate_topic_with_posts(topic_params) end) - |> Stream.run() + Logger.info("---- Generating #{total} topics without posts") + + total = + 500..520 + |> Task.async_stream(fn _ -> generate_topic_with_posts(topic_params) end) + |> Enum.count(&(not is_nil(&1))) + + Logger.info("---- Generating #{total} topics with posts") Logger.info("---- Done.") end @@ -113,9 +119,6 @@ defmodule Philomena.DevSeeds do now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) {:ok, %{body: body}} = PhilomenaProxy.Http.get(image_def["url"]) - - Logger.info("[Images] Fetched #{image_def["url"]}") - File.write!(file, body) upload = %Plug.Upload{ @@ -124,8 +127,6 @@ defmodule Philomena.DevSeeds do filename: "fixtures-#{now}" } - Logger.info("[Images] Creating image ...") - Images.create_image(pleb_attrs, Map.merge(image_def, %{"image" => upload})) |> case do {:ok, %{image: image, upload_pid: upload_pid}} -> @@ -136,7 +137,7 @@ defmodule Philomena.DevSeeds do Images.reindex_image(image) Tags.reindex_tags(image.added_tags) - Logger.info("[Images] Created image ##{image.id}") + Logger.info("[Images] Created #{image_def["url"]} with id ##{image.id}") {:error, :image, changeset, _so_far} -> Logger.error(inspect(changeset.errors)) @@ -257,8 +258,6 @@ defmodule Philomena.DevSeeds do 1..count |> Task.async_stream(generate_post, timeout: :infinity) - - Logger.info("[Topics] Created topic ##{topic.id} with #{count} replies") end defp generate_topic_with_posts(params) do @@ -270,15 +269,6 @@ defmodule Philomena.DevSeeds do end end - defp generate_topic_without_posts(params) do - result = generate_topic(params) - - if !is_nil(result) do - {topic, _op} = result - Logger.info("[Topics] Created topic ##{topic.id}") - end - end - defp generate_topic(params) do forum = Enum.random(params.forums) op = random_user(params.users) From 93e4e39d54e5baef4ba355c908a9cfa814d3b044 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 19:10:36 +0000 Subject: [PATCH 51/61] Enable module metadata in dev logs for easier navigation --- config/dev.exs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index a4617d27d..876dbcdc7 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -85,6 +85,11 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime -config :logger, :console, format: "$time $metadata[$level] $message\n" -# Uncomment to show additional metadata i the logs to see where they come from -# metadata: [:application, :mfa] +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [ + :module + # Uncomment to show additional metadata to see more details + # about where they come from + # :application, :mfa + ] From 01e49fcb61ab2f6e7d46d1f5c560fba1ed01491f Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 19:10:59 +0000 Subject: [PATCH 52/61] Remove unnecessary `[Thumbnails]` prefix now that module metadata is in the logs --- lib/philomena/images/thumbnailer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/philomena/images/thumbnailer.ex b/lib/philomena/images/thumbnailer.ex index f25559a1d..1beea1465 100644 --- a/lib/philomena/images/thumbnailer.ex +++ b/lib/philomena/images/thumbnailer.ex @@ -78,7 +78,7 @@ defmodule Philomena.Images.Thumbnailer do def generate_thumbnails(image_id) do image = Repo.get!(Image, image_id) - Logger.debug("[Thumbnailer] Generating thumbnails for the image #{image.id}") + Logger.debug("Generating thumbnails for the image #{image.id}") file = download_image_file(image) {:ok, analysis} = Analyzers.analyze_path(file) From 284558ca43c9062f809fe58501c1887776e8eac4 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 19:14:20 +0000 Subject: [PATCH 53/61] Add log filtering via `PHILOMENA_LOG` env var --- docker-compose.yml | 8 +++++ lib/philomena/application.ex | 64 +++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2a386b5d8..30e8b2df7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,14 @@ services: # Set this env var to `1` if you want to drop your current postgres DB and # reseed the DB from scratch again. - DROP_DB + # Use this env variable to control the logs levels from different modules. + # The syntax is `{module.function}=level,...` and the `module.function`. + # The filter evaluation stops on the first `{module.function}` that has a + # prefix match against the log event's module and function. The last entry + # in the list of filters should be a bare `level` which will be used as a + # catch-all for all other log events that do not match any of the previous + # filters. + - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,debug} - MIX_ENV=dev - PGPASSWORD=postgres - ANONYMOUS_NAME_SALT=2fmJRo0OgMFe65kyAJBxPT0QtkVes/jnKDdtP21fexsRqiw8TlSY7yO+uFyMZycp diff --git a/lib/philomena/application.ex b/lib/philomena/application.ex index e34f8ac2e..b57389410 100644 --- a/lib/philomena/application.ex +++ b/lib/philomena/application.ex @@ -6,15 +6,7 @@ defmodule Philomena.Application do use Application def start(_type, _args) do - exclude_log_event? = fn event -> - # Skip DB logs, they are too verbose - Map.get(event.meta, :application) in [:ecto_sql, :exq] - end - - :logger.add_primary_filter( - :sql_logs, - {fn event, _ -> if(exclude_log_event?.(event), do: :stop, else: :ignore) end, []} - ) + configure_logging() # List all child processes to be supervised children = [ @@ -66,4 +58,58 @@ defmodule Philomena.Application do do: Base.encode16(:crypto.strong_rand_bytes(6)) defp valid_node_name(node), do: node + + defp configure_logging() do + # Log filtering design is borrowed from the Rust's `tracing` observability framework. + # Specifically from the `EnvFilter` syntax: + # https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html + # However, it implements a simple subset of that syntax for prefix matching. + # + # It would also be cool to get tracing's spans model for better low-level and + # concurrent logs context. But spans implementation would require a lot of work, + # unless there is an existing library for that. Anyway, for now, this should suffice. + filters = + System.get_env("PHILOMENA_LOG", "") + |> String.split(",") + |> Enum.map(&String.trim(&1)) + |> Enum.reject(&(&1 == "")) + |> Enum.map(fn directive -> + {selector, level} = + case String.split(directive, "=", parts: 2) do + [selector, level] -> {selector, level} + [level] -> {nil, level} + end + + {selector, String.to_existing_atom(level)} + end) + + if not Enum.empty?(filters) do + allow_log_event? = fn event -> + case(Map.get(event.meta, :mfa)) do + nil -> + false + + {module, function, _arity} -> + scope = "#{inspect(module)}.#{function}" + + filters + |> Enum.find(fn {selector, _level} -> + is_nil(selector) or String.starts_with?(scope, selector) + end) + |> case do + nil -> + false + + {_selector, level} -> + Logger.compare_levels(event.level, level) != :lt + end + end + end + + :logger.add_primary_filter( + :sql_logs, + {fn event, _ -> if(allow_log_event?.(event), do: :ignore, else: :stop) end, []} + ) + end + end end From 673c577f5ac3b335082b121e036ed5ca103a7a8e Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 20:13:32 +0000 Subject: [PATCH 54/61] Add `dev.sh` script wrapper over `docker compose` --- docker-compose.yml | 5 +---- docker/app/run-development | 5 ----- scripts/dev.sh | 28 ++++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100755 scripts/dev.sh diff --git a/docker-compose.yml b/docker-compose.yml index 30e8b2df7..2e6089539 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,9 +12,6 @@ services: context: . dockerfile: ./docker/app/Dockerfile environment: - # Set this env var to `1` if you want to drop your current postgres DB and - # reseed the DB from scratch again. - - DROP_DB # Use this env variable to control the logs levels from different modules. # The syntax is `{module.function}=level,...` and the `module.function`. # The filter evaluation stops on the first `{module.function}` that has a @@ -22,7 +19,7 @@ services: # in the list of filters should be a bare `level` which will be used as a # catch-all for all other log events that do not match any of the previous # filters. - - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,debug} + - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,PhilomenaMedia.Objects=none,debug} - MIX_ENV=dev - PGPASSWORD=postgres - ANONYMOUS_NAME_SALT=2fmJRo0OgMFe65kyAJBxPT0QtkVes/jnKDdtP21fexsRqiw8TlSY7yO+uFyMZycp diff --git a/docker/app/run-development b/docker/app/run-development index 740e5eaec..c0783a5ca 100755 --- a/docker/app/run-development +++ b/docker/app/run-development @@ -52,11 +52,6 @@ until step wget --no-check-certificate -qO - http://opensearch:9200; do sleep 2 done -# If `DROP_DB` env var is set, drop the database -if [[ "${DROP_DB:-}" == "1" ]]; then - step dropdb -h postgres -U postgres philomena_dev -fi - # Try to create the database if it doesn't exist yet if step createdb -h postgres -U postgres philomena_dev; then step mix ecto.setup_dev diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 000000000..35fb8d6f2 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Script to start the docker-compose stack for development. + +set -euo pipefail + +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# Set this env var to `1` if you want to drop your current postgres DB and +# reseed the DB from scratch again. +drop_db=${drop_db:-0} + + +if [[ "$drop_db" == "1" ]]; then + info "Dropping databases..." + + # It's important to stop all containers to make sure they shut down cleanly. + # Also, `valkey` stores its data in RAM, so to drop its "database" we need + # to stop its container. + step docker compose down + + # We aren't using `--volumes` parameter with the `docker compose down` because + # we don't want to delete the build caches, which are also stored in volumes. + step docker volume rm \ + philomena_postgres_data \ + philomena_opensearch_data +fi + +step docker compose up --no-log-prefix From 2b8cc218203f1e1645ec4197b849748311915f1d Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 20:14:02 +0000 Subject: [PATCH 55/61] Reindex only images that were changed after generating comments --- priv/repo/seeds_development.exs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index e5eafba21..b80fd297b 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -188,15 +188,20 @@ defmodule Philomena.DevSeeds do {:ok, %{comment: comment}} -> Comments.approve_comment(comment, user) Comments.reindex_comment(comment) - Images.reindex_image(image) + {:ok, image} {:error, :comment, changeset, _so_far} -> - Logger.error(inspect(changeset.errors)) + Logger.error("Failed to create image comments: #{inspect(changeset.errors)}") + {:error, nil} end end - 1..1000 - |> Task.async_stream(generate_comment) + images = 1..1000 |> Task.async_stream(generate_comment, timeout: :infinity) + + # Reindex all images that got comments + for({:ok, {:ok, image}} <- images, do: image) + |> Enum.uniq_by(& &1.id) + |> Task.async_stream(&Images.reindex_image/1) |> Stream.run() end From c227a4bb74177d94c33c6e073bb107e760b6d6eb Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 20:14:26 +0000 Subject: [PATCH 56/61] approve_comment already reindexes the comment --- priv/repo/seeds_development.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index b80fd297b..202596d9d 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -187,7 +187,6 @@ defmodule Philomena.DevSeeds do |> case do {:ok, %{comment: comment}} -> Comments.approve_comment(comment, user) - Comments.reindex_comment(comment) {:ok, image} {:error, :comment, changeset, _so_far} -> From 1bc362d3e60951539b798a89478f32be3525978c Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 22:27:31 +0000 Subject: [PATCH 57/61] Display new uploads immediately once their thumbnails were generated in dev env --- config/dev.exs | 4 ++++ lib/philomena_web/controllers/activity_controller.ex | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/dev.exs b/config/dev.exs index 876dbcdc7..b98dabcdd 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -78,6 +78,10 @@ config :philomena, csp_relaxed: true # Enable Vite HMR config :philomena, vite_reload: true +# Display the uploads immediately once thumbnails are generated, we don't need +# to wait more than that in dev mode. +config :philomena, new_uploads_hidden_duration: "0 minutes" + # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. config :phoenix, :stacktrace_depth, 20 diff --git a/lib/philomena_web/controllers/activity_controller.ex b/lib/philomena_web/controllers/activity_controller.ex index 3e85233c8..a232eda27 100644 --- a/lib/philomena_web/controllers/activity_controller.ex +++ b/lib/philomena_web/controllers/activity_controller.ex @@ -17,13 +17,21 @@ defmodule PhilomenaWeb.ActivityController do alias Philomena.Repo import Ecto.Query + # Delay displaying new uploads on the homepage to give the users some time to + # add last-minute tags on the image after the fact of upload. + @new_uploads_hidden_duration Application.compile_env( + :philomena, + :new_uploads_hidden_duration, + "3 minutes" + ) + def index(conn, _params) do user = conn.assigns.current_user {:ok, {images, _tags}} = ImageLoader.search_string( conn, - "created_at.lte:3 minutes ago, -thumbnails_generated:false", + "created_at.lte:#{@new_uploads_hidden_duration} ago, -thumbnails_generated:false", pagination: %{conn.assigns.image_pagination | page_number: 1} ) From a8fe3bce490a6ca57daed398860ea9cea8129aaf Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 22:27:49 +0000 Subject: [PATCH 58/61] Use uppercase env var name for DROP_DB --- scripts/dev.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/dev.sh b/scripts/dev.sh index 35fb8d6f2..3c7c5216a 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -7,10 +7,9 @@ set -euo pipefail # Set this env var to `1` if you want to drop your current postgres DB and # reseed the DB from scratch again. -drop_db=${drop_db:-0} +DROP_DB=${DROP_DB:-0} - -if [[ "$drop_db" == "1" ]]; then +if [[ "$DROP_DB" == "1" ]]; then info "Dropping databases..." # It's important to stop all containers to make sure they shut down cleanly. From c87e7dde7fbae05d990216f0541a81e3063d06a8 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 5 Apr 2025 22:37:22 +0000 Subject: [PATCH 59/61] Add a FIXME about the pagination bug with low activity --- lib/philomena_web/controllers/activity_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/philomena_web/controllers/activity_controller.ex b/lib/philomena_web/controllers/activity_controller.ex index a232eda27..966ab323f 100644 --- a/lib/philomena_web/controllers/activity_controller.ex +++ b/lib/philomena_web/controllers/activity_controller.ex @@ -40,6 +40,8 @@ defmodule PhilomenaWeb.ActivityController do conn, %{range: %{first_seen_at: %{gt: "now-3d"}}}, sorts: &%{query: &1, sorts: [%{wilson_score: :desc}, %{first_seen_at: :desc}]}, + # FIXME: if there is very little activity for the last 3 days this may return + # no results as it may select the page number that doesn't exist. pagination: %{page_number: :rand.uniform(6), page_size: 4} ) From d5196d1adfd7cade1b325cbd4d5073e97ac90118 Mon Sep 17 00:00:00 2001 From: MareStare Date: Sun, 6 Apr 2025 00:50:37 +0000 Subject: [PATCH 60/61] Fix an type error in objects.ex and increase logs verbosity level for this module --- docker-compose.yml | 2 +- lib/philomena_media/objects.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2e6089539..cecb1d342 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: # in the list of filters should be a bare `level` which will be used as a # catch-all for all other log events that do not match any of the previous # filters. - - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,PhilomenaMedia.Objects=none,debug} + - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,PhilomenaMedia.Objects=info,debug} - MIX_ENV=dev - PGPASSWORD=postgres - ANONYMOUS_NAME_SALT=2fmJRo0OgMFe65kyAJBxPT0QtkVes/jnKDdtP21fexsRqiw8TlSY7yO+uFyMZycp diff --git a/lib/philomena_media/objects.ex b/lib/philomena_media/objects.ex index 171c6b764..0233c5126 100644 --- a/lib/philomena_media/objects.ex +++ b/lib/philomena_media/objects.ex @@ -217,7 +217,7 @@ defmodule PhilomenaMedia.Objects do @spec replicate_request(operation_fn(), (keyword() -> [term()])) :: :ok defp replicate_request(operation, args) do - operation_name = Function.info(operation, :name) + {:name, operation_name} = Function.info(operation, :name) backends = backends() total_err = From 2b12042681eac4f6a271843dc5499defd233cd31 Mon Sep 17 00:00:00 2001 From: MareStare Date: Mon, 7 Apr 2025 02:26:34 +0000 Subject: [PATCH 61/61] Fix a comment --- docker-compose.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index cecb1d342..c56e3f8ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,13 +13,12 @@ services: dockerfile: ./docker/app/Dockerfile environment: # Use this env variable to control the logs levels from different modules. - # The syntax is `{module.function}=level,...` and the `module.function`. - # The filter evaluation stops on the first `{module.function}` that has a - # prefix match against the log event's module and function. The last entry - # in the list of filters should be a bare `level` which will be used as a - # catch-all for all other log events that do not match any of the previous - # filters. - - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,PhilomenaMedia.Objects=info,debug} + # The syntax is `{module.function}=level,...`. The filter evaluation stops + # on the first `{module.function}` that has a prefix match against the log + # event's module and function. The last entry in the list of filters should + # be a bare `level` which will be used as a catch-all for all other log + # events that do not match any of the previous filters. + - PHILOMENA_LOG=${PHILOMENA_LOG-Ecto=none,Exq=none,PhilomenaMedia.Objects=debug,debug} - MIX_ENV=dev - PGPASSWORD=postgres - ANONYMOUS_NAME_SALT=2fmJRo0OgMFe65kyAJBxPT0QtkVes/jnKDdtP21fexsRqiw8TlSY7yO+uFyMZycp