From e154ebbf7933123e91d5b5c6f5070e78eb3e383b Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 15 Aug 2021 21:53:04 +0300 Subject: [PATCH 001/271] Initial meilisearch implementation, doesn't delete posts yet --- config/config.exs | 7 ++- config/test.exs | 2 + lib/mix/tasks/pleroma/search/meilisearch.ex | 38 ++++++++++++ lib/pleroma/activity.ex | 1 + lib/pleroma/activity/search.ex | 4 +- lib/pleroma/application.ex | 6 +- lib/pleroma/search/meilisearch.ex | 60 +++++++++++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 6 ++ .../controllers/search_controller.ex | 5 +- 9 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 lib/mix/tasks/pleroma/search/meilisearch.ex create mode 100644 lib/pleroma/search/meilisearch.ex diff --git a/config/config.exs b/config/config.exs index 4e21ce457..1df7dd44b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -866,9 +866,14 @@ config :pleroma, ConcurrentLimiter, [ {Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]}, - {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]} + {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}, + {Pleroma.Search, [max_running: 20, max_waiting: 50]} ] +config :pleroma, Pleroma.Search, module: Pleroma.Activity.Search + +config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/" + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/test.exs b/config/test.exs index d5c25f65e..d1c356f14 100644 --- a/config/test.exs +++ b/config/test.exs @@ -133,6 +133,8 @@ ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, logger: Pleroma.LoggerMock +config :pleroma, Pleroma.Search, module: Pleroma.Activity.Search + # Reduce recompilation time # https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects config :phoenix, :plug_init_mode, :runtime diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex new file mode 100644 index 000000000..2af8e5853 --- /dev/null +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Search.Meilisearch do + import Mix.Pleroma + + import Ecto.Query + + def run(["index"]) do + start_pleroma() + + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + Pleroma.Repo.chunk_stream( + from(Pleroma.Object, + limit: 200, + where: fragment("data->>'type' = 'Note'") and fragment("LENGTH(data->>'source') > 0") + ), + 100, + :batches + ) + |> Stream.map(fn objects -> + Enum.map(objects, fn object -> + data = object.data + %{id: object.id, source: data["source"], ap: data["id"]} + end) + end) + |> Stream.each(fn activities -> + {:ok, _} = + Pleroma.HTTP.post( + "#{endpoint}/indexes/objects/documents", + Jason.encode!(activities) + ) + end) + |> Stream.run() + end +end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index ebfd4ed45..9563136f9 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -367,6 +367,7 @@ def restrict_deactivated_users(query) do end defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search + def add_to_index(_activity), do: nil def direct_conversation_id(activity, for_user) do alias Pleroma.Conversation.Participation diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index 0b9b24aa4..3dce9d355 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -136,7 +136,7 @@ defp query_with(q, :rum, search_query, :websearch) do ) end - defp maybe_restrict_local(q, user) do + def maybe_restrict_local(q, user) do limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) case {limit, user} do @@ -149,7 +149,7 @@ defp maybe_restrict_local(q, user) do defp restrict_local(q), do: where(q, local: true) - defp maybe_fetch(activities, user, search_query) do + def maybe_fetch(activities, user, search_query) do with true <- Regex.match?(~r/https?:/, search_query), {:ok, object} <- Fetcher.fetch_object_from_id(search_query), %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 1c1db8c10..62d1b8b39 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -321,7 +321,11 @@ defp http_children(_, _), do: [] def limiters_setup do config = Config.get(ConcurrentLimiter, []) - [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy] + [ + Pleroma.Web.RichMedia.Helpers, + Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, + Pleroma.Search + ] |> Enum.each(fn module -> mod_config = Keyword.get(config, module, []) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex new file mode 100644 index 000000000..92e0d3429 --- /dev/null +++ b/lib/pleroma/search/meilisearch.ex @@ -0,0 +1,60 @@ +defmodule Pleroma.Search.Meilisearch do + require Logger + + alias Pleroma.Activity + + import Pleroma.Activity.Search + import Ecto.Query + + def search(user, query, options \\ []) do + limit = Enum.min([Keyword.get(options, :limit), 40]) + offset = Keyword.get(options, :offset, 0) + author = Keyword.get(options, :author) + + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, result} = + Pleroma.HTTP.post( + "#{endpoint}/indexes/objects/search", + Jason.encode!(%{q: query, offset: offset, limit: limit}) + ) + + hits = Jason.decode!(result.body)["hits"] |> Enum.map(& &1["ap"]) + + try do + hits + |> Activity.create_by_object_ap_id() + |> Activity.with_preloaded_object() + |> Activity.with_preloaded_object() + |> Activity.restrict_deactivated_users() + |> maybe_restrict_local(user) + |> maybe_restrict_author(author) + |> maybe_restrict_blocked(user) + |> maybe_fetch(user, query) + |> order_by([activity], desc: activity.id) + |> Pleroma.Repo.all() + rescue + _ -> maybe_fetch([], user, query) + end + end + + def add_to_index(activity) do + object = activity.object + + if activity.data["type"] == "Create" and not is_nil(object) and object.data["type"] == "Note" do + data = object.data + + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, result} = + Pleroma.HTTP.post( + "#{endpoint}/indexes/objects/documents", + Jason.encode!([%{id: object.id, source: data["source"], ap: data["id"]}]) + ) + + if not Map.has_key?(Jason.decode!(result.body), "updateId") do + Logger.error("Failed to add activity #{activity.id} to index: #{result.body}") + end + end + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a5d7036d9..034c3b185 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -140,6 +140,12 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) end) + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + ConcurrentLimiter.limit(Pleroma.Search, fn -> + Task.start(fn -> search_module.add_to_index(activity) end) + end) + {:ok, activity} else %Activity{} = activity -> diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 5e6e04734..99c33eba6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller - alias Pleroma.Activity alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ControllerHelper @@ -100,7 +99,9 @@ defp resource_search(_, "accounts", query, options) do end defp resource_search(_, "statuses", query, options) do - statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) + search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity) + + statuses = with_fallback(fn -> search_module.search(options[:for_user], query, options) end) StatusView.render("index.json", activities: statuses, From 0318e9a59945d7a5625111157867f0f9ebaffd91 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 16 Aug 2021 10:18:01 +0300 Subject: [PATCH 002/271] Add logging to milisiearch index and make it use desc(id) --- lib/mix/tasks/pleroma/search/meilisearch.ex | 26 +++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 2af8e5853..1fece96e5 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -3,8 +3,9 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.Search.Meilisearch do - import Mix.Pleroma + require Logger + import Mix.Pleroma import Ecto.Query def run(["index"]) do @@ -12,12 +13,25 @@ def run(["index"]) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + {:ok, _} = + Pleroma.HTTP.post( + "#{endpoint}/indexes/objects/settings/ranking-rules", + Jason.encode!([ + "desc(id)", + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness" + ]) + ) + Pleroma.Repo.chunk_stream( from(Pleroma.Object, - limit: 200, where: fragment("data->>'type' = 'Note'") and fragment("LENGTH(data->>'source') > 0") ), - 100, + 200, :batches ) |> Stream.map(fn objects -> @@ -26,12 +40,14 @@ def run(["index"]) do %{id: object.id, source: data["source"], ap: data["id"]} end) end) - |> Stream.each(fn activities -> + |> Stream.each(fn objects -> {:ok, _} = Pleroma.HTTP.post( "#{endpoint}/indexes/objects/documents", - Jason.encode!(activities) + Jason.encode!(objects) ) + + IO.puts("Indexed #{Enum.count(objects)} entries") end) |> Stream.run() end From 365024abec905e427babb5403f0fccbde65f4bcd Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 16 Aug 2021 22:24:31 +0300 Subject: [PATCH 003/271] Ensure only indexing public posts and implement clearing and delete --- lib/mix/tasks/pleroma/search/meilisearch.ex | 15 ++++++++++++++- lib/pleroma/activity.ex | 1 + lib/pleroma/search/meilisearch.ex | 17 ++++++++++++++++- lib/pleroma/web/common_api.ex | 7 +++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 1fece96e5..0b86fdece 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -4,6 +4,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do require Logger + require Pleroma.Constants import Mix.Pleroma import Ecto.Query @@ -29,7 +30,11 @@ def run(["index"]) do Pleroma.Repo.chunk_stream( from(Pleroma.Object, - where: fragment("data->>'type' = 'Note'") and fragment("LENGTH(data->>'source') > 0") + # Only index public posts which are notes and have some text + where: + fragment("data->>'type' = 'Note'") and + fragment("LENGTH(data->>'source') > 0") and + fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) ), 200, :batches @@ -51,4 +56,12 @@ def run(["index"]) do end) |> Stream.run() end + + def run(["clear"]) do + start_pleroma() + + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, _} = Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects/documents", "", [], []) + end end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 9563136f9..2c168fd41 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -368,6 +368,7 @@ def restrict_deactivated_users(query) do defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search def add_to_index(_activity), do: nil + def remove_from_index(_object), do: nil def direct_conversation_id(activity, for_user) do alias Pleroma.Conversation.Participation diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 92e0d3429..dbe6b2d67 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -1,5 +1,6 @@ defmodule Pleroma.Search.Meilisearch do require Logger + require Pleroma.Constants alias Pleroma.Activity @@ -41,7 +42,8 @@ def search(user, query, options \\ []) do def add_to_index(activity) do object = activity.object - if activity.data["type"] == "Create" and not is_nil(object) and object.data["type"] == "Note" do + if activity.data["type"] == "Create" and not is_nil(object) and object.data["type"] == "Note" and + Pleroma.Constants.as_public() in object.data["to"] do data = object.data endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) @@ -57,4 +59,17 @@ def add_to_index(activity) do end end end + + def remove_from_index(object) do + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, _} = + Pleroma.HTTP.request( + :delete, + "#{endpoint}/indexes/objects/documents/#{object.id}", + "", + [], + [] + ) + end end diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 89f5dd606..54a8aa213 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -147,6 +147,13 @@ def delete(activity_id, user) do true <- User.superuser?(user) || user.ap_id == object.data["actor"], {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do + # Also delete from search index + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + ConcurrentLimiter.limit(Pleroma.Search, fn -> + Task.start(fn -> search_module.remove_from_index(object) end) + end) + {:ok, delete} else {:find_activity, _} -> From ea6a6a128712e81c4f298b2bb2cedfadf2295cff Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 16 Aug 2021 22:30:56 +0300 Subject: [PATCH 004/271] Make the indexing batch differently and more, show number indexed --- lib/mix/tasks/pleroma/search/meilisearch.ex | 65 ++++++++++++--------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 0b86fdece..2a6438528 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -28,33 +28,46 @@ def run(["index"]) do ]) ) - Pleroma.Repo.chunk_stream( - from(Pleroma.Object, - # Only index public posts which are notes and have some text - where: - fragment("data->>'type' = 'Note'") and - fragment("LENGTH(data->>'source') > 0") and - fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) - ), - 200, - :batches - ) - |> Stream.map(fn objects -> - Enum.map(objects, fn object -> - data = object.data - %{id: object.id, source: data["source"], ap: data["id"]} - end) - end) - |> Stream.each(fn objects -> - {:ok, _} = - Pleroma.HTTP.post( - "#{endpoint}/indexes/objects/documents", - Jason.encode!(objects) - ) + chunk_size = 100_000 - IO.puts("Indexed #{Enum.count(objects)} entries") - end) - |> Stream.run() + Pleroma.Repo.transaction( + fn -> + Pleroma.Repo.stream( + from(Pleroma.Object, + # Only index public posts which are notes and have some text + where: + fragment("data->>'type' = 'Note'") and + fragment("LENGTH(data->>'source') > 0") and + fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()), + order_by: fragment("data->'published' DESC") + ), + timeout: :infinity + ) + |> Stream.chunk_every(chunk_size) + |> Stream.transform(0, fn objects, acc -> + new_acc = acc + Enum.count(objects) + + IO.puts("Indexed #{new_acc} entries") + + {[objects], new_acc} + end) + |> Stream.map(fn objects -> + Enum.map(objects, fn object -> + data = object.data + %{id: object.id, source: data["source"], ap: data["id"]} + end) + end) + |> Stream.each(fn objects -> + {:ok, _} = + Pleroma.HTTP.post( + "#{endpoint}/indexes/objects/documents", + Jason.encode!(objects) + ) + end) + |> Stream.run() + end, + timeout: :infinity + ) end def run(["clear"]) do From 38996f551a4ec014e9f4cb4a691d31beecab43ba Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 17 Aug 2021 00:06:32 +0300 Subject: [PATCH 005/271] Make meilisearch sort on publish date converted to unix time --- lib/mix/tasks/pleroma/search/meilisearch.ex | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 2a6438528..2dd9c0a62 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -18,7 +18,7 @@ def run(["index"]) do Pleroma.HTTP.post( "#{endpoint}/indexes/objects/settings/ranking-rules", Jason.encode!([ - "desc(id)", + "desc(published)", "typo", "words", "proximity", @@ -54,7 +54,15 @@ def run(["index"]) do |> Stream.map(fn objects -> Enum.map(objects, fn object -> data = object.data - %{id: object.id, source: data["source"], ap: data["id"]} + + {:ok, published, _} = DateTime.from_iso8601(data["published"]) + + %{ + id: object.id, + source: data["source"], + ap: data["id"], + published: published |> DateTime.to_unix() + } end) end) |> Stream.each(fn objects -> From 9beaebd97e1746df010aecfcc01d9e2e9a4c60ac Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 17 Aug 2021 00:30:14 +0300 Subject: [PATCH 006/271] Tweak search ordering to hopefully return newer results --- lib/mix/tasks/pleroma/search/meilisearch.ex | 15 ++++++++++++--- lib/pleroma/search/meilisearch.ex | 13 +++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 2dd9c0a62..dcecbd7cf 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -39,7 +39,7 @@ def run(["index"]) do fragment("data->>'type' = 'Note'") and fragment("LENGTH(data->>'source') > 0") and fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()), - order_by: fragment("data->'published' DESC") + order_by: [desc: fragment("data->'published'")] ), timeout: :infinity ) @@ -66,11 +66,15 @@ def run(["index"]) do end) end) |> Stream.each(fn objects -> - {:ok, _} = + {:ok, result} = Pleroma.HTTP.post( "#{endpoint}/indexes/objects/documents", Jason.encode!(objects) ) + + if not Map.has_key?(Jason.decode!(result.body), "updateId") do + IO.puts("Failed to index: #{result}") + end end) |> Stream.run() end, @@ -83,6 +87,11 @@ def run(["clear"]) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, _} = Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects/documents", "", [], []) + {:ok, result} = + Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects/documents", "", [], []) + + if not Map.has_key?(Jason.decode!(result.body), "updateId") do + IO.puts("Failed to clear: #{result}") + end end end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index dbe6b2d67..9fdb0a07f 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -32,7 +32,7 @@ def search(user, query, options \\ []) do |> maybe_restrict_author(author) |> maybe_restrict_blocked(user) |> maybe_fetch(user, query) - |> order_by([activity], desc: activity.id) + |> order_by([object: obj], desc: obj.data["published"]) |> Pleroma.Repo.all() rescue _ -> maybe_fetch([], user, query) @@ -48,10 +48,19 @@ def add_to_index(activity) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + {:ok, published, _} = DateTime.from_iso8601(data["published"]) + {:ok, result} = Pleroma.HTTP.post( "#{endpoint}/indexes/objects/documents", - Jason.encode!([%{id: object.id, source: data["source"], ap: data["id"]}]) + Jason.encode!([ + %{ + id: object.id, + source: data["source"], + ap: data["id"], + published: published |> DateTime.to_unix() + } + ]) ) if not Map.has_key?(Jason.decode!(result.body), "updateId") do From 00c48a33acf0bd59fa7e7b58a67b049e4f4adc31 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 17 Aug 2021 00:57:53 +0300 Subject: [PATCH 007/271] Use content instead of source and scrub it --- lib/mix/tasks/pleroma/search/meilisearch.ex | 12 ++++-------- lib/pleroma/search/meilisearch.ex | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index dcecbd7cf..5270de255 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -37,7 +37,7 @@ def run(["index"]) do # Only index public posts which are notes and have some text where: fragment("data->>'type' = 'Note'") and - fragment("LENGTH(data->>'source') > 0") and + fragment("LENGTH(data->>'content') > 0") and fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()), order_by: [desc: fragment("data->'published'")] ), @@ -56,10 +56,11 @@ def run(["index"]) do data = object.data {:ok, published, _} = DateTime.from_iso8601(data["published"]) + {:ok, content} = FastSanitize.strip_tags(data["content"]) %{ id: object.id, - source: data["source"], + content: content, ap: data["id"], published: published |> DateTime.to_unix() } @@ -87,11 +88,6 @@ def run(["clear"]) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, result} = - Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects/documents", "", [], []) - - if not Map.has_key?(Jason.decode!(result.body), "updateId") do - IO.puts("Failed to clear: #{result}") - end + {:ok, _} = Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects", "", [], []) end end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 9fdb0a07f..87fdeaf5e 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -56,7 +56,7 @@ def add_to_index(activity) do Jason.encode!([ %{ id: object.id, - source: data["source"], + content: data["content"] |> Pleroma.HTML.filter_tags(), ap: data["id"], published: published |> DateTime.to_unix() } From e35d87ea54f70a39206f6103ef0e7334e2a428cc Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 17 Aug 2021 01:37:43 +0300 Subject: [PATCH 008/271] Make the chunk size smaller --- lib/mix/tasks/pleroma/search/meilisearch.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 5270de255..44af25f3e 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -28,7 +28,7 @@ def run(["index"]) do ]) ) - chunk_size = 100_000 + chunk_size = 10_000 Pleroma.Repo.transaction( fn -> From 2b2e409ad72862967cabf06344874ae9bff9860f Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 22 Aug 2021 16:37:52 +0300 Subject: [PATCH 009/271] Also index incoming federated posts --- lib/pleroma/search/search.ex | 18 ++++++++++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 7 ++----- lib/pleroma/web/activity_pub/side_effects.ex | 7 +++++++ lib/pleroma/web/common_api.ex | 8 ++------ 4 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 lib/pleroma/search/search.ex diff --git a/lib/pleroma/search/search.ex b/lib/pleroma/search/search.ex new file mode 100644 index 000000000..e363abf19 --- /dev/null +++ b/lib/pleroma/search/search.ex @@ -0,0 +1,18 @@ +defmodule Pleroma.Search do + def add_to_index(activity) do + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + ConcurrentLimiter.limit(Pleroma.Search, fn -> + Task.start(fn -> search_module.add_to_index(activity) end) + end) + end + + def remove_from_index(object) do + # Also delete from search index + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + ConcurrentLimiter.limit(Pleroma.Search, fn -> + Task.start(fn -> search_module.remove_from_index(object) end) + end) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 034c3b185..7178cf9eb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -140,11 +140,8 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) end) - search_module = Pleroma.Config.get([Pleroma.Search, :module]) - - ConcurrentLimiter.limit(Pleroma.Search, fn -> - Task.start(fn -> search_module.add_to_index(activity) end) - end) + # Add local posts to search index + Pleroma.Search.add_to_index(activity) {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 5eefd2824..15e006b18 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -197,6 +197,7 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do # - Increase replies count # - Set up ActivityExpiration # - Set up notifications + # - Index incoming posts for search (if needed) @impl true def handle(%{data: %{"type" => "Create"}} = activity, meta) do with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta), @@ -226,6 +227,8 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) end) + Pleroma.Search.add_to_index(Map.put(activity, :object, object)) + meta = meta |> add_notifications(notifications) @@ -286,6 +289,7 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do # - Reduce the user note count # - Reduce the reply count # - Stream out the activity + # - Removes posts from search index (if needed) @impl true def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do deleted_object = @@ -325,6 +329,9 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, if result == :ok do Notification.create_notifications(object) + + Pleroma.Search.remove_from_index(object) + {:ok, object, meta} else {:error, result} diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 54a8aa213..ba6c07975 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -147,12 +147,8 @@ def delete(activity_id, user) do true <- User.superuser?(user) || user.ap_id == object.data["actor"], {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do - # Also delete from search index - search_module = Pleroma.Config.get([Pleroma.Search, :module]) - - ConcurrentLimiter.limit(Pleroma.Search, fn -> - Task.start(fn -> search_module.remove_from_index(object) end) - end) + # Remove from search index for local posts + Pleroma.Search.remove_from_index(object) {:ok, delete} else From 9f16ca80e0fe60b8b0e3e8ddb9b06ca0bec31002 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 22 Aug 2021 18:47:41 +0300 Subject: [PATCH 010/271] Mark only content as searchable for meilisearch --- lib/mix/tasks/pleroma/search/meilisearch.ex | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 44af25f3e..ebd3cc81f 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -28,6 +28,14 @@ def run(["index"]) do ]) ) + {:ok, _} = + Pleroma.HTTP.post( + "#{endpoint}/indexes/objects/settings/searchable-attributes", + Jason.encode!([ + "content" + ]) + ) + chunk_size = 10_000 Pleroma.Repo.transaction( @@ -55,8 +63,14 @@ def run(["index"]) do Enum.map(objects, fn object -> data = object.data + content_str = + case data["content"] do + [nil | rest] -> to_string(rest) + str -> str + end + {:ok, published, _} = DateTime.from_iso8601(data["published"]) - {:ok, content} = FastSanitize.strip_tags(data["content"]) + {:ok, content} = FastSanitize.strip_tags(content_str) %{ id: object.id, From 3dedadf192a3acd0c1dfc2b11eba5a247ae7f61c Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 22 Aug 2021 19:38:03 +0300 Subject: [PATCH 011/271] Adjust content indexing to skip more unneeded stuff --- lib/mix/tasks/pleroma/search/meilisearch.ex | 45 ++++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index ebd3cc81f..3704e0bdc 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -52,13 +52,6 @@ def run(["index"]) do timeout: :infinity ) |> Stream.chunk_every(chunk_size) - |> Stream.transform(0, fn objects, acc -> - new_acc = acc + Enum.count(objects) - - IO.puts("Indexed #{new_acc} entries") - - {[objects], new_acc} - end) |> Stream.map(fn objects -> Enum.map(objects, fn object -> data = object.data @@ -70,15 +63,34 @@ def run(["index"]) do end {:ok, published, _} = DateTime.from_iso8601(data["published"]) - {:ok, content} = FastSanitize.strip_tags(content_str) - %{ - id: object.id, - content: content, - ap: data["id"], - published: published |> DateTime.to_unix() - } + content = + with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str), + trimmed <- String.trim(scrubbed) do + trimmed + end + + # Only index if there is anything in the string. If there is a single symbol, + # it's probably a dot from mastodon posts with just the picture + if String.length(content) > 1 do + %{ + id: object.id, + content: content, + ap: data["id"], + published: published |> DateTime.to_unix() + } + else + nil + end end) + |> Enum.filter(fn o -> not is_nil(o) end) + end) + |> Stream.transform(0, fn objects, acc -> + new_acc = acc + Enum.count(objects) + + IO.puts("Indexed #{new_acc} entries") + + {[objects], new_acc} end) |> Stream.each(fn objects -> {:ok, result} = @@ -102,6 +114,9 @@ def run(["clear"]) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, _} = Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects", "", [], []) + {:ok, _} = + Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects/documents", "", [], + timeout: :infinity + ) end end From 35e9192cedcbc56fb07c9933e2988bf900256b53 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 22 Aug 2021 22:53:18 +0300 Subject: [PATCH 012/271] Rework task indexing to share code with the main module The code in the main module now scrubs new posts too --- lib/mix/tasks/pleroma/search/meilisearch.ex | 35 +--------------- lib/pleroma/search/meilisearch.ex | 46 ++++++++++++++------- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 3704e0bdc..b5a394e34 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -51,40 +51,9 @@ def run(["index"]) do ), timeout: :infinity ) + |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1) + |> Stream.filter(fn o -> not is_nil(o) end) |> Stream.chunk_every(chunk_size) - |> Stream.map(fn objects -> - Enum.map(objects, fn object -> - data = object.data - - content_str = - case data["content"] do - [nil | rest] -> to_string(rest) - str -> str - end - - {:ok, published, _} = DateTime.from_iso8601(data["published"]) - - content = - with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str), - trimmed <- String.trim(scrubbed) do - trimmed - end - - # Only index if there is anything in the string. If there is a single symbol, - # it's probably a dot from mastodon posts with just the picture - if String.length(content) > 1 do - %{ - id: object.id, - content: content, - ap: data["id"], - published: published |> DateTime.to_unix() - } - else - nil - end - end) - |> Enum.filter(fn o -> not is_nil(o) end) - end) |> Stream.transform(0, fn objects, acc -> new_acc = acc + Enum.count(objects) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 87fdeaf5e..10468e36c 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -39,28 +39,46 @@ def search(user, query, options \\ []) do end end - def add_to_index(activity) do - object = activity.object - - if activity.data["type"] == "Create" and not is_nil(object) and object.data["type"] == "Note" and + def object_to_search_data(object) do + if not is_nil(object) and object.data["type"] == "Note" and Pleroma.Constants.as_public() in object.data["to"] do data = object.data - endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + content_str = + case data["content"] do + [nil | rest] -> to_string(rest) + str -> str + end - {:ok, published, _} = DateTime.from_iso8601(data["published"]) + content = + with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str), + trimmed <- String.trim(scrubbed) do + trimmed + end + + if String.length(content) > 1 do + {:ok, published, _} = DateTime.from_iso8601(data["published"]) + + %{ + id: object.id, + content: content, + ap: data["id"], + published: published |> DateTime.to_unix() + } + end + end + end + + def add_to_index(activity) do + maybe_search_data = object_to_search_data(activity) + + if activity.data["type"] == "Create" and maybe_search_data do + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) {:ok, result} = Pleroma.HTTP.post( "#{endpoint}/indexes/objects/documents", - Jason.encode!([ - %{ - id: object.id, - content: data["content"] |> Pleroma.HTML.filter_tags(), - ap: data["id"], - published: published |> DateTime.to_unix() - } - ]) + Jason.encode!([maybe_search_data]) ) if not Map.has_key?(Jason.decode!(result.body), "updateId") do From 410c8cb765bbec1014cb2bbdbcc44d3a25f834e1 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 22 Aug 2021 23:47:43 +0300 Subject: [PATCH 013/271] Make indexing logs rewrite themselves --- lib/mix/tasks/pleroma/search/meilisearch.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index b5a394e34..2485a441d 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -57,7 +57,9 @@ def run(["index"]) do |> Stream.transform(0, fn objects, acc -> new_acc = acc + Enum.count(objects) - IO.puts("Indexed #{new_acc} entries") + # Reset to the beginning of the line and rewrite it + IO.write("\r") + IO.write("Indexed #{new_acc} entries") {[objects], new_acc} end) @@ -76,6 +78,8 @@ def run(["index"]) do end, timeout: :infinity ) + + IO.write("\n") end def run(["clear"]) do From 2c7d973af7797ae860829c1764ade521a17e7263 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 19:35:21 +0300 Subject: [PATCH 014/271] Implement meilisearch auth --- lib/mix/tasks/pleroma/search/meilisearch.ex | 75 ++++++++++++--------- lib/pleroma/search/meilisearch.ex | 69 ++++++++++++------- 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 2485a441d..230be5aa1 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -9,32 +9,30 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do import Mix.Pleroma import Ecto.Query + import Pleroma.Search.Meilisearch, only: [meili_post!: 2, meili_delete!: 1] + def run(["index"]) do start_pleroma() - endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + meili_post!( + "/indexes/objects/settings/ranking-rules", + [ + "desc(published)", + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness" + ] + ) - {:ok, _} = - Pleroma.HTTP.post( - "#{endpoint}/indexes/objects/settings/ranking-rules", - Jason.encode!([ - "desc(published)", - "typo", - "words", - "proximity", - "attribute", - "wordsPosition", - "exactness" - ]) - ) - - {:ok, _} = - Pleroma.HTTP.post( - "#{endpoint}/indexes/objects/settings/searchable-attributes", - Jason.encode!([ - "content" - ]) - ) + meili_post!( + "/indexes/objects/settings/searchable-attributes", + [ + "content" + ] + ) chunk_size = 10_000 @@ -64,14 +62,14 @@ def run(["index"]) do {[objects], new_acc} end) |> Stream.each(fn objects -> - {:ok, result} = - Pleroma.HTTP.post( - "#{endpoint}/indexes/objects/documents", - Jason.encode!(objects) + result = + meili_post!( + "/indexes/objects/documents", + objects ) - if not Map.has_key?(Jason.decode!(result.body), "updateId") do - IO.puts("Failed to index: #{result}") + if not Map.has_key?(result, "updateId") do + IO.puts("Failed to index: #{inspect(result)}") end end) |> Stream.run() @@ -85,11 +83,26 @@ def run(["index"]) do def run(["clear"]) do start_pleroma() + meili_delete!("/indexes/objects/documents") + end + + def run(["show-private-key", master_key]) do + start_pleroma() + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, _} = - Pleroma.HTTP.request(:delete, "#{endpoint}/indexes/objects/documents", "", [], - timeout: :infinity + {:ok, result} = + Pleroma.HTTP.get( + Path.join(endpoint, "/keys"), + [{"X-Meili-API-Key", master_key}] ) + + decoded = Jason.decode!(result.body) + + if decoded["private"] do + IO.puts(decoded["private"]) + else + IO.puts("Error fetching the key, check the master key is correct: #{inspect(decoded)}") + end end end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 10468e36c..8745d539d 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -7,20 +7,50 @@ defmodule Pleroma.Search.Meilisearch do import Pleroma.Activity.Search import Ecto.Query + defp meili_headers() do + private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key]) + + if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}] + end + + def meili_post!(path, params) do + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, result} = + Pleroma.HTTP.post( + Path.join(endpoint, path), + Jason.encode!(params), + meili_headers() + ) + + Jason.decode!(result.body) + end + + def meili_delete!(path) do + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, _} = + Pleroma.HTTP.request( + :delete, + Path.join(endpoint, path), + "", + meili_headers(), + timeout: :infinity + ) + end + def search(user, query, options \\ []) do limit = Enum.min([Keyword.get(options, :limit), 40]) offset = Keyword.get(options, :offset, 0) author = Keyword.get(options, :author) - endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - - {:ok, result} = - Pleroma.HTTP.post( - "#{endpoint}/indexes/objects/search", - Jason.encode!(%{q: query, offset: offset, limit: limit}) + result = + meili_post!( + "/indexes/objects/search", + %{q: query, offset: offset, limit: limit} ) - hits = Jason.decode!(result.body)["hits"] |> Enum.map(& &1["ap"]) + hits = result["hits"] |> Enum.map(& &1["ap"]) try do hits @@ -73,30 +103,19 @@ def add_to_index(activity) do maybe_search_data = object_to_search_data(activity) if activity.data["type"] == "Create" and maybe_search_data do - endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - - {:ok, result} = - Pleroma.HTTP.post( - "#{endpoint}/indexes/objects/documents", - Jason.encode!([maybe_search_data]) + result = + meili_post!( + "/indexes/objects/documents", + [maybe_search_data] ) - if not Map.has_key?(Jason.decode!(result.body), "updateId") do - Logger.error("Failed to add activity #{activity.id} to index: #{result.body}") + if not Map.has_key?(result, "updateId") do + Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") end end end def remove_from_index(object) do - endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - - {:ok, _} = - Pleroma.HTTP.request( - :delete, - "#{endpoint}/indexes/objects/documents/#{object.id}", - "", - [], - [] - ) + meili_delete!("/indexes/objects/documents/#{object.id}") end end From a67f9da5cc46b4e184aa1afe3dd1bd1df31de15b Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 20:02:34 +0300 Subject: [PATCH 015/271] Add a message with a count of posts to index --- lib/mix/tasks/pleroma/search/meilisearch.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 230be5aa1..557b06182 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -38,7 +38,7 @@ def run(["index"]) do Pleroma.Repo.transaction( fn -> - Pleroma.Repo.stream( + query = from(Pleroma.Object, # Only index public posts which are notes and have some text where: @@ -46,7 +46,13 @@ def run(["index"]) do fragment("LENGTH(data->>'content') > 0") and fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()), order_by: [desc: fragment("data->'published'")] - ), + ) + + count = query |> Pleroma.Repo.aggregate(:count, :data) + IO.puts("Entries to index: #{count}") + + Pleroma.Repo.stream( + query, timeout: :infinity ) |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1) From 09a1ae1b6eca4efbb935aa1c0da950009d110fb2 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 20:21:46 +0300 Subject: [PATCH 016/271] Add the meilisearch.stats command --- lib/mix/tasks/pleroma/search/meilisearch.ex | 10 +++++++++- lib/pleroma/search/meilisearch.ex | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 557b06182..f2d9fe312 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -9,7 +9,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do import Mix.Pleroma import Ecto.Query - import Pleroma.Search.Meilisearch, only: [meili_post!: 2, meili_delete!: 1] + import Pleroma.Search.Meilisearch, only: [meili_post!: 2, meili_delete!: 1, meili_get!: 1] def run(["index"]) do start_pleroma() @@ -111,4 +111,12 @@ def run(["show-private-key", master_key]) do IO.puts("Error fetching the key, check the master key is correct: #{inspect(decoded)}") end end + + def run(["stats"]) do + start_pleroma() + + result = meili_get!("/indexes/objects/stats") + IO.puts("Number of entries: #{result["numberOfDocuments"]}") + IO.puts("Indexing? #{result["isIndexing"]}") + end end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 8745d539d..1ad17bf9f 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -13,6 +13,18 @@ defp meili_headers() do if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}] end + def meili_get!(path) do + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + {:ok, result} = + Pleroma.HTTP.get( + Path.join(endpoint, path), + meili_headers() + ) + + Jason.decode!(result.body) + end + def meili_post!(path, params) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) From 07ccab9766a6289326676a4814537564f25f35fa Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 20:27:16 +0300 Subject: [PATCH 017/271] Add search/meilisearch documentation --- docs/configuration/search.md | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/configuration/search.md diff --git a/docs/configuration/search.md b/docs/configuration/search.md new file mode 100644 index 000000000..14ec2bc63 --- /dev/null +++ b/docs/configuration/search.md @@ -0,0 +1,99 @@ +# Configuring search + +{! backend/administration/CLI_tasks/general_cli_task_info.include !} + +## Built-in search + +To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`: + +> config :pleroma, Pleroma.Search, module: Pleroma.Activity + +While it has no external dependencies, it has problems with performance and relevancy. + +## Meilisearch + +To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`: + +> config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch + +You then need to set the address of the meilisearch instance, and optionally the private key for authentication. + +> config :pleroma, Pleroma.Search.Meilisearch, +> url: "http://127.0.0.1:7700/", +> private_key: "private key" + +Information about setting up meilisearch can be found in the +[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html). +You probably want to start it with `MEILI_NO_ANALYTICS=true` and `MEILI_NO_CENTRY=true` environment variables, +to disable analytics. + +### Private key authentication (optional) + +To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_, +you have to get the _private key_, which is actually used for authentication. + +=== "OTP" + ```sh + ./bin/pleroma_ctl search.meilisearch show-private-key + ``` + +=== "From Source" + ```sh + mix pleroma.search.meilisearch show-private-key + ``` + +This is the key you actually put into your configuration file. + +### Initial indexing + +After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed. You'll only +have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM +consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2 +million posts while idle and up to 7G while indexing initially, but your experience may be different). + +To start te initial indexing, run the `index` command: + +=== "OTP" + ```sh + ./bin/pleroma_ctl search.meilisearch index + ``` + +=== "From Source" + ```sh + mix pleroma.search.meilisearch index + ``` + +This will show you the total amount of posts to index, and then show you the amount of posts indexed currently, until the numbers eventually +become the same. The posts are indexed in big batches and meilisearch will take some time to actually index them, even after you have +inserted all the posts into it. Depending on the amount of posts, this may be as long as several hours. To get information about the status +of indexing and how many posts have actually been indexed, use the `stats` command: + +=== "OTP" + ```sh + ./bin/pleroma_ctl search.meilisearch stats + ``` + +=== "From Source" + ```sh + mix pleroma.search.meilisearch stats + ``` + +### Clearing the index + +In case you need to clear the index (for example, to re-index from scratch, if that needs to happen for some reason), you can +use the `clear` command: + +=== "OTP" + ```sh + ./bin/pleroma_ctl search.meilisearch clear + ``` + +=== "From Source" + ```sh + mix pleroma.search.meilisearch clear + ``` + +This will clear **all** the posts from the search index. Note, that deleted posts are also removed from index by the instance itself, so +there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information +that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size +depends on the amount of text in posts. From d9ef7e075880ba39dd4ca8e21566c680070faa42 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 21:15:15 +0300 Subject: [PATCH 018/271] Fix activity being passed to objec_to_search_data --- lib/pleroma/search/meilisearch.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 1ad17bf9f..212bdd473 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -112,7 +112,7 @@ def object_to_search_data(object) do end def add_to_index(activity) do - maybe_search_data = object_to_search_data(activity) + maybe_search_data = object_to_search_data(activity.object) if activity.data["type"] == "Create" and maybe_search_data do result = From 005947e9f77b40d1b6dd6c05f952df6ecb2aa1fc Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 23:52:21 +0300 Subject: [PATCH 019/271] Add tests for local post indexing for meilisearch --- config/test.exs | 4 +- test/pleroma/search/meilisearch_test.exs | 108 +++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 test/pleroma/search/meilisearch_test.exs diff --git a/config/test.exs b/config/test.exs index d1c356f14..c9b2b51ba 100644 --- a/config/test.exs +++ b/config/test.exs @@ -133,7 +133,9 @@ ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, logger: Pleroma.LoggerMock -config :pleroma, Pleroma.Search, module: Pleroma.Activity.Search +config :pleroma, Pleroma.Search, module: Pleroma.Activity + +config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil # Reduce recompilation time # https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs new file mode 100644 index 000000000..6e13c8edf --- /dev/null +++ b/test/pleroma/search/meilisearch_test.exs @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.MeilisearchTest do + require Pleroma.Constants + + use Pleroma.DataCase + + import Pleroma.Factory + import Tesla.Mock + import Mock + + alias Pleroma.Web.CommonAPI + alias Pleroma.Search.Meilisearch + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "meilisearch" do + setup do: clear_config([Pleroma.Search, :module], Meilisearch) + + setup_with_mocks( + [ + {Meilisearch, [:passthrough], + [ + add_to_index: fn a -> passthrough([a]) end, + remove_from_index: fn a -> passthrough([a]) end + ]} + ], + context, + do: {:ok, context} + ) + + test "indexes a local post on creation" do + user = insert(:user) + + mock_global(fn + %{method: :post, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} -> + assert match?( + [%{"content" => "guys i just don't wanna leave the swamp"}], + Jason.decode!(body) + ) + + json(%{updateId: 1}) + end) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: "public" + }) + + assert_called(Meilisearch.add_to_index(activity)) + end + + test "doesn't index posts that are not public" do + user = insert(:user) + + Enum.each(["unlisted", "private", "direct"], fn visiblity -> + {:ok, _} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: visiblity + }) + end) + + history = call_history(Meilisearch) + assert Enum.count(history) == 3 + + Enum.each(history, fn {_, _, return} -> + assert is_nil(return) + end) + end + + test "deletes posts from index when deleted locally" do + user = insert(:user) + + mock_global(fn + %{method: :post, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} -> + assert match?( + [%{"content" => "guys i just don't wanna leave the swamp"}], + Jason.decode!(body) + ) + + json(%{updateId: 1}) + + %{method: :delete, url: "http://127.0.0.1:7700/indexes/objects/documents/" <> id} -> + assert String.length(id) > 1 + json(%{updateId: 2}) + end) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: "public" + }) + + assert_called(Meilisearch.add_to_index(activity)) + + {:ok, _} = CommonAPI.delete(activity.id, user) + + assert_called(Meilisearch.remove_from_index(:_)) + end + end +end From a5bb7f9345ff73469c0d776bce5455ec4f27b4ee Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 23 Aug 2021 23:52:37 +0300 Subject: [PATCH 020/271] Add private_key: nil to default meilisearch options --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 1df7dd44b..711775982 100644 --- a/config/config.exs +++ b/config/config.exs @@ -872,7 +872,7 @@ config :pleroma, Pleroma.Search, module: Pleroma.Activity.Search -config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/" +config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. From 40280cc273ad7f2b355846e2f41b9873a8d5ff2c Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 28 Aug 2021 15:59:13 +0300 Subject: [PATCH 021/271] Reorder ranking rules for (maybe) better results --- lib/mix/tasks/pleroma/search/meilisearch.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index f2d9fe312..cdf9ab0bd 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -18,12 +18,12 @@ def run(["index"]) do "/indexes/objects/settings/ranking-rules", [ "desc(published)", - "typo", "words", + "exactness", "proximity", - "attribute", "wordsPosition", - "exactness" + "typo", + "attribute" ] ) From 6beef2d1179ab9a377e87872b7fbe2997bbbbebd Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Fri, 8 Oct 2021 12:24:37 +0300 Subject: [PATCH 022/271] Move add_to_index / remove_from_index to Pleroma.Actitivy.Search --- lib/pleroma/activity.ex | 2 -- lib/pleroma/activity/search.ex | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 2c168fd41..ebfd4ed45 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -367,8 +367,6 @@ def restrict_deactivated_users(query) do end defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search - def add_to_index(_activity), do: nil - def remove_from_index(_object), do: nil def direct_conversation_id(activity, for_user) do alias Pleroma.Conversation.Participation diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index 3dce9d355..47ab5208c 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -45,6 +45,9 @@ def search(user, search_query, options \\ []) do end end + def add_to_index(_activity), do: nil + def remove_from_index(_object), do: nil + def maybe_restrict_author(query, %User{} = author) do Activity.Queries.by_author(query, author) end From c569ad05b3d812c87171e68eac79eec749321033 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 12 Oct 2021 19:14:39 +0300 Subject: [PATCH 023/271] Add more documentation about rum to meilisearch docs --- docs/configuration/search.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/configuration/search.md b/docs/configuration/search.md index 14ec2bc63..e9743f1a4 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -12,6 +12,15 @@ While it has no external dependencies, it has problems with performance and rele ## Meilisearch +Note that it's quite a bit more memory hungry than PostgreSQL (around 4-5G for ~1.2 million +posts while idle and up to 7G while indexing initially). The disk usage for this additional index is also +around 4 gigabytes. Like [RUM](./cheatsheet.md#rum-indexing-for-full-text-search) indexes, it offers considerably +higher performance and ordering by timestamp in a reasonable amount of time. +Additionally, the search results seem to be more accurate. + +Due to high memory usage, it may be best to set it up on a different machine, if running pleroma on a low-resource +computer, and use private key authentication to secure the remote search instance. + To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`: > config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch From 95cb2bb694e3f8857895b21331b02b9277d65d9b Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 12 Oct 2021 19:17:37 +0300 Subject: [PATCH 024/271] Don't try removing from index again in common_api It's already removed in the side effects of the pipeline --- lib/pleroma/web/common_api.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index ba6c07975..89f5dd606 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -147,9 +147,6 @@ def delete(activity_id, user) do true <- User.superuser?(user) || user.ap_id == object.data["actor"], {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do - # Remove from search index for local posts - Pleroma.Search.remove_from_index(object) - {:ok, delete} else {:find_activity, _} -> From cf558208c202d5188954e26077d35bcc1ae02fce Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 12 Oct 2021 19:34:57 +0300 Subject: [PATCH 025/271] Use proper deleted object for removing from index --- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 15e006b18..4762b5ac6 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -330,7 +330,7 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, if result == :ok do Notification.create_notifications(object) - Pleroma.Search.remove_from_index(object) + Pleroma.Search.remove_from_index(deleted_object) {:ok, object, meta} else From e4b7a3f51f270f468c15cc4ce850c847633c030b Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Fri, 29 Oct 2021 00:38:00 +0300 Subject: [PATCH 026/271] Modify some meilisearch variables --- config/config.exs | 2 +- lib/pleroma/search/meilisearch.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 711775982..b55dd94cb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -867,7 +867,7 @@ config :pleroma, ConcurrentLimiter, [ {Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]}, {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}, - {Pleroma.Search, [max_running: 20, max_waiting: 50]} + {Pleroma.Search, [max_running: 30, max_waiting: 50]} ] config :pleroma, Pleroma.Search, module: Pleroma.Activity.Search diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 212bdd473..b8248e40c 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -47,7 +47,7 @@ def meili_delete!(path) do Path.join(endpoint, path), "", meili_headers(), - timeout: :infinity + [] ) end From 0b4fd0d342e3ced073e82355b380cbfee5478c60 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Fri, 29 Oct 2021 13:58:24 +0300 Subject: [PATCH 027/271] Set content-type to application/json --- lib/pleroma/search/meilisearch.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index b8248e40c..d94ab8b64 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -10,7 +10,8 @@ defmodule Pleroma.Search.Meilisearch do defp meili_headers() do private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key]) - if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}] + [{"Content-Type", "application/json"}] ++ + if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}] end def meili_get!(path) do From 4445421297f4a4375ce9df4857a66ad08e984507 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Fri, 29 Oct 2021 21:04:59 +0300 Subject: [PATCH 028/271] Only add local posts to index in activity_pub Remote ones are already added in another place --- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7178cf9eb..cdc70aacf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -141,7 +141,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when end) # Add local posts to search index - Pleroma.Search.add_to_index(activity) + if local, do: Pleroma.Search.add_to_index(activity) {:ok, activity} else From e928e307f34542b0a0af8b615c986aeac478b637 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Wed, 10 Nov 2021 21:25:12 +0300 Subject: [PATCH 029/271] Add a reindex option Signed-off-by: Ekaterina Vaartis --- lib/mix/tasks/pleroma/search/meilisearch.ex | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index cdf9ab0bd..2a3c3a8b9 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -11,9 +11,11 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do import Pleroma.Search.Meilisearch, only: [meili_post!: 2, meili_delete!: 1, meili_get!: 1] - def run(["index"]) do + def run(["index" | args]) do start_pleroma() + is_reindex = "--reindex" in args + meili_post!( "/indexes/objects/settings/ranking-rules", [ @@ -68,6 +70,19 @@ def run(["index"]) do {[objects], new_acc} end) |> Stream.each(fn objects -> + objects = + objects + |> Enum.filter(fn o -> + if is_reindex do + result = meili_get!("/indexes/objects/documents/#{o.id}") + + # Filter out the already indexed documents. This is true when the document does not exist + result["errorCode"] == "document_not_found" + else + true + end + end) + result = meili_post!( "/indexes/objects/documents", From 9c1a9307079c8d007ae7cbf3e089d2bc5ea6b733 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 13 Nov 2021 15:07:51 +0300 Subject: [PATCH 030/271] Support reindexing meilisearch >=0.24.0 It has has a different error code key --- lib/mix/tasks/pleroma/search/meilisearch.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 2a3c3a8b9..3b134ad3f 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -76,8 +76,14 @@ def run(["index" | args]) do if is_reindex do result = meili_get!("/indexes/objects/documents/#{o.id}") + # With >= 0.24.0 the name for "errorCode" is just "code" + error_code_key = + if meili_get!("/version")["pkgVersion"] |> Version.match?(">= 0.24.0"), + do: "code", + else: "errorCode" + # Filter out the already indexed documents. This is true when the document does not exist - result["errorCode"] == "document_not_found" + result[error_code_key] == "document_not_found" else true end From 8898b5e927bae27a521e4eadd0faf970ad27c5bc Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 14 Nov 2021 20:15:12 +0300 Subject: [PATCH 031/271] Fix a typo in search docs --- docs/configuration/search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/search.md b/docs/configuration/search.md index e9743f1a4..9adc7884f 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -60,7 +60,7 @@ have to do it one time, but it might take a while, depending on the amount of po consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2 million posts while idle and up to 7G while indexing initially, but your experience may be different). -To start te initial indexing, run the `index` command: +To start the initial indexing, run the `index` command: === "OTP" ```sh From 7009ef5672ad20f92374d218cd614a38cd70515e Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 14 Nov 2021 20:24:05 +0300 Subject: [PATCH 032/271] Move the search.ex file so credo doesn't complain --- lib/pleroma/{search => }/search.ex | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/pleroma/{search => }/search.ex (100%) diff --git a/lib/pleroma/search/search.ex b/lib/pleroma/search.ex similarity index 100% rename from lib/pleroma/search/search.ex rename to lib/pleroma/search.ex From 39e596a5b51c0c86b6d6bd5f23177a1e6a64cf0b Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sun, 14 Nov 2021 21:42:18 +0300 Subject: [PATCH 033/271] Style fixes --- lib/mix/tasks/pleroma/search/meilisearch.ex | 3 ++- lib/pleroma/search/meilisearch.ex | 2 +- test/pleroma/search/meilisearch_test.exs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 3b134ad3f..62ace7e39 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -82,7 +82,8 @@ def run(["index" | args]) do do: "code", else: "errorCode" - # Filter out the already indexed documents. This is true when the document does not exist + # Filter out the already indexed documents. + # This is true when the document does not exist result[error_code_key] == "document_not_found" else true diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index d94ab8b64..41f99ad9f 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Search.Meilisearch do import Pleroma.Activity.Search import Ecto.Query - defp meili_headers() do + defp meili_headers do private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key]) [{"Content-Type", "application/json"}] ++ diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs index 6e13c8edf..251388ea2 100644 --- a/test/pleroma/search/meilisearch_test.exs +++ b/test/pleroma/search/meilisearch_test.exs @@ -11,8 +11,8 @@ defmodule Pleroma.Search.MeilisearchTest do import Tesla.Mock import Mock - alias Pleroma.Web.CommonAPI alias Pleroma.Search.Meilisearch + alias Pleroma.Web.CommonAPI setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) From 0fae71f88d142f64ec18a49ff4292db816dacdc8 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 16 Nov 2021 21:54:26 +0300 Subject: [PATCH 034/271] Rename search.ex to database_search.ex and add search/2 --- lib/pleroma/{search.ex => search/database_search.ex} | 8 +++++++- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/activity_pub/side_effects.ex | 4 ++-- .../web/mastodon_api/controllers/search_controller.ex | 4 +--- 4 files changed, 11 insertions(+), 7 deletions(-) rename lib/pleroma/{search.ex => search/database_search.ex} (68%) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search/database_search.ex similarity index 68% rename from lib/pleroma/search.ex rename to lib/pleroma/search/database_search.ex index e363abf19..be0e19be0 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search/database_search.ex @@ -1,4 +1,4 @@ -defmodule Pleroma.Search do +defmodule Pleroma.Search.DatabaseSearch do def add_to_index(activity) do search_module = Pleroma.Config.get([Pleroma.Search, :module]) @@ -15,4 +15,10 @@ def remove_from_index(object) do Task.start(fn -> search_module.remove_from_index(object) end) end) end + + def search(query, options) do + search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity) + + search_module.search(options[:for_user], query, options) + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index cdc70aacf..7e3444676 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -141,7 +141,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when end) # Add local posts to search index - if local, do: Pleroma.Search.add_to_index(activity) + if local, do: Pleroma.Search.DatabaseSearch.add_to_index(activity) {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 4762b5ac6..fa57eab69 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -227,7 +227,7 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) end) - Pleroma.Search.add_to_index(Map.put(activity, :object, object)) + Pleroma.Search.DatabaseSearch.add_to_index(Map.put(activity, :object, object)) meta = meta @@ -330,7 +330,7 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, if result == :ok do Notification.create_notifications(object) - Pleroma.Search.remove_from_index(deleted_object) + Pleroma.Search.DatabaseSearch.remove_from_index(deleted_object) {:ok, object, meta} else diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 99c33eba6..10f1aa532 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -99,9 +99,7 @@ defp resource_search(_, "accounts", query, options) do end defp resource_search(_, "statuses", query, options) do - search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity) - - statuses = with_fallback(fn -> search_module.search(options[:for_user], query, options) end) + statuses = with_fallback(fn -> Pleroma.Search.DatabaseSearch.search(query, options) end) StatusView.render("index.json", activities: statuses, From a6946048fbe049aa223d094d36eb767739ab5ff2 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Wed, 17 Nov 2021 22:29:49 +0300 Subject: [PATCH 035/271] Rename Activity.Search to Search.DatabaseSearch --- config/config.exs | 2 +- config/test.exs | 2 +- docs/configuration/search.md | 2 +- lib/pleroma/activity.ex | 2 +- lib/pleroma/activity/search.ex | 165 ------------------ lib/pleroma/search.ex | 24 +++ lib/pleroma/search/database_search.ex | 157 +++++++++++++++-- lib/pleroma/search/meilisearch.ex | 2 +- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/activity_pub/side_effects.ex | 4 +- .../controllers/search_controller.ex | 2 +- .../database_search_test.ex} | 10 +- 12 files changed, 181 insertions(+), 193 deletions(-) delete mode 100644 lib/pleroma/activity/search.ex create mode 100644 lib/pleroma/search.ex rename test/pleroma/{activity/search_test.exs => search/database_search_test.ex} (86%) diff --git a/config/config.exs b/config/config.exs index b55dd94cb..f7f3a1454 100644 --- a/config/config.exs +++ b/config/config.exs @@ -870,7 +870,7 @@ {Pleroma.Search, [max_running: 30, max_waiting: 50]} ] -config :pleroma, Pleroma.Search, module: Pleroma.Activity.Search +config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil diff --git a/config/test.exs b/config/test.exs index c9b2b51ba..ea29be638 100644 --- a/config/test.exs +++ b/config/test.exs @@ -133,7 +133,7 @@ ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, logger: Pleroma.LoggerMock -config :pleroma, Pleroma.Search, module: Pleroma.Activity +config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil diff --git a/docs/configuration/search.md b/docs/configuration/search.md index 9adc7884f..c7e77d9c2 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -6,7 +6,7 @@ To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`: -> config :pleroma, Pleroma.Search, module: Pleroma.Activity +> config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch While it has no external dependencies, it has problems with performance and relevancy. diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index ebfd4ed45..389c80691 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -366,7 +366,7 @@ def restrict_deactivated_users(query) do from(activity in query, where: activity.actor not in subquery(deactivated_users_query)) end - defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search + defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch def direct_conversation_id(activity, for_user) do alias Pleroma.Conversation.Participation diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex deleted file mode 100644 index 47ab5208c..000000000 --- a/lib/pleroma/activity/search.ex +++ /dev/null @@ -1,165 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Activity.Search do - alias Pleroma.Activity - alias Pleroma.Object.Fetcher - alias Pleroma.Pagination - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Visibility - - require Pleroma.Constants - - import Ecto.Query - - def search(user, search_query, options \\ []) do - index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin - limit = Enum.min([Keyword.get(options, :limit), 40]) - offset = Keyword.get(options, :offset, 0) - author = Keyword.get(options, :author) - - search_function = - if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do - :websearch - else - :plain - end - - try do - Activity - |> Activity.with_preloaded_object() - |> Activity.restrict_deactivated_users() - |> restrict_public(user) - |> query_with(index_type, search_query, search_function) - |> maybe_restrict_local(user) - |> maybe_restrict_author(author) - |> maybe_restrict_blocked(user) - |> Pagination.fetch_paginated( - %{"offset" => offset, "limit" => limit, "skip_order" => index_type == :rum}, - :offset - ) - |> maybe_fetch(user, search_query) - rescue - _ -> maybe_fetch([], user, search_query) - end - end - - def add_to_index(_activity), do: nil - def remove_from_index(_object), do: nil - - def maybe_restrict_author(query, %User{} = author) do - Activity.Queries.by_author(query, author) - end - - def maybe_restrict_author(query, _), do: query - - def maybe_restrict_blocked(query, %User{} = user) do - Activity.Queries.exclude_authors(query, User.blocked_users_ap_ids(user)) - end - - def maybe_restrict_blocked(query, _), do: query - - defp restrict_public(q, user) when not is_nil(user) do - intended_recipients = [ - Pleroma.Constants.as_public(), - Pleroma.Web.ActivityPub.Utils.as_local_public() - ] - - from([a, o] in q, - where: fragment("?->>'type' = 'Create'", a.data), - where: fragment("? && ?", ^intended_recipients, a.recipients) - ) - end - - defp restrict_public(q, _user) do - from([a, o] in q, - where: fragment("?->>'type' = 'Create'", a.data), - where: ^Pleroma.Constants.as_public() in a.recipients - ) - end - - defp query_with(q, :gin, search_query, :plain) do - %{rows: [[tsc]]} = - Ecto.Adapters.SQL.query!( - Pleroma.Repo, - "select current_setting('default_text_search_config')::regconfig::oid;" - ) - - from([a, o] in q, - where: - fragment( - "to_tsvector(?::oid::regconfig, ?->>'content') @@ plainto_tsquery(?)", - ^tsc, - o.data, - ^search_query - ) - ) - end - - defp query_with(q, :gin, search_query, :websearch) do - %{rows: [[tsc]]} = - Ecto.Adapters.SQL.query!( - Pleroma.Repo, - "select current_setting('default_text_search_config')::regconfig::oid;" - ) - - from([a, o] in q, - where: - fragment( - "to_tsvector(?::oid::regconfig, ?->>'content') @@ websearch_to_tsquery(?)", - ^tsc, - o.data, - ^search_query - ) - ) - end - - defp query_with(q, :rum, search_query, :plain) do - from([a, o] in q, - where: - fragment( - "? @@ plainto_tsquery(?)", - o.fts_content, - ^search_query - ), - order_by: [fragment("? <=> now()::date", o.inserted_at)] - ) - end - - defp query_with(q, :rum, search_query, :websearch) do - from([a, o] in q, - where: - fragment( - "? @@ websearch_to_tsquery(?)", - o.fts_content, - ^search_query - ), - order_by: [fragment("? <=> now()::date", o.inserted_at)] - ) - end - - def maybe_restrict_local(q, user) do - limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) - - case {limit, user} do - {:all, _} -> restrict_local(q) - {:unauthenticated, %User{}} -> q - {:unauthenticated, _} -> restrict_local(q) - {false, _} -> q - end - end - - defp restrict_local(q), do: where(q, local: true) - - def maybe_fetch(activities, user, search_query) do - with true <- Regex.match?(~r/https?:/, search_query), - {:ok, object} <- Fetcher.fetch_object_from_id(search_query), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), - true <- Visibility.visible_for_user?(activity, user) do - [activity | activities] - else - _ -> activities - end - end -end diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex new file mode 100644 index 000000000..ae0b28c54 --- /dev/null +++ b/lib/pleroma/search.ex @@ -0,0 +1,24 @@ +defmodule Pleroma.Search do + def add_to_index(activity) do + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + ConcurrentLimiter.limit(Pleroma.Search, fn -> + Task.start(fn -> search_module.add_to_index(activity) end) + end) + end + + def remove_from_index(object) do + # Also delete from search index + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + ConcurrentLimiter.limit(Pleroma.Search, fn -> + Task.start(fn -> search_module.remove_from_index(object) end) + end) + end + + def search(query, options) do + search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity) + + search_module.search(options[:for_user], query, options) + end +end diff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex index be0e19be0..5a8b8ca67 100644 --- a/lib/pleroma/search/database_search.ex +++ b/lib/pleroma/search/database_search.ex @@ -1,24 +1,153 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Search.DatabaseSearch do - def add_to_index(activity) do - search_module = Pleroma.Config.get([Pleroma.Search, :module]) + alias Pleroma.Activity + alias Pleroma.Object.Fetcher + alias Pleroma.Pagination + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility - ConcurrentLimiter.limit(Pleroma.Search, fn -> - Task.start(fn -> search_module.add_to_index(activity) end) - end) + require Pleroma.Constants + + import Ecto.Query + + def search(user, search_query, options \\ []) do + index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin + limit = Enum.min([Keyword.get(options, :limit), 40]) + offset = Keyword.get(options, :offset, 0) + author = Keyword.get(options, :author) + + search_function = + if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do + :websearch + else + :plain + end + + try do + Activity + |> Activity.with_preloaded_object() + |> Activity.restrict_deactivated_users() + |> restrict_public() + |> query_with(index_type, search_query, search_function) + |> maybe_restrict_local(user) + |> maybe_restrict_author(author) + |> maybe_restrict_blocked(user) + |> Pagination.fetch_paginated( + %{"offset" => offset, "limit" => limit, "skip_order" => index_type == :rum}, + :offset + ) + |> maybe_fetch(user, search_query) + rescue + _ -> maybe_fetch([], user, search_query) + end end - def remove_from_index(object) do - # Also delete from search index - search_module = Pleroma.Config.get([Pleroma.Search, :module]) + def add_to_index(_activity), do: nil + def remove_from_index(_object), do: nil - ConcurrentLimiter.limit(Pleroma.Search, fn -> - Task.start(fn -> search_module.remove_from_index(object) end) - end) + def maybe_restrict_author(query, %User{} = author) do + Activity.Queries.by_author(query, author) end - def search(query, options) do - search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity) + def maybe_restrict_author(query, _), do: query - search_module.search(options[:for_user], query, options) + def maybe_restrict_blocked(query, %User{} = user) do + Activity.Queries.exclude_authors(query, User.blocked_users_ap_ids(user)) + end + + def maybe_restrict_blocked(query, _), do: query + + def restrict_public(q) do + from([a, o] in q, + where: fragment("?->>'type' = 'Create'", a.data), + where: ^Pleroma.Constants.as_public() in a.recipients + ) + end + + defp query_with(q, :gin, search_query, :plain) do + %{rows: [[tsc]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select current_setting('default_text_search_config')::regconfig::oid;" + ) + + from([a, o] in q, + where: + fragment( + "to_tsvector(?::oid::regconfig, ?->>'content') @@ plainto_tsquery(?)", + ^tsc, + o.data, + ^search_query + ) + ) + end + + defp query_with(q, :gin, search_query, :websearch) do + %{rows: [[tsc]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select current_setting('default_text_search_config')::regconfig::oid;" + ) + + from([a, o] in q, + where: + fragment( + "to_tsvector(?::oid::regconfig, ?->>'content') @@ websearch_to_tsquery(?)", + ^tsc, + o.data, + ^search_query + ) + ) + end + + defp query_with(q, :rum, search_query, :plain) do + from([a, o] in q, + where: + fragment( + "? @@ plainto_tsquery(?)", + o.fts_content, + ^search_query + ), + order_by: [fragment("? <=> now()::date", o.inserted_at)] + ) + end + + defp query_with(q, :rum, search_query, :websearch) do + from([a, o] in q, + where: + fragment( + "? @@ websearch_to_tsquery(?)", + o.fts_content, + ^search_query + ), + order_by: [fragment("? <=> now()::date", o.inserted_at)] + ) + end + + def maybe_restrict_local(q, user) do + limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) + + case {limit, user} do + {:all, _} -> restrict_local(q) + {:unauthenticated, %User{}} -> q + {:unauthenticated, _} -> restrict_local(q) + {false, _} -> q + end + end + + defp restrict_local(q), do: where(q, local: true) + + def maybe_fetch(activities, user, search_query) do + with true <- Regex.match?(~r/https?:/, search_query), + {:ok, object} <- Fetcher.fetch_object_from_id(search_query), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user) do + [activity | activities] + else + _ -> activities + end end end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 41f99ad9f..fa9e27b03 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Search.Meilisearch do alias Pleroma.Activity - import Pleroma.Activity.Search + import Pleroma.Search.DatabaseSearch import Ecto.Query defp meili_headers do diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7e3444676..cdc70aacf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -141,7 +141,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when end) # Add local posts to search index - if local, do: Pleroma.Search.DatabaseSearch.add_to_index(activity) + if local, do: Pleroma.Search.add_to_index(activity) {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index fa57eab69..4762b5ac6 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -227,7 +227,7 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) end) - Pleroma.Search.DatabaseSearch.add_to_index(Map.put(activity, :object, object)) + Pleroma.Search.add_to_index(Map.put(activity, :object, object)) meta = meta @@ -330,7 +330,7 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, if result == :ok do Notification.create_notifications(object) - Pleroma.Search.DatabaseSearch.remove_from_index(deleted_object) + Pleroma.Search.remove_from_index(deleted_object) {:ok, object, meta} else diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 10f1aa532..e4acba226 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -99,7 +99,7 @@ defp resource_search(_, "accounts", query, options) do end defp resource_search(_, "statuses", query, options) do - statuses = with_fallback(fn -> Pleroma.Search.DatabaseSearch.search(query, options) end) + statuses = with_fallback(fn -> Pleroma.Search.search(query, options) end) StatusView.render("index.json", activities: statuses, diff --git a/test/pleroma/activity/search_test.exs b/test/pleroma/search/database_search_test.ex similarity index 86% rename from test/pleroma/activity/search_test.exs rename to test/pleroma/search/database_search_test.ex index 3b5fd2c3c..c123d0b84 100644 --- a/test/pleroma/activity/search_test.exs +++ b/test/pleroma/search/database_search_test.ex @@ -2,8 +2,8 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Activity.SearchTest do - alias Pleroma.Activity.Search +defmodule Pleroma.Search.DatabaseSearchTest do + alias Pleroma.Search.DatabaseSearch alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -13,7 +13,7 @@ test "it finds something" do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"}) - [result] = Search.search(nil, "wednesday") + [result] = DatabaseSearch.search(nil, "wednesday") assert result.id == post.id end @@ -45,7 +45,7 @@ test "using plainto_tsquery on postgres < 11" do {:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"}) # plainto doesn't understand complex queries - assert [result] = Search.search(nil, "wednesday -dudes") + assert [result] = DatabaseSearch.search(nil, "wednesday -dudes") assert result.id == post.id end @@ -55,7 +55,7 @@ test "using websearch_to_tsquery" do {:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"}) {:ok, other_post} = CommonAPI.post(user, %{status: "it's wednesday my bros"}) - assert [result] = Search.search(nil, "wednesday -dudes") + assert [result] = DatabaseSearch.search(nil, "wednesday -dudes") assert result.id == other_post.id end From a12f63bc81481e3f852934e8cc1269e16a57cf0a Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 22 Nov 2021 21:39:54 +0300 Subject: [PATCH 036/271] Implement suggestions from the Meilisearch MR - Index unlisted posts - Move version check outside of the streaming and only do it once - Use a PUT request instead of checking manually if there is need to insert - Add error handling, sort of --- lib/mix/tasks/pleroma/search/meilisearch.ex | 84 +++++++++----------- lib/pleroma/search/meilisearch.ex | 85 ++++++++++++++------- 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 62ace7e39..6730a99a9 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -3,38 +3,40 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.Search.Meilisearch do - require Logger require Pleroma.Constants import Mix.Pleroma import Ecto.Query - import Pleroma.Search.Meilisearch, only: [meili_post!: 2, meili_delete!: 1, meili_get!: 1] + import Pleroma.Search.Meilisearch, + only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1] - def run(["index" | args]) do + def run(["index"]) do start_pleroma() - is_reindex = "--reindex" in args + {:ok, _} = + meili_post( + "/indexes/objects/settings/ranking-rules", + [ + "desc(published)", + "words", + "exactness", + "proximity", + "wordsPosition", + "typo", + "attribute" + ] + ) - meili_post!( - "/indexes/objects/settings/ranking-rules", - [ - "desc(published)", - "words", - "exactness", - "proximity", - "wordsPosition", - "typo", - "attribute" - ] - ) + {:ok, _} = + meili_post( + "/indexes/objects/settings/searchable-attributes", + [ + "content" + ] + ) - meili_post!( - "/indexes/objects/settings/searchable-attributes", - [ - "content" - ] - ) + IO.puts("Created indices. Starting to insert posts.") chunk_size = 10_000 @@ -42,11 +44,11 @@ def run(["index" | args]) do fn -> query = from(Pleroma.Object, - # Only index public posts which are notes and have some text + # Only index public and unlisted posts which are notes and have some text where: fragment("data->>'type' = 'Note'") and - fragment("LENGTH(data->>'content') > 0") and - fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()), + (fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or + fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())), order_by: [desc: fragment("data->'published'")] ) @@ -70,34 +72,18 @@ def run(["index" | args]) do {[objects], new_acc} end) |> Stream.each(fn objects -> - objects = - objects - |> Enum.filter(fn o -> - if is_reindex do - result = meili_get!("/indexes/objects/documents/#{o.id}") - - # With >= 0.24.0 the name for "errorCode" is just "code" - error_code_key = - if meili_get!("/version")["pkgVersion"] |> Version.match?(">= 0.24.0"), - do: "code", - else: "errorCode" - - # Filter out the already indexed documents. - # This is true when the document does not exist - result[error_code_key] == "document_not_found" - else - true - end - end) - result = - meili_post!( + meili_put( "/indexes/objects/documents", objects ) - if not Map.has_key?(result, "updateId") do - IO.puts("Failed to index: #{inspect(result)}") + with {:ok, res} <- result do + if not Map.has_key?(res, "updateId") do + IO.puts("\nFailed to index: #{inspect(result)}") + end + else + e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}") end end) |> Stream.run() @@ -137,7 +123,7 @@ def run(["show-private-key", master_key]) do def run(["stats"]) do start_pleroma() - result = meili_get!("/indexes/objects/stats") + {:ok, result} = meili_get("/indexes/objects/stats") IO.puts("Number of entries: #{result["numberOfDocuments"]}") IO.puts("Indexing? #{result["isIndexing"]}") end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index fa9e27b03..21b44de86 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -14,29 +14,50 @@ defp meili_headers do if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}] end - def meili_get!(path) do + def meili_get(path) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, result} = + result = Pleroma.HTTP.get( Path.join(endpoint, path), meili_headers() ) - Jason.decode!(result.body) + with {:ok, res} <- result do + {:ok, Jason.decode!(res.body)} + end end - def meili_post!(path, params) do + def meili_post(path, params) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, result} = + result = Pleroma.HTTP.post( Path.join(endpoint, path), Jason.encode!(params), meili_headers() ) - Jason.decode!(result.body) + with {:ok, res} <- result do + {:ok, Jason.decode!(res.body)} + end + end + + def meili_put(path, params) do + endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + + result = + Pleroma.HTTP.request( + :put, + Path.join(endpoint, path), + Jason.encode!(params), + meili_headers(), + [] + ) + + with {:ok, res} <- result do + {:ok, Jason.decode!(res.body)} + end end def meili_delete!(path) do @@ -57,34 +78,40 @@ def search(user, query, options \\ []) do offset = Keyword.get(options, :offset, 0) author = Keyword.get(options, :author) - result = - meili_post!( + res = + meili_post( "/indexes/objects/search", %{q: query, offset: offset, limit: limit} ) - hits = result["hits"] |> Enum.map(& &1["ap"]) + with {:ok, result} <- res do + hits = result["hits"] |> Enum.map(& &1["ap"]) - try do - hits - |> Activity.create_by_object_ap_id() - |> Activity.with_preloaded_object() - |> Activity.with_preloaded_object() - |> Activity.restrict_deactivated_users() - |> maybe_restrict_local(user) - |> maybe_restrict_author(author) - |> maybe_restrict_blocked(user) - |> maybe_fetch(user, query) - |> order_by([object: obj], desc: obj.data["published"]) - |> Pleroma.Repo.all() - rescue - _ -> maybe_fetch([], user, query) + try do + hits + |> Activity.create_by_object_ap_id() + |> Activity.with_preloaded_object() + |> Activity.with_preloaded_object() + |> Activity.restrict_deactivated_users() + |> maybe_restrict_local(user) + |> maybe_restrict_author(author) + |> maybe_restrict_blocked(user) + |> maybe_fetch(user, query) + |> order_by([object: obj], desc: obj.data["published"]) + |> Pleroma.Repo.all() + rescue + _ -> maybe_fetch([], user, query) + end end end def object_to_search_data(object) do + # Only index public or unlisted Notes if not is_nil(object) and object.data["type"] == "Note" and - Pleroma.Constants.as_public() in object.data["to"] do + not is_nil(object.data["content"]) and + (Pleroma.Constants.as_public() in object.data["to"] or + Pleroma.Constants.as_public() in object.data["cc"]) and + String.length(object.data["content"]) > 1 do data = object.data content_str = @@ -117,13 +144,17 @@ def add_to_index(activity) do if activity.data["type"] == "Create" and maybe_search_data do result = - meili_post!( + meili_put( "/indexes/objects/documents", [maybe_search_data] ) - if not Map.has_key?(result, "updateId") do - Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") + with {:ok, res} <- result, + true <- Map.has_key?(res, "updateId") do + # Do nothing + else + _ -> + Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") end end end From 3a11e79de0c7092bf4fe0649e4ab1fcb53eb14a3 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 20 Dec 2021 17:46:23 +0300 Subject: [PATCH 037/271] Add config description for meilisearch --- config/description.exs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/config/description.exs b/config/description.exs index 3a2a65272..81fefaf89 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3475,5 +3475,40 @@ ] } ] + }, + %{ + group: :pleroma, + key: Pleroma.Search, + type: :group, + description: "General search settings.", + children: [ + %{ + key: :module, + type: :keyword, + description: "Selected search module.", + suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch] + } + ] + }, + %{ + group: :pleroma, + key: Pleroma.Search.Meilisearch, + type: :group, + description: "Meilisearch settings.", + children: [ + %{ + key: :url, + type: :string, + description: "Meilisearch URL.", + suggestion: ["http://127.0.0.1:7700/"] + }, + %{ + key: :private_key, + type: :string, + description: + "Private key for meilisearch authentication, or `nil` to disable private key authentication.", + suggestion: [nil] + } + ] } ] From 3412713c5b2fd24605b18933ef70de164ee14f2d Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 20 Dec 2021 18:16:33 +0300 Subject: [PATCH 038/271] Update search.md documentation with meilisearch indexing steps --- docs/configuration/search.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/configuration/search.md b/docs/configuration/search.md index c7e77d9c2..7dbbd3e17 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -60,6 +60,15 @@ have to do it one time, but it might take a while, depending on the amount of po consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2 million posts while idle and up to 7G while indexing initially, but your experience may be different). +The sequence of actions is as follows: + +1. First, change the configuration to use `Pleroma.Search.Meilisearch` as the search backend +2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything +3. Start the initial indexing process (as described below with `index`), + and wait until the task says it sent everything from the database to index +4. Wait until everything is actually indexed (by checking with `stats` as described below), + at this point you don't have to do anything, just wait a while. + To start the initial indexing, run the `index` command: === "OTP" From 3179ed0921197a8a8f32a519c7d41dc09011024d Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 20 Dec 2021 18:48:52 +0300 Subject: [PATCH 039/271] Make chunk size configurable --- config/config.exs | 5 ++++- lib/mix/tasks/pleroma/search/meilisearch.ex | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index f7f3a1454..bcbc59b83 100644 --- a/config/config.exs +++ b/config/config.exs @@ -872,7 +872,10 @@ config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch -config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil +config :pleroma, Pleroma.Search.Meilisearch, + url: "http://127.0.0.1:7700/", + private_key: nil, + initial_indexing_chunk_size: 100_000 # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 6730a99a9..021552f7b 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -38,7 +38,7 @@ def run(["index"]) do IO.puts("Created indices. Starting to insert posts.") - chunk_size = 10_000 + chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size]) Pleroma.Repo.transaction( fn -> From 571533ae2618478f26db312e52265e143356debd Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 20 Dec 2021 19:05:59 +0300 Subject: [PATCH 040/271] Don't support meilisearch < 0.24.0, since it breaks things --- lib/mix/tasks/pleroma/search/meilisearch.ex | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 021552f7b..5098668ad 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -14,17 +14,29 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do def run(["index"]) do start_pleroma() + meili_version = + ( + {:ok, result} = meili_get("/version") + + result["pkgVersion"] + ) + + # The ranking rule syntax was changed but nothing about that is mentioned in the changelog + if not Version.match?(meili_version, ">= 0.24.0") do + raise "Meilisearch <0.24.0 not supported" + end + {:ok, _} = meili_post( "/indexes/objects/settings/ranking-rules", [ - "desc(published)", + "published:desc", "words", "exactness", "proximity", - "wordsPosition", "typo", - "attribute" + "attribute", + "sort" ] ) From 4f2637acc6c46ea39ae38e869903e7ffcc38b34d Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 20 Dec 2021 19:27:22 +0300 Subject: [PATCH 041/271] Add description for initial_indexing_chunk_size --- config/description.exs | 8 ++++++++ docs/configuration/search.md | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/config/description.exs b/config/description.exs index 81fefaf89..cea4401ba 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3508,6 +3508,14 @@ description: "Private key for meilisearch authentication, or `nil` to disable private key authentication.", suggestion: [nil] + }, + %{ + key: :initial_indexing_chunk_size, + type: :int, + description: + "Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <> + " since there's a limit on maximum insert size", + suggestion: [100_000] } ] } diff --git a/docs/configuration/search.md b/docs/configuration/search.md index 7dbbd3e17..a785a18ad 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -25,11 +25,15 @@ To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pl > config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch -You then need to set the address of the meilisearch instance, and optionally the private key for authentication. +You then need to set the address of the meilisearch instance, and optionally the private key for authentication. You might +also want to change the `initial_indexing_chunk_size` to be smaller if you're server is not very powerful, but not higher than `100_000`, +because meilisearch will refuse to process it if it's too big. However, in general you want this to be as big as possible, because meilisearch +indexes faster when it can process many posts in a single batch. > config :pleroma, Pleroma.Search.Meilisearch, > url: "http://127.0.0.1:7700/", -> private_key: "private key" +> private_key: "private key", +> initial_indexing_chunk_size: 100_000 Information about setting up meilisearch can be found in the [official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html). From 6f2f457751ea09507045e6dd5d5869a14befd3d1 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Mon, 20 Dec 2021 22:38:50 +0300 Subject: [PATCH 042/271] Add a search backend behaviour --- lib/pleroma/search/database_search.ex | 5 +++++ lib/pleroma/search/meilisearch.ex | 4 ++++ lib/pleroma/search/search_backend.ex | 17 +++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 lib/pleroma/search/search_backend.ex diff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex index 5a8b8ca67..3735a5fab 100644 --- a/lib/pleroma/search/database_search.ex +++ b/lib/pleroma/search/database_search.ex @@ -13,6 +13,8 @@ defmodule Pleroma.Search.DatabaseSearch do import Ecto.Query + @behaviour Pleroma.Search.SearchBackend + def search(user, search_query, options \\ []) do index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin limit = Enum.min([Keyword.get(options, :limit), 40]) @@ -45,7 +47,10 @@ def search(user, search_query, options \\ []) do end end + @impl true def add_to_index(_activity), do: nil + + @impl true def remove_from_index(_object), do: nil def maybe_restrict_author(query, %User{} = author) do diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 21b44de86..33bbf8392 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -7,6 +7,8 @@ defmodule Pleroma.Search.Meilisearch do import Pleroma.Search.DatabaseSearch import Ecto.Query + @behaviour Pleroma.Search.SearchBackend + defp meili_headers do private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key]) @@ -139,6 +141,7 @@ def object_to_search_data(object) do end end + @impl true def add_to_index(activity) do maybe_search_data = object_to_search_data(activity.object) @@ -159,6 +162,7 @@ def add_to_index(activity) do end end + @impl true def remove_from_index(object) do meili_delete!("/indexes/objects/documents/#{object.id}") end diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex new file mode 100644 index 000000000..ed6bfd329 --- /dev/null +++ b/lib/pleroma/search/search_backend.ex @@ -0,0 +1,17 @@ +defmodule Pleroma.Search.SearchBackend do + @doc """ + Add the object associated with the activity to the search index. + + The whole activity is passed, to allow filtering on things such as scope. + """ + @callback add_to_index(activity :: Pleroma.Activity.t()) :: nil + + @doc """ + Remove the object from the index. + + Just the object, as opposed to the whole activity, is passed, since the object + is what contains the actual content and there is no need for fitlering when removing + from index. + """ + @callback remove_from_index(object :: Pleroma.Object.t()) :: nil +end From 2bc21c6f1884bae3226f760ed1da39dd9c5f2958 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 22 Jan 2022 15:23:11 +0300 Subject: [PATCH 043/271] Use oban for search indexing --- config/config.exs | 3 ++- lib/pleroma/search.ex | 15 ++++--------- lib/pleroma/workers/search_indexing_worker.ex | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 lib/pleroma/workers/search_indexing_worker.ex diff --git a/config/config.exs b/config/config.exs index bcbc59b83..3e2f0da3f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -571,7 +571,8 @@ remote_fetcher: 2, attachments_cleanup: 1, new_users_digest: 1, - mute_expire: 5 + mute_expire: 5, + search_indexing: 1 ], plugins: [Oban.Plugins.Pruner], crontab: [ diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index ae0b28c54..af858fc46 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,19 +1,12 @@ defmodule Pleroma.Search do - def add_to_index(activity) do - search_module = Pleroma.Config.get([Pleroma.Search, :module]) + alias Pleroma.Workers.SearchIndexingWorker - ConcurrentLimiter.limit(Pleroma.Search, fn -> - Task.start(fn -> search_module.add_to_index(activity) end) - end) + def add_to_index(activity) do + SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity.id}) end def remove_from_index(object) do - # Also delete from search index - search_module = Pleroma.Config.get([Pleroma.Search, :module]) - - ConcurrentLimiter.limit(Pleroma.Search, fn -> - Task.start(fn -> search_module.remove_from_index(object) end) - end) + SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object.id}) end def search(query, options) do diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex new file mode 100644 index 000000000..43b7bad1e --- /dev/null +++ b/lib/pleroma/workers/search_indexing_worker.ex @@ -0,0 +1,21 @@ +defmodule Pleroma.Workers.SearchIndexingWorker do + use Pleroma.Workers.WorkerHelper, queue: "search_indexing" + + @impl Oban.Worker + + def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do + activity = Pleroma.Activity.get_by_id_with_object(activity_id) + + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + search_module.add_to_index(activity) + end + + def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do + object = Pleroma.Object.get_by_id(object_id) + + search_module = Pleroma.Config.get([Pleroma.Search, :module]) + + search_module.remove_from_index(object) + end +end From d89dc5518b5c0eb232e7ac85ddd538f89c32606d Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 22 Jan 2022 16:31:32 +0300 Subject: [PATCH 044/271] Fix meilisearch tests and jobs for oban --- lib/pleroma/workers/search_indexing_worker.ex | 4 +++ test/pleroma/search/meilisearch_test.exs | 35 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex index 43b7bad1e..70a8d42d0 100644 --- a/lib/pleroma/workers/search_indexing_worker.ex +++ b/lib/pleroma/workers/search_indexing_worker.ex @@ -9,6 +9,8 @@ def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module.add_to_index(activity) + + :ok end def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do @@ -17,5 +19,7 @@ def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) d search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module.remove_from_index(object) + + :ok end end diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs index 251388ea2..da614577f 100644 --- a/test/pleroma/search/meilisearch_test.exs +++ b/test/pleroma/search/meilisearch_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Search.MeilisearchTest do require Pleroma.Constants use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory import Tesla.Mock @@ -13,6 +14,7 @@ defmodule Pleroma.Search.MeilisearchTest do alias Pleroma.Search.Meilisearch alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.SearchIndexingWorker setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -27,7 +29,8 @@ defmodule Pleroma.Search.MeilisearchTest do {Meilisearch, [:passthrough], [ add_to_index: fn a -> passthrough([a]) end, - remove_from_index: fn a -> passthrough([a]) end + remove_from_index: fn a -> passthrough([a]) end, + meili_put: fn u, a -> passthrough([u, a]) end ]} ], context, @@ -38,7 +41,7 @@ test "indexes a local post on creation" do user = insert(:user) mock_global(fn - %{method: :post, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} -> + %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} -> assert match?( [%{"content" => "guys i just don't wanna leave the swamp"}], Jason.decode!(body) @@ -53,6 +56,15 @@ test "indexes a local post on creation" do visibility: "public" }) + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued( + worker: SearchIndexingWorker, + args: args + ) + + assert :ok = perform_job(SearchIndexingWorker, args) + assert_called(Meilisearch.add_to_index(activity)) end @@ -60,26 +72,25 @@ test "doesn't index posts that are not public" do user = insert(:user) Enum.each(["unlisted", "private", "direct"], fn visiblity -> - {:ok, _} = + {:ok, activity} = CommonAPI.post(user, %{ status: "guys i just don't wanna leave the swamp", visibility: visiblity }) + + Meilisearch.add_to_index(activity) + assert_not_called(Meilisearch.meili_put(:_)) end) history = call_history(Meilisearch) assert Enum.count(history) == 3 - - Enum.each(history, fn {_, _, return} -> - assert is_nil(return) - end) end test "deletes posts from index when deleted locally" do user = insert(:user) mock_global(fn - %{method: :post, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} -> + %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} -> assert match?( [%{"content" => "guys i just don't wanna leave the swamp"}], Jason.decode!(body) @@ -98,10 +109,16 @@ test "deletes posts from index when deleted locally" do visibility: "public" }) - assert_called(Meilisearch.add_to_index(activity)) + args = %{"op" => "add_to_index", "activity" => activity.id} + assert_enqueued(worker: SearchIndexingWorker, args: args) + assert :ok = perform_job(SearchIndexingWorker, args) {:ok, _} = CommonAPI.delete(activity.id, user) + delete_args = %{"op" => "remove_from_index", "object" => activity.object.id} + assert_enqueued(worker: SearchIndexingWorker, args: delete_args) + assert :ok = perform_job(SearchIndexingWorker, delete_args) + assert_called(Meilisearch.remove_from_index(:_)) end end From 3387935e8354e32171fe6e28a8f96f49154acbb3 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 22 Jan 2022 16:52:06 +0300 Subject: [PATCH 045/271] Don't try removing deleted users and such from index as posts --- lib/pleroma/search.ex | 8 ++++---- lib/pleroma/web/activity_pub/side_effects.ex | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index af858fc46..3b266e59b 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,12 +1,12 @@ defmodule Pleroma.Search do alias Pleroma.Workers.SearchIndexingWorker - def add_to_index(activity) do - SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity.id}) + def add_to_index(%Pleroma.Activity{id: activity_id}) do + SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) end - def remove_from_index(object) do - SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object.id}) + def remove_from_index(%Pleroma.Object{id: object_id}) do + SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) end def search(query, options) do diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 4762b5ac6..644e62630 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -330,7 +330,10 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, if result == :ok do Notification.create_notifications(object) - Pleroma.Search.remove_from_index(deleted_object) + # Only remove from index when deleting actual objects, not users or anything else + with %Pleroma.Object{} <- deleted_object do + Pleroma.Search.remove_from_index(deleted_object) + end {:ok, object, meta} else From fd2cfc80d2853c27f4d0c07631849da9b8d73e85 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 22 Jan 2022 17:17:43 +0300 Subject: [PATCH 046/271] Change search_indexing = 10 and retries for indexing = 2 --- config/config.exs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 3e2f0da3f..ce9338014 100644 --- a/config/config.exs +++ b/config/config.exs @@ -572,7 +572,7 @@ attachments_cleanup: 1, new_users_digest: 1, mute_expire: 5, - search_indexing: 1 + search_indexing: 10 ], plugins: [Oban.Plugins.Pruner], crontab: [ @@ -583,7 +583,8 @@ config :pleroma, :workers, retries: [ federator_incoming: 5, - federator_outgoing: 5 + federator_outgoing: 5, + search_indexing: 2 ] config :pleroma, Pleroma.Formatter, From 79225d9b0adcd848502e5ba0bbbb295855a30ba0 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 22 Jan 2022 21:09:53 +0300 Subject: [PATCH 047/271] Actually, unlisted posts are indexed --- test/pleroma/search/meilisearch_test.exs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs index da614577f..04a2d75d9 100644 --- a/test/pleroma/search/meilisearch_test.exs +++ b/test/pleroma/search/meilisearch_test.exs @@ -71,19 +71,23 @@ test "indexes a local post on creation" do test "doesn't index posts that are not public" do user = insert(:user) - Enum.each(["unlisted", "private", "direct"], fn visiblity -> + Enum.each(["private", "direct"], fn visibility -> {:ok, activity} = CommonAPI.post(user, %{ status: "guys i just don't wanna leave the swamp", - visibility: visiblity + visibility: visibility }) - Meilisearch.add_to_index(activity) + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued(worker: SearchIndexingWorker, args: args) + assert :ok = perform_job(SearchIndexingWorker, args) + assert_not_called(Meilisearch.meili_put(:_)) end) history = call_history(Meilisearch) - assert Enum.count(history) == 3 + assert Enum.count(history) == 2 end test "deletes posts from index when deleted locally" do From 1e23f527e3e22108b402552a0766e488048ed3f4 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 22 Mar 2022 20:29:17 +0300 Subject: [PATCH 048/271] Change the meilisearch key auth to conform to 0.25.0 --- docs/configuration/search.md | 6 +++--- lib/mix/tasks/pleroma/search/meilisearch.ex | 14 ++++++++------ lib/pleroma/search/meilisearch.ex | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/configuration/search.md b/docs/configuration/search.md index a785a18ad..82217e5ee 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -47,15 +47,15 @@ you have to get the _private key_, which is actually used for authentication. === "OTP" ```sh - ./bin/pleroma_ctl search.meilisearch show-private-key + ./bin/pleroma_ctl search.meilisearch show-keys ``` === "From Source" ```sh - mix pleroma.search.meilisearch show-private-key + mix pleroma.search.meilisearch show-keys ``` -This is the key you actually put into your configuration file. +You will see a "Default Admin API Key", this is the key you actually put into your configuration file. ### Initial indexing diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 5098668ad..db56876fa 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -22,7 +22,7 @@ def run(["index"]) do ) # The ranking rule syntax was changed but nothing about that is mentioned in the changelog - if not Version.match?(meili_version, ">= 0.24.0") do + if not Version.match?(meili_version, ">= 0.25.0") do raise "Meilisearch <0.24.0 not supported" end @@ -112,7 +112,7 @@ def run(["clear"]) do meili_delete!("/indexes/objects/documents") end - def run(["show-private-key", master_key]) do + def run(["show-keys", master_key]) do start_pleroma() endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) @@ -120,15 +120,17 @@ def run(["show-private-key", master_key]) do {:ok, result} = Pleroma.HTTP.get( Path.join(endpoint, "/keys"), - [{"X-Meili-API-Key", master_key}] + [{"Authorization", "Bearer #{master_key}"}] ) decoded = Jason.decode!(result.body) - if decoded["private"] do - IO.puts(decoded["private"]) + if decoded["results"] do + Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} -> + IO.puts("#{desc}: #{key}") + end) else - IO.puts("Error fetching the key, check the master key is correct: #{inspect(decoded)}") + IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}") end end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 33bbf8392..0f9182ffc 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -13,7 +13,7 @@ defp meili_headers do private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key]) [{"Content-Type", "application/json"}] ++ - if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}] + if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}] end def meili_get(path) do From 84608be87e2c5961a4deb9030307c978bf1168e5 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Tue, 22 Mar 2022 20:45:49 +0300 Subject: [PATCH 049/271] Change updateId to uid because apparently that's the new name --- lib/mix/tasks/pleroma/search/meilisearch.ex | 2 +- lib/pleroma/search/meilisearch.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index db56876fa..d4a83c3cd 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -91,7 +91,7 @@ def run(["index"]) do ) with {:ok, res} <- result do - if not Map.has_key?(res, "updateId") do + if not Map.has_key?(res, "uid") do IO.puts("\nFailed to index: #{inspect(result)}") end else diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 0f9182ffc..3db65f261 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -153,7 +153,7 @@ def add_to_index(activity) do ) with {:ok, res} <- result, - true <- Map.has_key?(res, "updateId") do + true <- Map.has_key?(res, "uid") do # Do nothing else _ -> From b150e6f15e0f06c8e23c0ac66aeaf80eb2f8c31a Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Wed, 23 Mar 2022 11:36:01 +0300 Subject: [PATCH 050/271] Update meilisearch docs --- docs/configuration/search.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/configuration/search.md b/docs/configuration/search.md index 82217e5ee..f131948a7 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -37,8 +37,10 @@ indexes faster when it can process many posts in a single batch. Information about setting up meilisearch can be found in the [official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html). -You probably want to start it with `MEILI_NO_ANALYTICS=true` and `MEILI_NO_CENTRY=true` environment variables, -to disable analytics. +You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics. +At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces +the `--enable-auto-batching` option which drastically improves performance. Without this option, the search +is hardly usable on a somewhat big instance. ### Private key authentication (optional) From e20f74c71b078d706bc93632773f9b590d2fb018 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Fri, 26 Aug 2022 23:39:58 +0300 Subject: [PATCH 051/271] Remove duplicate function call --- lib/pleroma/search/meilisearch.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 3db65f261..53f8a2544 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -93,7 +93,6 @@ def search(user, query, options \\ []) do hits |> Activity.create_by_object_ap_id() |> Activity.with_preloaded_object() - |> Activity.with_preloaded_object() |> Activity.restrict_deactivated_users() |> maybe_restrict_local(user) |> maybe_restrict_author(author) From 119b2b847b76c7300bd71699d9f2e5676bdb0bb4 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 27 Aug 2022 00:09:37 +0300 Subject: [PATCH 052/271] Instead of checking string length, explicitly check for "" and "." --- lib/pleroma/search/meilisearch.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 53f8a2544..2e13b8407 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -112,7 +112,7 @@ def object_to_search_data(object) do not is_nil(object.data["content"]) and (Pleroma.Constants.as_public() in object.data["to"] or Pleroma.Constants.as_public() in object.data["cc"]) and - String.length(object.data["content"]) > 1 do + object.data["content"] not in ["", "."] do data = object.data content_str = @@ -127,7 +127,8 @@ def object_to_search_data(object) do trimmed end - if String.length(content) > 1 do + # Make sure we have a non-empty string + if content != "" do {:ok, published, _} = DateTime.from_iso8601(data["published"]) %{ From 102ebb42bdba1673da39a8fa8ed1662bc8565aa4 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 27 Aug 2022 00:19:08 +0300 Subject: [PATCH 053/271] Make search a callback --- lib/pleroma/search/database_search.ex | 1 + lib/pleroma/search/meilisearch.ex | 1 + lib/pleroma/search/search_backend.ex | 11 +++++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex index 3735a5fab..9a340abf1 100644 --- a/lib/pleroma/search/database_search.ex +++ b/lib/pleroma/search/database_search.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Search.DatabaseSearch do @behaviour Pleroma.Search.SearchBackend + @impl true def search(user, search_query, options \\ []) do index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin limit = Enum.min([Keyword.get(options, :limit), 40]) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 2e13b8407..4e88169d2 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -75,6 +75,7 @@ def meili_delete!(path) do ) end + @impl true def search(user, query, options \\ []) do limit = Enum.min([Keyword.get(options, :limit), 40]) offset = Keyword.get(options, :offset, 0) diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex index ed6bfd329..a42e2f5f6 100644 --- a/lib/pleroma/search/search_backend.ex +++ b/lib/pleroma/search/search_backend.ex @@ -1,10 +1,17 @@ defmodule Pleroma.Search.SearchBackend do + @doc """ + Search statuses with a query, restricting to only those the user should have access to. + """ + @callback search(user :: Pleroma.User.t(), query :: String.t(), options :: [any()]) :: [ + Pleroma.Activity.t() + ] + @doc """ Add the object associated with the activity to the search index. The whole activity is passed, to allow filtering on things such as scope. """ - @callback add_to_index(activity :: Pleroma.Activity.t()) :: nil + @callback add_to_index(activity :: Pleroma.Activity.t()) :: :ok | {:error, any()} @doc """ Remove the object from the index. @@ -13,5 +20,5 @@ defmodule Pleroma.Search.SearchBackend do is what contains the actual content and there is no need for fitlering when removing from index. """ - @callback remove_from_index(object :: Pleroma.Object.t()) :: nil + @callback remove_from_index(object :: Pleroma.Object.t()) :: {:ok, any()} | {:error, any()} end From 5ac67632384bfb284ac51f2a450d41cf3913378a Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 27 Aug 2022 00:31:36 +0300 Subject: [PATCH 054/271] Make add_to_index and remove_from_index report errors --- lib/mix/tasks/pleroma/search/meilisearch.ex | 4 +-- lib/pleroma/search/meilisearch.ex | 27 +++++++++++-------- lib/pleroma/workers/search_indexing_worker.ex | 4 --- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index d4a83c3cd..72a558228 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -9,7 +9,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do import Ecto.Query import Pleroma.Search.Meilisearch, - only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1] + only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete: 1] def run(["index"]) do start_pleroma() @@ -109,7 +109,7 @@ def run(["index"]) do def run(["clear"]) do start_pleroma() - meili_delete!("/indexes/objects/documents") + meili_delete("/indexes/objects/documents") end def run(["show-keys", master_key]) do diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 4e88169d2..24789b00c 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -62,17 +62,16 @@ def meili_put(path, params) do end end - def meili_delete!(path) do + def meili_delete(path) do endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) - {:ok, _} = - Pleroma.HTTP.request( - :delete, - Path.join(endpoint, path), - "", - meili_headers(), - [] - ) + Pleroma.HTTP.request( + :delete, + Path.join(endpoint, path), + "", + meili_headers(), + [] + ) end @impl true @@ -155,16 +154,22 @@ def add_to_index(activity) do with {:ok, res} <- result, true <- Map.has_key?(res, "uid") do - # Do nothing + # Added successfully + :ok else _ -> + # There was an error, report it Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") + {:error, result} end + else + # The post isn't something we can search, that's ok + :ok end end @impl true def remove_from_index(object) do - meili_delete!("/indexes/objects/documents/#{object.id}") + meili_delete("/indexes/objects/documents/#{object.id}") end end diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex index 70a8d42d0..43b7bad1e 100644 --- a/lib/pleroma/workers/search_indexing_worker.ex +++ b/lib/pleroma/workers/search_indexing_worker.ex @@ -9,8 +9,6 @@ def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module.add_to_index(activity) - - :ok end def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do @@ -19,7 +17,5 @@ def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) d search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module.remove_from_index(object) - - :ok end end From 6256822afd368e5f6b410d47c5ff9b584e50a461 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 27 Aug 2022 01:11:50 +0300 Subject: [PATCH 055/271] Check for updateId, not uid --- lib/pleroma/search/meilisearch.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 24789b00c..0b90971b1 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -153,7 +153,7 @@ def add_to_index(activity) do ) with {:ok, res} <- result, - true <- Map.has_key?(res, "uid") do + true <- Map.has_key?(res, "updateId") do # Added successfully :ok else From 5a39866388c411f2bcee9848352f8c420513f34f Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Sat, 27 Aug 2022 01:43:59 +0300 Subject: [PATCH 056/271] Specifically strip mentions for search indexing --- lib/mix/tasks/pleroma/search/meilisearch.ex | 1 + lib/pleroma/search/meilisearch.ex | 3 ++- priv/scrubbers/search_indexing.ex | 24 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 priv/scrubbers/search_indexing.ex diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 72a558228..8379a0c25 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -13,6 +13,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do def run(["index"]) do start_pleroma() + Pleroma.HTML.compile_scrubbers() meili_version = ( diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 0b90971b1..7af7f460a 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -122,7 +122,8 @@ def object_to_search_data(object) do end content = - with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str), + with {:ok, scrubbed} <- + FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), trimmed <- String.trim(scrubbed) do trimmed end diff --git a/priv/scrubbers/search_indexing.ex b/priv/scrubbers/search_indexing.ex new file mode 100644 index 000000000..02756ab79 --- /dev/null +++ b/priv/scrubbers/search_indexing.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTML.Scrubber.SearchIndexing do + @moduledoc """ + An HTML scrubbing policy that scrubs things for searching. + """ + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + # Explicitly remove mentions + def scrub({:a, attrs, children}) do + if(Enum.any?(attrs, fn {att, val} -> att == "class" and String.contains?(val, "mention") end), + do: nil, + # Strip the tag itself, leave only children (text, presumably) + else: children + ) + end + + Meta.strip_comments() + Meta.strip_everything_not_covered() +end From bed44f9f628c3e184ba367765c87c47656060f28 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 13:10:18 -0400 Subject: [PATCH 057/271] Update to Phoenix 1.7 Chase the other deps to match versions you get from a current "mix phx.new" project creation --- mix.exs | 15 ++++++++------- mix.lock | 24 +++++++++++++----------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/mix.exs b/mix.exs index bb16f0a9c..3fcbfffaf 100644 --- a/mix.exs +++ b/mix.exs @@ -113,18 +113,19 @@ defp oauth_deps do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.6.0"}, - {:phoenix_ecto, "~> 4.4.0"}, + {:phoenix, "~> 1.7.3"}, + {:phoenix_ecto, "~> 4.4"}, + {:ecto_sql, "~> 3.10"}, {:ecto_enum, "~> 1.4"}, {:postgrex, ">= 0.0.0"}, - {:phoenix_html, "~> 3.1"}, + {:phoenix_html, "~> 3.3"}, {:phoenix_live_reload, "~> 1.3.3", only: :dev}, - {:phoenix_live_view, "~> 0.17.1"}, - {:phoenix_live_dashboard, "~> 0.6.2"}, - {:telemetry_metrics, "~> 0.6.1"}, + {:phoenix_live_view, "~> 0.19.0"}, + {:phoenix_live_dashboard, "~> 0.8.0"}, + {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, {:tzdata, "~> 1.0.3"}, - {:plug_cowboy, "~> 2.3"}, + {:plug_cowboy, "~> 2.5"}, # oban 2.14 requires Elixir 1.12+ {:oban, "~> 2.13.4"}, {:gettext, diff --git a/mix.lock b/mix.lock index 43012a58e..fd8e02f95 100644 --- a/mix.lock +++ b/mix.lock @@ -16,22 +16,22 @@ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"}, "covertool": {:hex, :covertool, "2.0.4", "54acff6cddd88d28dea663cd2e1fe20dd32fcf5f5d3aff7d59031ce44ce39efa", [:rebar3], [], "hexpm", "5c9568ba4308fda2082172737c80c31d991ea83961eb10791f06106a870d0cdc"}, - "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark": {:hex, :earmark, "1.4.22", "ea3e45c6359446dc308be0a64ce82a03260d973de7d0625a762e6d352ff57958", [:mix], [{:earmark_parser, "~> 1.4.23", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "1caf5145665a42fd76d5317286b0c171861fb1c04f86ab103dde76868814fdfb"}, "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, - "ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"}, + "ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"}, - "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"}, + "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.11", "6e20144c1446dcccfcdb4c142c9d8b7992a90a569b1d5958cbea5458550b25f0", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "def61f1f92d4f40d51c80bbae2157212d6c0a459eb604be446e47369cbd40b23"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, @@ -84,13 +84,13 @@ "open_api_spex": {:hex, :open_api_spex, "3.16.1", "8137c338129d63060b4b04947c6c57429f86267045c479c703a38a6d3f98dee1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "ef6fd778ac121af866b48b75ad4ad256b6ff33949113ea4aa1629af8bfdfdbfb"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, - "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, + "phoenix": {:hex, :phoenix, "1.7.3", "4d8eca2c020c9ed81a28e7a8c60e0a4f6f9f7f6e12eb91dfd01301eac07424c1", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6b1bc308758f95ecf3e0d795389440a2ca88a903e0fda1f921c780918c16d640"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.14", "5ec615d4d61bf9d4755f158bd6c80372b715533fe6d6219e12d74fb5eedbeac1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "afeb6ba43ce329a6f7fc1c9acdfc6d3039995345f025febb7f409a92f6faebd3"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.0", "de5643d03e3cdf5ff19cd45b5d14543a3b1ad8551d529f6b24246e88a6c6f1b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a650b6f814c4f386314b98f1aebf92f8652649166612f84ef2e60a20894addfa"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.2", "a4950b63ace57720b0fc1c6da083c53346b36f99021de89595cc4f026288ff51", [:mix], [], "hexpm", "45741676a94c71f9afdfed9d22d49b6856c026ff504db04e3dc03a1d86f8201c"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, @@ -100,7 +100,7 @@ "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, + "postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "prom_ex": {:hex, :prom_ex, "1.7.1", "39331ee3fe6f9a8587d8208bf9274a253bb80281700e127dd18786cda5e08c37", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.10.2", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "4c978872b88a929833925a0f4d0561824804c671fdd04581e765509ed0a6ed08"}, "prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"}, @@ -132,5 +132,7 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, + "websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } From 6e1ea2eafc24cb31e796e93313dcf9c6b6ba9845 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 13:36:07 -0400 Subject: [PATCH 058/271] Remove unnecessary compilers as of Phoenix 1.7 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 3fcbfffaf..9eafa2474 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ def project do version: version("2.5.52"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), - compilers: [:phoenix, :gettext] ++ Mix.compilers(), + compilers: Mix.compilers(), elixirc_options: [warnings_as_errors: warnings_as_errors()], xref: [exclude: [:eldap]], start_permanent: Mix.env() == :prod, From e3110cb34e350bc9353d082328778daae47ba9e8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 13:36:21 -0400 Subject: [PATCH 059/271] Fix deprecated calls to get_flash/2 --- lib/pleroma/web.ex | 2 +- lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex | 8 ++++---- lib/pleroma/web/templates/o_auth/mfa/totp.html.eex | 8 ++++---- lib/pleroma/web/templates/o_auth/o_auth/register.html.eex | 8 ++++---- lib/pleroma/web/templates/o_auth/o_auth/show.html.eex | 8 ++++---- test/pleroma/web/o_auth/o_auth_controller_test.exs | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index aee41b0fe..7a8b176cd 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -136,7 +136,7 @@ def view do namespace: Pleroma.Web # Import convenience functions from controllers - import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] + import Phoenix.Controller, only: [get_csrf_token: 0, view_module: 1] import Pleroma.Web.ErrorHelpers import Pleroma.Web.Gettext diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex index e45d13bdf..e3639aae7 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> - +<%= if Phoenix.Flash.get(@flash, :info) do %> + <% end %> -<%= if get_flash(@conn, :error) do %> - +<%= if Phoenix.Flash.get(@flash, :error) do %> + <% end %>

<%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %>

diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex index 50e6c04b6..f995b8805 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> - +<%= if Phoenix.Flash.get(@flash, :info) do %> + <% end %> -<%= if get_flash(@conn, :error) do %> - +<%= if Phoenix.Flash.get(@flash, :error) do %> + <% end %>

<%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %>

diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex index 1f661efb2..e7f65266f 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> - +<%= if Phoenix.Flash.get(@flash, :info) do %> + <% end %> -<%= if get_flash(@conn, :error) do %> - +<%= if Phoenix.Flash.get(@flash, :error) do %> + <% end %>

<%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %>

diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index b3654f3eb..5b38f7142 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> - +<%= if Phoenix.Flash.get(@flash, :info) do %> + <% end %> -<%= if get_flash(@conn, :error) do %> - +<%= if Phoenix.Flash.get(@flash, :error) do %> + <% end %> <%= form_for @conn, Routes.o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index f41d6a322..83a08d9fc 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -186,7 +186,7 @@ test "on authentication error, GET /oauth//callback redirects to `redi assert html_response(conn, 302) assert redirected_to(conn) == app.redirect_uris - assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + assert conn.assigns.flash["error"] == "Failed to authenticate: (error description)." end test "GET /oauth/registration_details renders registration details form", %{ @@ -307,7 +307,7 @@ test "with invalid params, POST /oauth/register?op=register renders registration |> post("/oauth/register", bad_params) assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ - assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." + assert conn.assigns.flash["error"] == "Error: #{bad_param} has already been taken." end end @@ -398,7 +398,7 @@ test "with invalid params, POST /oauth/register?op=connect renders registration_ |> post("/oauth/register", params) assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ - assert get_flash(conn, :error) == "Invalid Username/Password" + assert conn.assigns.flash["error"] == "Invalid Username/Password" end end From f622f82c0e1ca66f1dc3493d900f60a24ab96865 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 13:38:46 -0400 Subject: [PATCH 060/271] No user facing changes --- changelog.d/3900.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/3900.skip diff --git a/changelog.d/3900.skip b/changelog.d/3900.skip new file mode 100644 index 000000000..e69de29bb From a7e7db4a29a09dc2193455de696d5cda561db5d4 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 13:47:15 -0400 Subject: [PATCH 061/271] Phoenix.Endpoint.Cowboy2Handler -> Plug.Cowboy.Handler --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 178b6be99..6c9eb9bfc 100644 --- a/config/config.exs +++ b/config/config.exs @@ -133,7 +133,7 @@ {"/websocket", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, - {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} + {:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}} ]} ] ], From 4dc2d4bf7b39ca427fdb9be90a9789b7c4d9824e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 15:30:31 -0400 Subject: [PATCH 062/271] Remove locked version of plug Required for Phoenix 1.7 websocket changes --- mix.exs | 3 --- mix.lock | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index 9eafa2474..e346834e5 100644 --- a/mix.exs +++ b/mix.exs @@ -197,9 +197,6 @@ defp deps do {:open_api_spex, "~> 3.16"}, {:ecto_psql_extras, "~> 0.6"}, - # indirect dependency version override - {:plug, "~> 1.10.4", override: true}, - ## dev & test {:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_machina, "~> 2.4", only: :test}, diff --git a/mix.lock b/mix.lock index fd8e02f95..65863fc40 100644 --- a/mix.lock +++ b/mix.lock @@ -94,7 +94,7 @@ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"}, + "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, From ffee478ed0298f2cf29d4d51ed28105119552496 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 15:30:58 -0400 Subject: [PATCH 063/271] Move websocket config for Shoutbox to the Endpoint This is the modern way of configuring it --- config/config.exs | 14 -------------- lib/pleroma/web/endpoint.ex | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/config/config.exs b/config/config.exs index 6c9eb9bfc..fb1903db9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -110,17 +110,6 @@ "xmpp" ] -websocket_config = [ - path: "/websocket", - serializer: [ - {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"}, - {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"} - ], - timeout: 60_000, - transport_log: false, - compress: false -] - # Configures the endpoint config :pleroma, Pleroma.Web.Endpoint, url: [host: "localhost"], @@ -130,9 +119,6 @@ {:_, [ {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, - {"/websocket", Phoenix.Endpoint.CowboyWebSocket, - {Phoenix.Transports.WebSocket, - {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, {:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}} ]} ] diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index d8d40cceb..9e0340cd2 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -9,7 +9,20 @@ defmodule Pleroma.Web.Endpoint do alias Pleroma.Config - socket("/socket", Pleroma.Web.UserSocket) + socket("/socket", Pleroma.Web.UserSocket, + websocket: [ + path: "/websocket", + serializer: [ + {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"}, + {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"} + ], + timeout: 60_000, + transport_log: false, + compress: false + ], + longpoll: false + ) + socket("/live", Phoenix.LiveView.Socket) plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) From 62322f71e2f2e607ee66a91a99e35eecbf89914d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 16:22:40 -0400 Subject: [PATCH 064/271] Clean up Plug.Parsers.MULTIPART deprecation warnings There is no need to the length setting to :multipart. The length setting is global for all of the parsers. --- lib/pleroma/web/endpoint.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 9e0340cd2..201a92653 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -116,7 +116,7 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.Parsers, parsers: [ :urlencoded, - {:multipart, length: {Config, :get, [[:instance, :upload_limit]]}}, + :multipart, :json ], pass: ["*/*"], From ba988a9abc6cb92d47b14f9ddf8ac78946215972 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 16:30:31 -0400 Subject: [PATCH 065/271] Fix test warnings warning: the URI path used in plug tests must start with "/" --- .../controllers/status_controller_test.exs | 22 +++++++++---------- .../controllers/timeline_controller_test.exs | 12 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 1e8979127..563034355 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -37,7 +37,7 @@ test "posting a status does not increment reblog_count when relaying", %{conn: c response = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "content_type" => "text/plain", "source" => "Pleroma FE", "status" => "Hello world", @@ -50,7 +50,7 @@ test "posting a status does not increment reblog_count when relaying", %{conn: c response = conn - |> get("api/v1/statuses/#{response["id"]}", %{}) + |> get("/api/v1/statuses/#{response["id"]}", %{}) |> json_response_and_validate_schema(200) assert response["reblogs_count"] == 0 @@ -109,7 +109,7 @@ test "posting a status", %{conn: conn} do conn_four = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -134,7 +134,7 @@ test "it fails to create a status if `expires_in` is less or equal than an hour" assert %{"error" => "Expiry date is too soon"} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -146,7 +146,7 @@ test "it fails to create a status if `expires_in` is less or equal than an hour" assert %{"error" => "Expiry date is too soon"} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -160,7 +160,7 @@ test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) + |> post("/api/v1/statuses", %{"status" => "GNO/Linux"}) |> json_response_and_validate_schema(422) end @@ -353,7 +353,7 @@ test "posting a direct status", %{conn: conn} do conn = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) + |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"}) assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200) assert response["visibility"] == "direct" @@ -390,7 +390,7 @@ test "discloses application metadata when enabled" do result = conn - |> get("api/v1/statuses/#{activity}") + |> get("/api/v1/statuses/#{activity}") assert %{ "content" => "cofe is my copilot", @@ -419,7 +419,7 @@ test "hides application metadata when disabled" do result = conn - |> get("api/v1/statuses/#{activity}") + |> get("/api/v1/statuses/#{activity}") assert %{ "content" => "club mate is my wingman", @@ -1334,7 +1334,7 @@ test "on pin removes deletion job, on unpin reschedule deletion" do assert %{"id" => id} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -1584,7 +1584,7 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c conn |> assign(:user, user3) |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) - |> get("api/v1/timelines/home") + |> get("/api/v1/timelines/home") [reblogged_activity] = json_response_and_validate_schema(conn3, 200) diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index b13a8033b..c120dd53c 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -527,7 +527,7 @@ test "direct timeline", %{conn: conn} do |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) # Only direct should be visible here - res_conn = get(conn_user_two, "api/v1/timelines/direct") + res_conn = get(conn_user_two, "/api/v1/timelines/direct") assert [status] = json_response_and_validate_schema(res_conn, :ok) @@ -539,14 +539,14 @@ test "direct timeline", %{conn: conn} do build_conn() |> assign(:user, user_one) |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) - |> get("api/v1/timelines/direct") + |> get("/api/v1/timelines/direct") [status] = json_response_and_validate_schema(res_conn, :ok) assert %{"visibility" => "direct"} = status # Both should be visible here - res_conn = get(conn_user_two, "api/v1/timelines/home") + res_conn = get(conn_user_two, "/api/v1/timelines/home") [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) @@ -559,14 +559,14 @@ test "direct timeline", %{conn: conn} do }) end) - res_conn = get(conn_user_two, "api/v1/timelines/direct") + res_conn = get(conn_user_two, "/api/v1/timelines/direct") statuses = json_response_and_validate_schema(res_conn, :ok) assert length(statuses) == 20 max_id = List.last(statuses)["id"] - res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}") + res_conn = get(conn_user_two, "/api/v1/timelines/direct?max_id=#{max_id}") assert [status] = json_response_and_validate_schema(res_conn, :ok) @@ -591,7 +591,7 @@ test "doesn't include DMs from blocked users" do visibility: "direct" }) - res_conn = get(conn, "api/v1/timelines/direct") + res_conn = get(conn, "/api/v1/timelines/direct") [status] = json_response_and_validate_schema(res_conn, :ok) assert status["id"] == direct.id From d9f031c9da42a1bde12a37bf26298c3a3c731121 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 21:11:13 +0000 Subject: [PATCH 066/271] Bump minimum Elixir to 1.12 --- .gitlab-ci.yml | 13 +++++++------ Dockerfile | 2 +- ci/Dockerfile | 2 +- mix.exs | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8daa9f434..2cb39a5c9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ image: git.pleroma.social:5050/pleroma/pleroma/ci-base variables: &global_variables + ELIXIR_VER: 1.12.3 POSTGRES_DB: pleroma_test POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -276,7 +277,7 @@ stop_review_app: amd64: stage: release - image: elixir:1.11.4 + image: elixir:$ELIXIR_VER only: &release-only - stable@pleroma/pleroma - develop@pleroma/pleroma @@ -316,7 +317,7 @@ amd64-musl: stage: release artifacts: *release-artifacts only: *release-only - image: elixir:1.11.4-alpine + image: elixir:$ELIXIR_VER-alpine tags: - amd64 cache: *release-cache @@ -334,7 +335,7 @@ arm: only: *release-only tags: - arm32-specified - image: arm32v7/elixir:1.11.4 + image: arm32v7/elixir:$ELIXIR_VER cache: *release-cache variables: *release-variables before_script: *before-release @@ -346,7 +347,7 @@ arm-musl: only: *release-only tags: - arm32-specified - image: arm32v7/elixir:1.11.4-alpine + image: arm32v7/elixir:$ELIXIR_VER-alpine cache: *release-cache variables: *release-variables before_script: *before-release-musl @@ -358,7 +359,7 @@ arm64: only: *release-only tags: - arm - image: arm64v8/elixir:1.11.4 + image: arm64v8/elixir:$ELIXIR_VER cache: *release-cache variables: *release-variables before_script: *before-release @@ -370,7 +371,7 @@ arm64-musl: only: *release-only tags: - arm - image: arm64v8/elixir:1.11.4-alpine + image: arm64v8/elixir:$ELIXIR_VER-alpine cache: *release-cache variables: *release-variables before_script: *before-release-musl diff --git a/Dockerfile b/Dockerfile index 310d18104..56c99da72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG ELIXIR_IMG=hexpm/elixir -ARG ELIXIR_VER=1.11.4 +ARG ELIXIR_VER=1.12.3 ARG ERLANG_VER=24.2.1 ARG ALPINE_VER=3.17.0 diff --git a/ci/Dockerfile b/ci/Dockerfile index ca28b7029..a2b566873 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM elixir:1.11.4 +FROM elixir:1.12.3 # Single RUN statement, otherwise intermediate images are created # https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run diff --git a/mix.exs b/mix.exs index e346834e5..6e3d69379 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ def project do [ app: :pleroma, version: version("2.5.52"), - elixir: "~> 1.11", + elixir: "~> 1.12", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), elixirc_options: [warnings_as_errors: warnings_as_errors()], From 2b8bbb288c26a95970e2d9e988f5eb303e052644 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 22:11:17 +0000 Subject: [PATCH 067/271] Phoenix.Socket.Transport.force_ssl/4 no longer exists --- lib/phoenix/transports/web_socket/raw.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex index 8cf9c32a2..cf4fda79f 100644 --- a/lib/phoenix/transports/web_socket/raw.ex +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -26,7 +26,6 @@ def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do conn |> fetch_query_params |> Transport.transport_log(opts[:transport_log]) - |> Transport.force_ssl(handler, endpoint, opts) |> Transport.check_origin(handler, endpoint, opts) case conn do From f0e5f0e8375bb55e13b0f054fcf09871d2967d24 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 31 May 2023 22:14:55 +0000 Subject: [PATCH 068/271] Fix compile warning warning: doing a prefix match with globs is deprecated, invalid segment "pleroma*path" --- lib/pleroma/web/router.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c1a690e28..6b9e158a3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -996,8 +996,8 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web.Fallback do get("/registration/:token", RedirectController, :registration_page) get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) - match(:*, "/api/pleroma*path", LegacyPleromaApiRerouterPlug, []) - get("/api*path", RedirectController, :api_not_implemented) + match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, []) + get("/api/*path", RedirectController, :api_not_implemented) get("/*path", RedirectController, :redirector_with_preload) options("/*path", RedirectController, :empty) From 2e45be2653f662f60e87fce56aa3c256e20bd1fb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 2 Jun 2023 11:34:04 -0400 Subject: [PATCH 069/271] Add :phoenix to extra_applications to suppress a warning Related to use of Phoenix.Flash.get/2 --- mix.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/mix.exs b/mix.exs index 6e3d69379..771b491b8 100644 --- a/mix.exs +++ b/mix.exs @@ -78,6 +78,7 @@ def application do :comeonin, :fast_sanitize, :os_mon, + :phoenix, :ssl ], included_applications: [:ex_syslogger] From bcd7ccac11f3f7217ffd8ec32d32457436315f58 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 3 Jun 2023 14:03:51 -0400 Subject: [PATCH 070/271] Support a type called "change" --- tools/check-changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check-changelog b/tools/check-changelog index 60692033f..64e59ae7f 100644 --- a/tools/check-changelog +++ b/tools/check-changelog @@ -6,7 +6,7 @@ git remote add upstream https://git.pleroma.social/pleroma/pleroma.git git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \ - grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\)$' + grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security|change\)$' ret=$? if [ $ret -eq 0 ]; then From c665d532951c34c7d1185e66b59390202b54d0c9 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 3 Jun 2023 14:04:12 -0400 Subject: [PATCH 071/271] Update to Phoenix 1.7 --- changelog.d/3900.change | 1 + changelog.d/3900.skip | 0 2 files changed, 1 insertion(+) create mode 100644 changelog.d/3900.change delete mode 100644 changelog.d/3900.skip diff --git a/changelog.d/3900.change b/changelog.d/3900.change new file mode 100644 index 000000000..fe0cc2fbf --- /dev/null +++ b/changelog.d/3900.change @@ -0,0 +1 @@ +Update to Phoenix 1.7 diff --git a/changelog.d/3900.skip b/changelog.d/3900.skip deleted file mode 100644 index e69de29bb..000000000 From 63ef1dcedca7fb315f611388e477d3847d1acaa1 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 3 Jun 2023 14:17:49 -0400 Subject: [PATCH 072/271] Phoenix.Router.routes/1 is the public function we are meant to be using here --- lib/pleroma/web/router.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6b9e158a3..a6e180c4c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1003,9 +1003,8 @@ defmodule Pleroma.Web.Router do options("/*path", RedirectController, :empty) end - # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+ def get_api_routes do - __MODULE__.__routes__() + Phoenix.Router.routes(__MODULE__) |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end) |> Enum.map(fn r -> r.path From 18a0c923d0da4c8fb6e33b383dabd1d06bb22968 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 3 Aug 2023 13:08:37 -0400 Subject: [PATCH 073/271] Resolve information disclosure vulnerability through emoji pack archive download endpoint The pack name has been sanitized so an attacker cannot upload a media file called pack.json with their own handcrafted list of emoji files as arbitrary files on the filesystem and then call the emoji pack archive download endpoint with a pack name crafted to the location of the media file they uploaded which tricks Pleroma into generating a zip file of the target files the attacker wants to download. The attack only works if the Pleroma instance does not have the AnonymizeFilename upload filter enabled, which is currently the default. Reported by: graf@poast.org --- changelog.d/emoji-pack-sanitization.security | 1 + lib/pleroma/emoji/pack.ex | 1 + test/pleroma/emoji/pack_test.exs | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 changelog.d/emoji-pack-sanitization.security diff --git a/changelog.d/emoji-pack-sanitization.security b/changelog.d/emoji-pack-sanitization.security new file mode 100644 index 000000000..f3218abd4 --- /dev/null +++ b/changelog.d/emoji-pack-sanitization.security @@ -0,0 +1 @@ +Emoji pack loader sanitizes pack names diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index a361ea200..6e58f8898 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -285,6 +285,7 @@ def update_metadata(name, data) do @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()} def load_pack(name) do + name = Path.basename(name) pack_file = Path.join([emoji_path(), name, "pack.json"]) with {:ok, _} <- File.stat(pack_file), diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 18b99da75..00001abfc 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -90,4 +90,8 @@ test "add emoji file", %{pack: pack} do assert updated_pack.files_count == 1 end + + test "load_pack/1 ignores path traversal in a forged pack name", %{pack: pack} do + assert {:ok, ^pack} = Pack.load_pack("../../../../../dump_pack") + end end From 4befb3b1d02f32eb2c56f12e4684a7bb3167b0ee Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 00:46:52 +0200 Subject: [PATCH 074/271] Config: Restrict permissions of OTP config file --- lib/pleroma/config/release_runtime_provider.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex index 91e5f1a54..9ec0f975e 100644 --- a/lib/pleroma/config/release_runtime_provider.ex +++ b/lib/pleroma/config/release_runtime_provider.ex @@ -20,6 +20,20 @@ def load(config, opts) do with_runtime_config = if File.exists?(config_path) do + # + %File.Stat{mode: mode} = File.lstat!(config_path) + + if Bitwise.band(mode, 0o007) > 0 do + raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}" + end + + if Bitwise.band(mode, 0o020) > 0 do + raise "Configuration at #{config_path} has group-wise write permissions, execute the following: chmod g-w #{config_path}" + end + + # Note: Elixir doesn't provides a getuid(2) + # so cannot forbid group-read only when config is owned by us + runtime_config = Config.Reader.read!(config_path) with_defaults From bd7381f2f4139c26d9dbb8aad77ce77be7777532 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 00:58:05 +0200 Subject: [PATCH 075/271] instance gen: Reduce permissions of pleroma directories and config files --- lib/mix/tasks/pleroma/instance.ex | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 5c93f19ff..5d8b254a2 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -266,12 +266,20 @@ def run(["gen" | rest]) do config_dir = Path.dirname(config_path) psql_dir = Path.dirname(psql_path) + # Note: Distros requiring group read (0o750) on those directories should + # pre-create the directories. [config_dir, psql_dir, static_dir, uploads_dir] |> Enum.reject(&File.exists?/1) - |> Enum.map(&File.mkdir_p!/1) + |> Enum.each(fn dir -> + File.mkdir_p!(dir) + File.chmod!(dir, 0o700) + end) shell_info("Writing config to #{config_path}.") + # Sadly no fchmod(2) equivalent in Elixir… + File.touch!(config_path) + File.chmod!(config_path, 0o640) File.write(config_path, result_config) shell_info("Writing the postgres script to #{psql_path}.") File.write(psql_path, result_psql) @@ -290,8 +298,7 @@ def run(["gen" | rest]) do else shell_error( "The task would have overwritten the following files:\n" <> - (Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <> - "Rerun with `--force` to overwrite them." + Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them." ) end end From 22df32b3f5cfe9fe0a4a97ff799df72c091b676e Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 01:00:25 +0200 Subject: [PATCH 076/271] changelog: Entry for config permissions restrictions Closes: https://git.pleroma.social/pleroma/pleroma/-/issues/3135 --- changelog.d/otp_perms.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/otp_perms.security diff --git a/changelog.d/otp_perms.security b/changelog.d/otp_perms.security new file mode 100644 index 000000000..a3da1c677 --- /dev/null +++ b/changelog.d/otp_perms.security @@ -0,0 +1 @@ +- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories \ No newline at end of file From 76e408e42d1da123a955b85490f05f6d810172f9 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 07:16:50 +0200 Subject: [PATCH 077/271] release_runtime_provider_test: chmod config for hardened permissions Git doesn't manages file permissions precisely enough for us. --- test/pleroma/config/release_runtime_provider_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/pleroma/config/release_runtime_provider_test.exs b/test/pleroma/config/release_runtime_provider_test.exs index 4e0d4c838..8ff578e63 100644 --- a/test/pleroma/config/release_runtime_provider_test.exs +++ b/test/pleroma/config/release_runtime_provider_test.exs @@ -17,6 +17,8 @@ test "loads release defaults config and warns about non-existent runtime config" end test "merged runtime config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs") @@ -25,6 +27,8 @@ test "merged runtime config" do end test "merged exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + ExUnit.CaptureIO.capture_io(fn -> merged = ReleaseRuntimeProvider.load([], @@ -37,6 +41,9 @@ test "merged exported config" do end test "runtime config is merged with exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs", From c37561214a803f9011d5ec6af8b8c07e547c19ff Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 06:49:19 +0200 Subject: [PATCH 078/271] Force the use of amd64 runners for jobs using ci-base --- .gitlab-ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b0381d11..91e568a32 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,7 +32,13 @@ before_script: after_script: - rm -rf _build/*/lib/pleroma +.using-ci-base: + tags: + - amd64 + build: + extends: + - .using-ci-base stage: build only: changes: &build_changes_policy @@ -44,6 +50,8 @@ build: - mix compile --force spec-build: + extends: + - .using-ci-base stage: test only: changes: @@ -57,6 +65,8 @@ spec-build: - mix pleroma.openapi_spec spec.json benchmark: + extends: + - .using-ci-base stage: benchmark when: manual variables: @@ -71,6 +81,8 @@ benchmark: - mix pleroma.load_testing unit-testing: + extends: + - .using-ci-base stage: test only: changes: *build_changes_policy @@ -94,6 +106,8 @@ unit-testing: path: coverage.xml unit-testing-erratic: + extends: + - .using-ci-base stage: test retry: 2 allow_failure: true @@ -129,6 +143,8 @@ unit-testing-erratic: # - mix test --trace --only federated unit-testing-rum: + extends: + - .using-ci-base stage: test only: changes: *build_changes_policy @@ -162,6 +178,8 @@ lint: - mix format --check-formatted analysis: + extends: + - .using-ci-base stage: test only: changes: *build_changes_policy From 5ac2b7417d052a493b38ee05b393ae7c78e89484 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 09:16:08 +0200 Subject: [PATCH 079/271] test: Fix warnings --- .../activity_pub/transmogrifier/emoji_react_handling_test.exs | 2 +- test/pleroma/web/mastodon_api/update_credentials_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs index 9d99df27c..83bf59c6f 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -65,7 +65,7 @@ test "it works for incoming unqualified emoji reactions" do object = Object.get_by_ap_id(data["object"]) assert object.data["reaction_count"] == 1 - assert match?([[emoji, _]], object.data["reactions"]) + assert match?([[^emoji, _]], object.data["reactions"]) end test "it reject invalid emoji reactions" do diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 57fa0f047..40f79d103 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -375,7 +375,7 @@ test "updates the user's background, upload_limit, returns a HTTP 413", %{ "pleroma_background_image" => new_background_oversized }) - assert user_response = json_response_and_validate_schema(res, 413) + assert _user_response = json_response_and_validate_schema(res, 413) assert user.background == %{} clear_config([:instance, :upload_limit], upload_limit) From 57f74537486cf7f721679f125741de9008478b00 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 05:13:28 +0200 Subject: [PATCH 080/271] Release 2.5.3 --- CHANGELOG.md | 6 ++++++ mix.exs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6fc6aaee..468ec1012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed +## 2.5.3 + +### Security +- Emoji pack loader sanitizes pack names +- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories + ## 2.5.2 ### Security diff --git a/mix.exs b/mix.exs index 79fd9c9ef..d1cdb151d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.2"), + version: version("2.5.3"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From fc10e07ffbc9d81c7a2ac38a3f9175f2edf2bd1f Mon Sep 17 00:00:00 2001 From: Mae Date: Fri, 4 Aug 2023 22:24:17 +0100 Subject: [PATCH 081/271] Prevent XML parser from loading external entities --- lib/pleroma/web/xml.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index b699446b0..380a80ab8 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -29,7 +29,10 @@ def parse_document(text) do {doc, _rest} = text |> :binary.bin_to_list() - |> :xmerl_scan.string(quiet: true) + |> :xmerl_scan.string( + quiet: true, + fetch_fun: fn _, _ -> raise "Resolving external entities not supported" end + ) {:ok, doc} rescue From 77d57c974ad83fcea77e424d53dc16a27e5d88b6 Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Fri, 4 Aug 2023 22:24:32 +0100 Subject: [PATCH 082/271] Add unit test for external entity loading --- test/fixtures/xml_external_entities.xml | 3 +++ test/pleroma/web/web_finger_test.exs | 23 +++++++++++++++++++++++ test/pleroma/web/xml_test.exs | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 test/fixtures/xml_external_entities.xml create mode 100644 test/pleroma/web/xml_test.exs diff --git a/test/fixtures/xml_external_entities.xml b/test/fixtures/xml_external_entities.xml new file mode 100644 index 000000000..d5ff87134 --- /dev/null +++ b/test/fixtures/xml_external_entities.xml @@ -0,0 +1,3 @@ + + ]> +&xxe; diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index fafef54fe..be5e08776 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -180,5 +180,28 @@ test "respects xml content-type" do {:ok, _data} = WebFinger.finger("pekorino@pawoo.net") end + + test "refuses to process XML remote entities" do + Tesla.Mock.mock(fn + %{ + url: "https://pawoo.net/.well-known/webfinger?resource=acct:pekorino@pawoo.net" + } -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/xml_external_entities.xml"), + headers: [{"content-type", "application/xrd+xml"}] + }} + + %{url: "https://pawoo.net/.well-known/host-meta"} -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta") + }} + end) + + assert :error = WebFinger.finger("pekorino@pawoo.net") + end end end diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs new file mode 100644 index 000000000..89d4709b6 --- /dev/null +++ b/test/pleroma/web/xml_test.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Web.XMLTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.XML + + test "refuses to load external entities from XML" do + data = File.read!("test/fixtures/xml_external_entities.xml") + assert(:error == XML.parse_document(data)) + end +end From cc848b78dca51fcd7e785eb92a7a3a4d5d1c419e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 4 Aug 2023 22:44:09 -0400 Subject: [PATCH 083/271] Document and test that XXE processing is disabled https://vuln.be/post/xxe-in-erlang-and-elixir/ --- changelog.d/akkoma-xml-remote-entities.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/akkoma-xml-remote-entities.security diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security new file mode 100644 index 000000000..b3c86bee1 --- /dev/null +++ b/changelog.d/akkoma-xml-remote-entities.security @@ -0,0 +1 @@ +Restrict XML parser from processing external entitites (XXE) From b631180b38ac63029f08bef137b13231bcf57b59 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 5 Aug 2023 08:27:42 +0200 Subject: [PATCH 084/271] Release 2.5.4 --- CHANGELOG.md | 5 +++++ changelog.d/akkoma-xml-remote-entities.security | 2 +- mix.exs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 468ec1012..9d9aadc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed +## 2.5.54 + +## Security +- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem + ## 2.5.3 ### Security diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security index b3c86bee1..5e6725e5b 100644 --- a/changelog.d/akkoma-xml-remote-entities.security +++ b/changelog.d/akkoma-xml-remote-entities.security @@ -1 +1 @@ -Restrict XML parser from processing external entitites (XXE) +Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/mix.exs b/mix.exs index d1cdb151d..12f721364 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.3"), + version: version("2.5.4"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From 9effa24f308917f70276c41f91fb204b7684d942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 10 Aug 2023 22:52:38 +0200 Subject: [PATCH 085/271] Implement api/v2/instance route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- changelog.d/instance-v2.add | 1 + .../api_spec/operations/instance_operation.ex | 172 ++++++++++++++++++ .../controllers/instance_controller.ex | 5 + .../web/mastodon_api/views/instance_view.ex | 120 +++++++++--- lib/pleroma/web/router.ex | 3 + lib/pleroma/web/web_finger.ex | 2 +- .../controllers/instance_controller_test.exs | 7 + 7 files changed, 287 insertions(+), 23 deletions(-) create mode 100644 changelog.d/instance-v2.add diff --git a/changelog.d/instance-v2.add b/changelog.d/instance-v2.add new file mode 100644 index 000000000..4dd7ce8c0 --- /dev/null +++ b/changelog.d/instance-v2.add @@ -0,0 +1 @@ +Implement /api/v2/instance route \ No newline at end of file diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index a07be7e40..8e395bde8 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -23,6 +23,18 @@ def show_operation do } end + def show2_operation do + %Operation{ + tags: ["Instance misc"], + summary: "Retrieve instance information", + description: "Information about the server", + operationId: "InstanceController.show2", + responses: %{ + 200 => Operation.response("Instance", "application/json", instance2()) + } + } + end + def peers_operation do %Operation{ tags: ["Instance misc"], @@ -165,6 +177,166 @@ defp instance do } end + defp instance2 do + %Schema{ + type: :object, + properties: %{ + domain: %Schema{type: :string, description: "The domain name of the instance"}, + title: %Schema{type: :string, description: "The title of the website"}, + version: %Schema{ + type: :string, + description: "The version of Pleroma installed on the instance" + }, + source_url: %Schema{ + type: :string, + description: "The version of Pleroma installed on the instance" + }, + description: %Schema{ + type: :string, + description: "Admin-defined description of the Pleroma site" + }, + usage: %Schema{ + type: :object, + description: "Instance usage statistics", + properties: %{ + users: %Schema{ + type: :object, + description: "User count statistics", + properties: %{ + active_month: %Schema{ + type: :integer, + description: "Monthly active users" + } + } + } + } + }, + email: %Schema{ + type: :string, + description: "An email that may be contacted for any inquiries", + format: :email + }, + urls: %Schema{ + type: :object, + description: "URLs of interest for clients apps", + properties: %{} + }, + stats: %Schema{ + type: :object, + description: "Statistics about how much information the instance contains", + properties: %{ + user_count: %Schema{ + type: :integer, + description: "Users registered on this instance" + }, + status_count: %Schema{ + type: :integer, + description: "Statuses authored by users on instance" + }, + domain_count: %Schema{ + type: :integer, + description: "Domains federated with this instance" + } + } + }, + thumbnail: %Schema{ + type: :object, + properties: %{ + url: %Schema{ + type: :string, + description: "Banner image for the website", + nullable: true + } + } + }, + languages: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Primary langauges of the website and its staff" + }, + registrations: %Schema{ + type: :object, + description: "Registrations-related configuration", + properties: %{ + enabled: %Schema{ + type: :boolean, + description: "Whether registrations are enabled" + }, + approval_required: %Schema{ + type: :boolean, + description: "Whether users need to be manually approved by admin" + } + } + }, + configuration: %Schema{ + type: :object, + description: "Instance configuration", + properties: %{ + urls: %Schema{ + type: :object, + properties: %{ + streaming: %Schema{ + type: :string, + description: "Websockets address for push streaming" + } + } + }, + statuses: %Schema{ + type: :object, + description: "A map with poll limits for local statuses", + properties: %{ + max_characters: %Schema{ + type: :integer, + description: "Posts character limit (CW/Subject included in the counter)" + }, + max_media_attachments: %Schema{ + type: :integer, + description: "Media attachment limit" + } + } + }, + media_attachments: %Schema{ + type: :object, + description: "A map with poll limits for media attachments", + properties: %{ + image_size_limit: %Schema{ + type: :integer, + description: "File size limit of uploaded images" + }, + video_size_limit: %Schema{ + type: :integer, + description: "File size limit of uploaded videos" + } + } + }, + polls: %Schema{ + type: :object, + description: "A map with poll limits for local polls", + properties: %{ + max_options: %Schema{ + type: :integer, + description: "Maximum number of options." + }, + max_characters_per_option: %Schema{ + type: :integer, + description: "Maximum number of characters per option." + }, + min_expiration: %Schema{ + type: :integer, + description: "Minimum expiration time (in seconds)." + }, + max_expiration: %Schema{ + type: :integer, + description: "Maximum expiration time (in seconds)." + } + } + } + } + } + } + } + end + defp array_of_domains do %Schema{ type: :array, diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 6410e872c..3757a850a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -16,6 +16,11 @@ def show(conn, _params) do render(conn, "show.json") end + @doc "GET /api/v2/instance" + def show2(conn, _params) do + render(conn, "show2.json") + end + @doc "GET /api/v1/instance/peers" def peers(conn, _params) do json(conn, Pleroma.Stats.get_peers()) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index efd2a0af6..c987cafd6 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -14,7 +14,7 @@ def render("show.json", _) do instance = Config.get(:instance) %{ - uri: Pleroma.Web.Endpoint.url(), + uri: Pleroma.Web.WebFinger.domain(), title: Keyword.get(instance, :name), description: Keyword.get(instance, :description), short_description: Keyword.get(instance, :short_description), @@ -30,6 +30,7 @@ def render("show.json", _) do languages: Keyword.get(instance, :languages, ["en"]), registrations: Keyword.get(instance, :registrations_open), approval_required: Keyword.get(instance, :account_approval_required), + configuration: configuration(), # Extra (not present in Mastodon): max_toot_chars: Keyword.get(instance, :limit), max_media_attachments: Keyword.get(instance, :max_media_attachments), @@ -41,19 +42,38 @@ def render("show.json", _) do background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image), shout_limit: Config.get([:shout, :limit]), description_limit: Keyword.get(instance, :description_limit), - pleroma: %{ - metadata: %{ - account_activation_required: Keyword.get(instance, :account_activation_required), - features: features(), - federation: federation(), - fields_limits: fields_limits(), - post_formats: Config.get([:instance, :allowed_post_formats]), - birthday_required: Config.get([:instance, :birthday_required]), - birthday_min_age: Config.get([:instance, :birthday_min_age]) - }, - stats: %{mau: Pleroma.User.active_user_count()}, - vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } + pleroma: pleroma_configuration(instance) + } + end + + def render("show2.json", _) do + instance = Config.get(:instance) + + %{ + domain: Pleroma.Web.WebFinger.domain(), + title: Keyword.get(instance, :name), + version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", + source_url: Pleroma.Application.repository(), + description: Keyword.get(instance, :short_description), + usage: %{users: %{active_month: Pleroma.User.active_user_count()}}, + thumbnail: %{ + url: + URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail)) + |> to_string + }, + languages: Keyword.get(instance, :languages, ["en"]), + configuration: configuration2(), + registrations: %{ + enabled: Keyword.get(instance, :registrations_open), + approval_required: Keyword.get(instance, :account_approval_required), + message: nil + }, + contact: %{ + email: Keyword.get(instance, :email), + account: nil + }, + # Extra (not present in Mastodon): + pleroma: pleroma_configuration2(instance) } end @@ -68,6 +88,7 @@ def features do "shareable_emoji_packs", "multifetch", "pleroma:api/v1/notifications:include_types_filter", + "quote_posting", "editing", if Config.get([:activitypub, :blockers_visible]) do "blockers_visible" @@ -78,13 +99,6 @@ def features do if Config.get([:gopher, :enabled]) do "gopher" end, - # backwards compat - if Config.get([:shout, :enabled]) do - "chat" - end, - if Config.get([:shout, :enabled]) do - "shout" - end, if Config.get([:instance, :allow_relay]) do "relay" end, @@ -94,6 +108,7 @@ def features do "pleroma_emoji_reactions", "pleroma_custom_emoji_reactions", "pleroma_chat_messages", + "email_list", if Config.get([:instance, :show_reactions]) do "exposable_reactions" end, @@ -132,7 +147,7 @@ def federation do |> Map.put(:enabled, Config.get([:instance, :federating])) end - def fields_limits do + defp fields_limits do %{ max_fields: Config.get([:instance, :max_account_fields]), max_remote_fields: Config.get([:instance, :max_remote_account_fields]), @@ -140,4 +155,65 @@ def fields_limits do value_length: Config.get([:instance, :account_field_value_length]) } end + + defp configuration do + %{ + statuses: %{ + max_characters: Config.get([:instance, :limit]), + max_media_attachments: Config.get([:instance, :max_media_attachments]) + }, + media_attachments: %{ + image_size_limit: Config.get([:instance, :upload_limit]), + video_size_limit: Config.get([:instance, :upload_limit]) + }, + polls: %{ + max_options: Config.get([:instance, :poll_limits, :max_options]), + max_characters_per_option: Config.get([:instance, :poll_limits, :max_option_chars]), + min_expiration: Config.get([:instance, :poll_limits, :min_expiration]), + max_expiration: Config.get([:instance, :poll_limits, :max_expiration]) + } + } + end + + defp configuration2 do + configuration() + |> Map.merge(%{ + urls: %{streaming: Pleroma.Web.Endpoint.websocket_url()} + }) + end + + defp pleroma_configuration(instance) do + %{ + metadata: %{ + account_activation_required: Keyword.get(instance, :account_activation_required), + features: features(), + federation: federation(), + fields_limits: fields_limits(), + post_formats: Config.get([:instance, :allowed_post_formats]), + birthday_required: Config.get([:instance, :birthday_required]), + birthday_min_age: Config.get([:instance, :birthday_min_age]) + }, + stats: %{mau: Pleroma.User.active_user_count()}, + vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + end + + defp pleroma_configuration2(instance) do + configuration = pleroma_configuration(instance) + + configuration + |> Map.merge(%{ + metadata: + configuration.metadata + |> Map.merge(%{ + avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), + background_upload_limit: Keyword.get(instance, :background_upload_limit), + banner_upload_limit: Keyword.get(instance, :banner_upload_limit), + background_image: + Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image), + description_limit: Keyword.get(instance, :description_limit), + shout_limit: Config.get([:shout, :limit]) + }) + }) + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6b9e158a3..5362100cc 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -774,11 +774,14 @@ defmodule Pleroma.Web.Router do scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through(:api) + get("/search", SearchController, :search2) post("/media", MediaController, :create2) get("/suggestions", SuggestionController, :index2) + + get("/instance", InstanceController, :show2) end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index f95dc2458..b28fad8d1 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -96,7 +96,7 @@ def represent_user(user, "XML") do |> XmlBuilder.to_doc() end - defp domain do + def domain do Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host() end diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index a556ef6a8..2243b0d4a 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -106,4 +106,11 @@ test "instance languages", %{conn: conn} do |> get("/api/v1/instance") |> json_response_and_validate_schema(200) end + + test "get instance information v2", %{conn: conn} do + clear_config([:auth, :oauth_consumer_strategies], []) + + assert get(conn, "/api/v2/instance") + |> json_response_and_validate_schema(200) + end end From 79e46ce73f782a83654986adc9fd0b256be6a2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 11 Aug 2023 13:57:22 +0200 Subject: [PATCH 086/271] InstanceView: Add common_information function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../web/mastodon_api/views/instance_view.ex | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index c987cafd6..e7a2feb7f 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -13,12 +13,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do def render("show.json", _) do instance = Config.get(:instance) - %{ + common_information(instance) + |> Map.merge(%{ uri: Pleroma.Web.WebFinger.domain(), - title: Keyword.get(instance, :name), description: Keyword.get(instance, :description), short_description: Keyword.get(instance, :short_description), - version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", email: Keyword.get(instance, :email), urls: %{ streaming_api: Pleroma.Web.Endpoint.websocket_url() @@ -27,7 +26,6 @@ def render("show.json", _) do thumbnail: URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail)) |> to_string, - languages: Keyword.get(instance, :languages, ["en"]), registrations: Keyword.get(instance, :registrations_open), approval_required: Keyword.get(instance, :account_approval_required), configuration: configuration(), @@ -43,16 +41,15 @@ def render("show.json", _) do shout_limit: Config.get([:shout, :limit]), description_limit: Keyword.get(instance, :description_limit), pleroma: pleroma_configuration(instance) - } + }) end def render("show2.json", _) do instance = Config.get(:instance) - %{ + common_information(instance) + |> Map.merge(%{ domain: Pleroma.Web.WebFinger.domain(), - title: Keyword.get(instance, :name), - version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", source_url: Pleroma.Application.repository(), description: Keyword.get(instance, :short_description), usage: %{users: %{active_month: Pleroma.User.active_user_count()}}, @@ -61,7 +58,6 @@ def render("show2.json", _) do URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail)) |> to_string }, - languages: Keyword.get(instance, :languages, ["en"]), configuration: configuration2(), registrations: %{ enabled: Keyword.get(instance, :registrations_open), @@ -74,6 +70,14 @@ def render("show2.json", _) do }, # Extra (not present in Mastodon): pleroma: pleroma_configuration2(instance) + }) + end + + defp common_information(instance) do + %{ + title: Keyword.get(instance, :name), + version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", + languages: Keyword.get(instance, :languages, ["en"]) } end From 535a5ecad04c9c49105a77e7025fe9f4b4d23ba6 Mon Sep 17 00:00:00 2001 From: Mint Date: Sat, 2 Sep 2023 01:43:25 +0300 Subject: [PATCH 087/271] CommonAPI: Prevent users from accessing media of other users commit 1afde067b12ad0062c1820091ea9b0a680819281 upstream. --- .../check-attachment-attribution.security | 1 + lib/pleroma/scheduled_activity.ex | 6 ++- lib/pleroma/web/common_api.ex | 12 ++++++ lib/pleroma/web/common_api/activity_draft.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 31 ++++++++------ test/pleroma/web/common_api/utils_test.exs | 41 +++++++++++++------ test/pleroma/web/common_api_test.exs | 18 ++++++++ .../views/scheduled_activity_view_test.exs | 2 +- .../chat_message_reference_view_test.exs | 2 +- 9 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 changelog.d/check-attachment-attribution.security diff --git a/changelog.d/check-attachment-attribution.security b/changelog.d/check-attachment-attribution.security new file mode 100644 index 000000000..e0e46525b --- /dev/null +++ b/changelog.d/check-attachment-attribution.security @@ -0,0 +1 @@ +CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex index a7be58512..0ed51ad07 100644 --- a/lib/pleroma/scheduled_activity.ex +++ b/lib/pleroma/scheduled_activity.ex @@ -40,7 +40,11 @@ defp with_media_attachments( %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset ) when is_list(media_ids) do - media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids}) + media_attachments = + Utils.attachments_from_ids( + %{media_ids: media_ids}, + User.get_cached_by_id(changeset.data.user_id) + ) params = params diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 89cc0d6fe..44eb00075 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -33,6 +33,7 @@ def block(blocker, blocked) do def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_attachment_attribution(maybe_attachment, user), :ok <- validate_chat_content_length(content, !!maybe_attachment), {_, {:ok, chat_message_data, _meta}} <- {:build_object, @@ -71,6 +72,17 @@ defp format_chat_content(content) do text end + defp validate_chat_attachment_attribution(nil, _), do: :ok + + defp validate_chat_attachment_attribution(attachment, user) do + with :ok <- Object.authorize_access(attachment, user) do + :ok + else + e -> + e + end + end + defp validate_chat_content_length(_, true), do: :ok defp validate_chat_content_length(nil, false), do: {:error, :no_content} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 9af635da8..63ed48a27 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -111,7 +111,7 @@ defp full_payload(%{status: status, summary: summary} = draft) do end defp attachments(%{params: params} = draft) do - attachments = Utils.attachments_from_ids(params) + attachments = Utils.attachments_from_ids(params, draft.user) draft = %__MODULE__{draft | attachments: attachments} case Utils.validate_attachments_count(attachments) do diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index ff0814329..6410815ea 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -23,21 +23,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do require Logger require Pleroma.Constants - def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do - attachments_from_ids_descs(ids, desc) + def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do + attachments_from_ids_descs(ids, desc, user) end - def attachments_from_ids(%{media_ids: ids}) do - attachments_from_ids_no_descs(ids) + def attachments_from_ids(%{media_ids: ids}, user) do + attachments_from_ids_no_descs(ids, user) end - def attachments_from_ids(_), do: [] + def attachments_from_ids(_, _), do: [] - def attachments_from_ids_no_descs([]), do: [] + def attachments_from_ids_no_descs([], _), do: [] - def attachments_from_ids_no_descs(ids) do + def attachments_from_ids_no_descs(ids, user) do Enum.map(ids, fn media_id -> - case get_attachment(media_id) do + case get_attachment(media_id, user) do %Object{data: data} -> data _ -> nil end @@ -45,21 +45,26 @@ def attachments_from_ids_no_descs(ids) do |> Enum.reject(&is_nil/1) end - def attachments_from_ids_descs([], _), do: [] + def attachments_from_ids_descs([], _, _), do: [] - def attachments_from_ids_descs(ids, descs_str) do + def attachments_from_ids_descs(ids, descs_str, user) do {_, descs} = Jason.decode(descs_str) Enum.map(ids, fn media_id -> - with %Object{data: data} <- get_attachment(media_id) do + with %Object{data: data} <- get_attachment(media_id, user) do Map.put(data, "name", descs[media_id]) end end) |> Enum.reject(&is_nil/1) end - defp get_attachment(media_id) do - Repo.get(Object, media_id) + defp get_attachment(media_id, user) do + with %Object{data: _data} = object <- Repo.get(Object, media_id), + :ok <- Object.authorize_access(object, user) do + object + else + _ -> nil + end end @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())} diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index d309c6ded..c52d3e9c5 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -586,41 +586,56 @@ test "returns recipients when object not found" do end end - describe "attachments_from_ids_descs/2" do + describe "attachments_from_ids_descs/3" do test "returns [] when attachment ids is empty" do - assert Utils.attachments_from_ids_descs([], "{}") == [] + assert Utils.attachments_from_ids_descs([], "{}", nil) == [] end test "returns list attachments with desc" do - object = insert(:note) + user = insert(:user) + object = insert(:note, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc, user) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end end - describe "attachments_from_ids/1" do + describe "attachments_from_ids/2" do test "returns attachments with descs" do - object = insert(:note) + user = insert(:user) + object = insert(:note, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids(%{ - media_ids: ["#{object.id}"], - descriptions: desc - }) == [ + assert Utils.attachments_from_ids( + %{ + media_ids: ["#{object.id}"], + descriptions: desc + }, + user + ) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end test "returns attachments without descs" do - object = insert(:note) - assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] + user = insert(:user) + object = insert(:note, %{user: user}) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user) == [object.data] end test "returns [] when not pass media_ids" do - assert Utils.attachments_from_ids(%{}) == [] + assert Utils.attachments_from_ids(%{}, nil) == [] + end + + test "returns [] when media_ids not belong to current user" do + user = insert(:user) + user2 = insert(:user) + + object = insert(:attachment, %{user: user}) + + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user2) == [] end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 5c9103e9f..e60691995 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -279,6 +279,24 @@ test "it reject messages via MRF" do assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == CommonAPI.post_chat_message(author, recipient, "GNO/Linux") end + + test "it reject messages with attachments not belonging to user" do + author = insert(:user) + not_author = insert(:user) + recipient = author + + attachment = insert(:attachment, %{user: not_author}) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123", + media_id: attachment.id + ) + + assert message == :forbidden + end end describe "unblocking" do diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs index e5e510d33..07a65a3bc 100644 --- a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -48,7 +48,7 @@ test "A scheduled activity with a media attachment" do id: to_string(scheduled_activity.id), media_attachments: %{media_ids: [upload.id]} - |> Utils.attachments_from_ids() + |> Utils.attachments_from_ids(user) |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), params: %{ in_reply_to_id: to_string(activity.id), diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs index 017c9c5c0..7ab3f5acd 100644 --- a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -24,7 +24,7 @@ test "it displays a chat message" do filename: "an_image.jpg" } - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, upload} = ActivityPub.upload(file, actor: recipient.ap_id) {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:", idempotency_key: "123") From 385492577d11e9667064d7f7e0dacdc00457064a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 23 Dec 2022 18:46:14 +0100 Subject: [PATCH 088/271] mix: version 2.5.5 --- CHANGELOG.md | 7 ++++++- mix.exs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9aadc6e..32ec440de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed -## 2.5.54 +## 2.5.5 + +## Security +- Prevent users from accessing media of other users by creating a status with reused attachment ID + +## 2.5.4 ## Security - Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/mix.exs b/mix.exs index 12f721364..e2aac0fc5 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.4"), + version: version("2.5.5"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From 28ef5ebd3c7c2fc083424dbf4434392fcd2a7aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 7 Sep 2023 15:00:24 +0200 Subject: [PATCH 089/271] Update InstanceView.features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index e7a2feb7f..c1cecf6e6 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -92,7 +92,6 @@ def features do "shareable_emoji_packs", "multifetch", "pleroma:api/v1/notifications:include_types_filter", - "quote_posting", "editing", if Config.get([:activitypub, :blockers_visible]) do "blockers_visible" @@ -103,6 +102,13 @@ def features do if Config.get([:gopher, :enabled]) do "gopher" end, + # backwards compat + if Config.get([:shout, :enabled]) do + "chat" + end, + if Config.get([:shout, :enabled]) do + "shout" + end, if Config.get([:instance, :allow_relay]) do "relay" end, @@ -112,7 +118,6 @@ def features do "pleroma_emoji_reactions", "pleroma_custom_emoji_reactions", "pleroma_chat_messages", - "email_list", if Config.get([:instance, :show_reactions]) do "exposable_reactions" end, From 2f6fc6a7ab0e757abfe9ec535842a12b887d2fe6 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 24 Sep 2023 22:52:41 +0200 Subject: [PATCH 090/271] TwitterAPI: Return proper error when healthcheck is disabled --- changelog.d/healthcheck-disabled-error.fix | 1 + lib/pleroma/web/twitter_api/controllers/util_controller.ex | 5 ++++- test/pleroma/web/twitter_api/util_controller_test.exs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/healthcheck-disabled-error.fix diff --git a/changelog.d/healthcheck-disabled-error.fix b/changelog.d/healthcheck-disabled-error.fix new file mode 100644 index 000000000..984384a52 --- /dev/null +++ b/changelog.d/healthcheck-disabled-error.fix @@ -0,0 +1 @@ +TwitterAPI: Return proper error when healthcheck is disabled diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index d5a24ae6c..ca8a98960 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -345,13 +345,16 @@ def captcha(conn, _params) do end def healthcheck(conn, _params) do - with true <- Config.get([:instance, :healthcheck]), + with {:cfg, true} <- {:cfg, Config.get([:instance, :healthcheck])}, %{healthy: true} = info <- Healthcheck.system_info() do json(conn, info) else %{healthy: false} = info -> service_unavailable(conn, info) + {:cfg, false} -> + service_unavailable(conn, %{"error" => "Healthcheck disabled"}) + _ -> service_unavailable(conn, %{}) end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs index a4da23635..d06ae71aa 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -106,7 +106,7 @@ test "returns 503 when healthcheck disabled", %{conn: conn} do |> get("/api/pleroma/healthcheck") |> json_response_and_validate_schema(503) - assert response == %{} + assert response == %{"error" => "Healthcheck disabled"} end test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do From a2a69709b51692be307940c79d0befdd3c9678bb Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 24 Oct 2023 19:57:31 -0400 Subject: [PATCH 091/271] Bump version to 2.6.0 --- CHANGELOG.md | 38 +++++++++++++++++-- changelog.d/2023-06-deps-update.skip | 0 changelog.d/3126.fix | 1 - changelog.d/3739.skip | 0 changelog.d/3801.fix | 1 - changelog.d/3831.skip | 0 changelog.d/3848.add | 1 - changelog.d/3870.skip | 0 changelog.d/3872.remove | 1 - changelog.d/3873.fix | 1 - changelog.d/3874.remove | 1 - changelog.d/3876.skip | 0 changelog.d/3877.skip | 0 changelog.d/3878.skip | 0 changelog.d/3879.fix | 1 - changelog.d/3880.remove | 1 - changelog.d/3882.add | 1 - changelog.d/3883.fix | 1 - changelog.d/3884.fix | 1 - changelog.d/3885.fix | 1 - changelog.d/3888.fix | 1 - changelog.d/3891.fix | 1 - changelog.d/3893.skip | 0 changelog.d/3897.add | 1 - changelog.d/3899.skip | 0 changelog.d/3901.security | 1 - changelog.d/3902.skip | 0 changelog.d/3909.skip | 0 .../akkoma-xml-remote-entities.security | 1 - changelog.d/amd64-runner.skip | 0 changelog.d/attachment-type-check.fix | 1 - changelog.d/changelog-improve.skip | 0 .../check-attachment-attribution.security | 1 - changelog.d/delete-status-of-banned-user.fix | 1 - changelog.d/deprecate-scrobbles.remove | 1 - .../disable-xml-entity-resolution.security | 1 - changelog.d/distro-docs-elixir-1.11.skip | 0 changelog.d/dockerfile-config-perms.fix | 1 - changelog.d/emoji-pack-sanitization.security | 1 - changelog.d/emoji-policy.add | 1 - ...d-collection-shouldnt-break-user-fetch.fix | 1 - changelog.d/fix-object-test.fix | 1 - changelog.d/gentoo_otp.skip | 0 changelog.d/gentoo_otp_hotfix.skip | 0 changelog.d/gentoo_otp_intro.skip | 0 .../handle-report-from-deactivated-user.fix | 1 - changelog.d/lint.skip | 0 changelog.d/media-altdomain.skip | 0 changelog.d/no_new_privs.add | 1 - changelog.d/otp_perms.security | 1 - changelog.d/pipeline-triggers.skip | 0 ...revent-bypassing-authorized-fetch-mode.fix | 1 - changelog.d/punycode-mention.fix | 1 - changelog.d/quote.add | 1 - changelog.d/testfix-system-config-use.skip | 0 changelog.d/unified-streaming.add | 1 - .../update-credentials-limit-error.fix | 1 - mix.exs | 2 +- 58 files changed, 35 insertions(+), 40 deletions(-) delete mode 100644 changelog.d/2023-06-deps-update.skip delete mode 100644 changelog.d/3126.fix delete mode 100644 changelog.d/3739.skip delete mode 100644 changelog.d/3801.fix delete mode 100644 changelog.d/3831.skip delete mode 100644 changelog.d/3848.add delete mode 100644 changelog.d/3870.skip delete mode 100644 changelog.d/3872.remove delete mode 100644 changelog.d/3873.fix delete mode 100644 changelog.d/3874.remove delete mode 100644 changelog.d/3876.skip delete mode 100644 changelog.d/3877.skip delete mode 100644 changelog.d/3878.skip delete mode 100644 changelog.d/3879.fix delete mode 100644 changelog.d/3880.remove delete mode 100644 changelog.d/3882.add delete mode 100644 changelog.d/3883.fix delete mode 100644 changelog.d/3884.fix delete mode 100644 changelog.d/3885.fix delete mode 100644 changelog.d/3888.fix delete mode 100644 changelog.d/3891.fix delete mode 100644 changelog.d/3893.skip delete mode 100644 changelog.d/3897.add delete mode 100644 changelog.d/3899.skip delete mode 100644 changelog.d/3901.security delete mode 100644 changelog.d/3902.skip delete mode 100644 changelog.d/3909.skip delete mode 100644 changelog.d/akkoma-xml-remote-entities.security delete mode 100644 changelog.d/amd64-runner.skip delete mode 100644 changelog.d/attachment-type-check.fix delete mode 100644 changelog.d/changelog-improve.skip delete mode 100644 changelog.d/check-attachment-attribution.security delete mode 100644 changelog.d/delete-status-of-banned-user.fix delete mode 100644 changelog.d/deprecate-scrobbles.remove delete mode 100644 changelog.d/disable-xml-entity-resolution.security delete mode 100644 changelog.d/distro-docs-elixir-1.11.skip delete mode 100644 changelog.d/dockerfile-config-perms.fix delete mode 100644 changelog.d/emoji-pack-sanitization.security delete mode 100644 changelog.d/emoji-policy.add delete mode 100644 changelog.d/featured-collection-shouldnt-break-user-fetch.fix delete mode 100644 changelog.d/fix-object-test.fix delete mode 100644 changelog.d/gentoo_otp.skip delete mode 100644 changelog.d/gentoo_otp_hotfix.skip delete mode 100644 changelog.d/gentoo_otp_intro.skip delete mode 100644 changelog.d/handle-report-from-deactivated-user.fix delete mode 100644 changelog.d/lint.skip delete mode 100644 changelog.d/media-altdomain.skip delete mode 100644 changelog.d/no_new_privs.add delete mode 100644 changelog.d/otp_perms.security delete mode 100644 changelog.d/pipeline-triggers.skip delete mode 100644 changelog.d/prevent-bypassing-authorized-fetch-mode.fix delete mode 100644 changelog.d/punycode-mention.fix delete mode 100644 changelog.d/quote.add delete mode 100644 changelog.d/testfix-system-config-use.skip delete mode 100644 changelog.d/unified-streaming.add delete mode 100644 changelog.d/update-credentials-limit-error.fix diff --git a/CHANGELOG.md b/CHANGELOG.md index 65acfad3e..211e611ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,49 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## Unreleased - -### Changed +## 2.6.0 +### Security +- Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes. +- CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID +- Disable XML entity resolution completely to fix a dos vulnerability ### Added - Support for Image activities, namely from Hubzilla +- Add OAuth scope descriptions +- Allow lang attribute in status text +- OnlyMedia Upload Filter +- Implement MRF policy to reject or delist according to emojis +- (hardening) Add no_new_privs=yes to OpenRC service files +- Implement quotes +- Add unified streaming endpoint ### Fixed - - rel="me" was missing its cache +- MediaProxy responses now return a sandbox CSP header +- Filter context activities using Visibility.visible_for_user? +- UploadedMedia: Add missing disposition_type to Content-Disposition +- fix not being able to fetch flash file from remote instance +- Fix abnormal behaviour when refetching a poll +- Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects" +- Fix opengraph and twitter card meta tags +- ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts +- OEmbed HTML tags are now filtered +- Restrict attachments to only uploaded files only +- Fix error 404 when deleting status of a banned user +- Fix config ownership in dockerfile to pass restriction test +- Fix user fetch completely broken if featured collection is not in a supported form +- Correctly handle the situation when a poll has both "anyOf" and "oneOf" but one of them being empty +- Fix handling report from a deactivated user +- Prevent using the .json format to bypass authorized fetch mode +- Fix mentioning punycode domains when using Markdown +- Show more informative errors when profile exceeds char limits ### Removed - BREAKING: Support for passwords generated with `crypt(3)` (Gnu Social migration artifact) +- remove BBS/SSH feature, replaced by an external bridge. +- Remove a few unused indexes. +- Cleanup OStatus-era user upgrades and ap_enabled indicator +- Deprecate Pleroma's audio scrobbling ## 2.5.4 diff --git a/changelog.d/2023-06-deps-update.skip b/changelog.d/2023-06-deps-update.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3126.fix b/changelog.d/3126.fix deleted file mode 100644 index 91d396c89..000000000 --- a/changelog.d/3126.fix +++ /dev/null @@ -1 +0,0 @@ -MediaProxy responses now return a sandbox CSP header diff --git a/changelog.d/3739.skip b/changelog.d/3739.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3801.fix b/changelog.d/3801.fix deleted file mode 100644 index 8c2ec0199..000000000 --- a/changelog.d/3801.fix +++ /dev/null @@ -1 +0,0 @@ -Filter context activities using Visibility.visible_for_user? diff --git a/changelog.d/3831.skip b/changelog.d/3831.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3848.add b/changelog.d/3848.add deleted file mode 100644 index d7b1b0a84..000000000 --- a/changelog.d/3848.add +++ /dev/null @@ -1 +0,0 @@ -Add OAuth scope descriptions diff --git a/changelog.d/3870.skip b/changelog.d/3870.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3872.remove b/changelog.d/3872.remove deleted file mode 100644 index 54cbb660e..000000000 --- a/changelog.d/3872.remove +++ /dev/null @@ -1 +0,0 @@ -remove BBS/SSH feature, replaced by an external bridge. \ No newline at end of file diff --git a/changelog.d/3873.fix b/changelog.d/3873.fix deleted file mode 100644 index 4699f7b58..000000000 --- a/changelog.d/3873.fix +++ /dev/null @@ -1 +0,0 @@ -UploadedMedia: Add missing disposition_type to Content-Disposition \ No newline at end of file diff --git a/changelog.d/3874.remove b/changelog.d/3874.remove deleted file mode 100644 index a81f744bf..000000000 --- a/changelog.d/3874.remove +++ /dev/null @@ -1 +0,0 @@ -Remove a few unused indexes. diff --git a/changelog.d/3876.skip b/changelog.d/3876.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3877.skip b/changelog.d/3877.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3878.skip b/changelog.d/3878.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3879.fix b/changelog.d/3879.fix deleted file mode 100644 index 7c58cc3c2..000000000 --- a/changelog.d/3879.fix +++ /dev/null @@ -1 +0,0 @@ -fix not being able to fetch flash file from remote instance \ No newline at end of file diff --git a/changelog.d/3880.remove b/changelog.d/3880.remove deleted file mode 100644 index 113c76c85..000000000 --- a/changelog.d/3880.remove +++ /dev/null @@ -1 +0,0 @@ -Cleanup OStatus-era user upgrades and ap_enabled indicator \ No newline at end of file diff --git a/changelog.d/3882.add b/changelog.d/3882.add deleted file mode 100644 index 4712de1dc..000000000 --- a/changelog.d/3882.add +++ /dev/null @@ -1 +0,0 @@ -Allow lang attribute in status text diff --git a/changelog.d/3883.fix b/changelog.d/3883.fix deleted file mode 100644 index 6824f2013..000000000 --- a/changelog.d/3883.fix +++ /dev/null @@ -1 +0,0 @@ -Fix abnormal behaviour when refetching a poll diff --git a/changelog.d/3884.fix b/changelog.d/3884.fix deleted file mode 100644 index f8dbb2bbf..000000000 --- a/changelog.d/3884.fix +++ /dev/null @@ -1 +0,0 @@ -Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects" \ No newline at end of file diff --git a/changelog.d/3885.fix b/changelog.d/3885.fix deleted file mode 100644 index c5fbb0ed4..000000000 --- a/changelog.d/3885.fix +++ /dev/null @@ -1 +0,0 @@ -Fix opengraph and twitter card meta tags diff --git a/changelog.d/3888.fix b/changelog.d/3888.fix deleted file mode 100644 index 886aa7b39..000000000 --- a/changelog.d/3888.fix +++ /dev/null @@ -1 +0,0 @@ -ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts \ No newline at end of file diff --git a/changelog.d/3891.fix b/changelog.d/3891.fix deleted file mode 100644 index f1fb62d82..000000000 --- a/changelog.d/3891.fix +++ /dev/null @@ -1 +0,0 @@ -OEmbed HTML tags are now filtered diff --git a/changelog.d/3893.skip b/changelog.d/3893.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3897.add b/changelog.d/3897.add deleted file mode 100644 index 5c4402f45..000000000 --- a/changelog.d/3897.add +++ /dev/null @@ -1 +0,0 @@ -OnlyMedia Upload Filter diff --git a/changelog.d/3899.skip b/changelog.d/3899.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3901.security b/changelog.d/3901.security deleted file mode 100644 index a3d8bd01f..000000000 --- a/changelog.d/3901.security +++ /dev/null @@ -1 +0,0 @@ -Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes. diff --git a/changelog.d/3902.skip b/changelog.d/3902.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3909.skip b/changelog.d/3909.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security deleted file mode 100644 index 5e6725e5b..000000000 --- a/changelog.d/akkoma-xml-remote-entities.security +++ /dev/null @@ -1 +0,0 @@ -Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/changelog.d/amd64-runner.skip b/changelog.d/amd64-runner.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/attachment-type-check.fix b/changelog.d/attachment-type-check.fix deleted file mode 100644 index 9e14b75f1..000000000 --- a/changelog.d/attachment-type-check.fix +++ /dev/null @@ -1 +0,0 @@ -Restrict attachments to only uploaded files only diff --git a/changelog.d/changelog-improve.skip b/changelog.d/changelog-improve.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/check-attachment-attribution.security b/changelog.d/check-attachment-attribution.security deleted file mode 100644 index e0e46525b..000000000 --- a/changelog.d/check-attachment-attribution.security +++ /dev/null @@ -1 +0,0 @@ -CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID diff --git a/changelog.d/delete-status-of-banned-user.fix b/changelog.d/delete-status-of-banned-user.fix deleted file mode 100644 index 1fa6a29d8..000000000 --- a/changelog.d/delete-status-of-banned-user.fix +++ /dev/null @@ -1 +0,0 @@ -Fix error 404 when deleting status of a banned user diff --git a/changelog.d/deprecate-scrobbles.remove b/changelog.d/deprecate-scrobbles.remove deleted file mode 100644 index c453a9784..000000000 --- a/changelog.d/deprecate-scrobbles.remove +++ /dev/null @@ -1 +0,0 @@ -Deprecate Pleroma's audio scrobbling diff --git a/changelog.d/disable-xml-entity-resolution.security b/changelog.d/disable-xml-entity-resolution.security deleted file mode 100644 index db8e12f67..000000000 --- a/changelog.d/disable-xml-entity-resolution.security +++ /dev/null @@ -1 +0,0 @@ -Disable XML entity resolution completely to fix a dos vulnerability diff --git a/changelog.d/distro-docs-elixir-1.11.skip b/changelog.d/distro-docs-elixir-1.11.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/dockerfile-config-perms.fix b/changelog.d/dockerfile-config-perms.fix deleted file mode 100644 index 49ea5becb..000000000 --- a/changelog.d/dockerfile-config-perms.fix +++ /dev/null @@ -1 +0,0 @@ -- Fix config ownership in dockerfile to pass restriction test diff --git a/changelog.d/emoji-pack-sanitization.security b/changelog.d/emoji-pack-sanitization.security deleted file mode 100644 index f3218abd4..000000000 --- a/changelog.d/emoji-pack-sanitization.security +++ /dev/null @@ -1 +0,0 @@ -Emoji pack loader sanitizes pack names diff --git a/changelog.d/emoji-policy.add b/changelog.d/emoji-policy.add deleted file mode 100644 index 45510c4f6..000000000 --- a/changelog.d/emoji-policy.add +++ /dev/null @@ -1 +0,0 @@ -Implement MRF policy to reject or delist according to emojis diff --git a/changelog.d/featured-collection-shouldnt-break-user-fetch.fix b/changelog.d/featured-collection-shouldnt-break-user-fetch.fix deleted file mode 100644 index e8ce288cc..000000000 --- a/changelog.d/featured-collection-shouldnt-break-user-fetch.fix +++ /dev/null @@ -1 +0,0 @@ -Fix user fetch completely broken if featured collection is not in a supported form diff --git a/changelog.d/fix-object-test.fix b/changelog.d/fix-object-test.fix deleted file mode 100644 index 5eea719f0..000000000 --- a/changelog.d/fix-object-test.fix +++ /dev/null @@ -1 +0,0 @@ -Correctly handle the situation when a poll has both "anyOf" and "oneOf" but one of them being empty diff --git a/changelog.d/gentoo_otp.skip b/changelog.d/gentoo_otp.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/gentoo_otp_hotfix.skip b/changelog.d/gentoo_otp_hotfix.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/gentoo_otp_intro.skip b/changelog.d/gentoo_otp_intro.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/handle-report-from-deactivated-user.fix b/changelog.d/handle-report-from-deactivated-user.fix deleted file mode 100644 index 6692d1aa8..000000000 --- a/changelog.d/handle-report-from-deactivated-user.fix +++ /dev/null @@ -1 +0,0 @@ -Fix handling report from a deactivated user diff --git a/changelog.d/lint.skip b/changelog.d/lint.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/media-altdomain.skip b/changelog.d/media-altdomain.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/no_new_privs.add b/changelog.d/no_new_privs.add deleted file mode 100644 index b67396a4b..000000000 --- a/changelog.d/no_new_privs.add +++ /dev/null @@ -1 +0,0 @@ -(hardening) Add no_new_privs=yes to OpenRC service files diff --git a/changelog.d/otp_perms.security b/changelog.d/otp_perms.security deleted file mode 100644 index a3da1c677..000000000 --- a/changelog.d/otp_perms.security +++ /dev/null @@ -1 +0,0 @@ -- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories \ No newline at end of file diff --git a/changelog.d/pipeline-triggers.skip b/changelog.d/pipeline-triggers.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/prevent-bypassing-authorized-fetch-mode.fix b/changelog.d/prevent-bypassing-authorized-fetch-mode.fix deleted file mode 100644 index 12f7260d7..000000000 --- a/changelog.d/prevent-bypassing-authorized-fetch-mode.fix +++ /dev/null @@ -1 +0,0 @@ -Prevent using the .json format to bypass authorized fetch mode \ No newline at end of file diff --git a/changelog.d/punycode-mention.fix b/changelog.d/punycode-mention.fix deleted file mode 100644 index f013c2dac..000000000 --- a/changelog.d/punycode-mention.fix +++ /dev/null @@ -1 +0,0 @@ -Fix mentioning punycode domains when using Markdown diff --git a/changelog.d/quote.add b/changelog.d/quote.add deleted file mode 100644 index 1c368ae75..000000000 --- a/changelog.d/quote.add +++ /dev/null @@ -1 +0,0 @@ -Implement quotes diff --git a/changelog.d/testfix-system-config-use.skip b/changelog.d/testfix-system-config-use.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/unified-streaming.add b/changelog.d/unified-streaming.add deleted file mode 100644 index 84821fcc8..000000000 --- a/changelog.d/unified-streaming.add +++ /dev/null @@ -1 +0,0 @@ -Add unified streaming endpoint diff --git a/changelog.d/update-credentials-limit-error.fix b/changelog.d/update-credentials-limit-error.fix deleted file mode 100644 index 7682f958e..000000000 --- a/changelog.d/update-credentials-limit-error.fix +++ /dev/null @@ -1 +0,0 @@ -Show more informative errors when profile exceeds char limits diff --git a/mix.exs b/mix.exs index b071e7c7b..082b39e55 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.54"), + version: version("2.6.0"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] ++ Mix.compilers(), From 6a13c2d180177692b976b179f0bd86f8f93e2803 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 25 Oct 2023 20:44:47 -0400 Subject: [PATCH 092/271] Add collect-changelog script --- .gitlab/merge_request_templates/Release.md | 2 +- tools/collect-changelog | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100755 tools/collect-changelog diff --git a/.gitlab/merge_request_templates/Release.md b/.gitlab/merge_request_templates/Release.md index 9638d6d11..e57556e6c 100644 --- a/.gitlab/merge_request_templates/Release.md +++ b/.gitlab/merge_request_templates/Release.md @@ -1,6 +1,6 @@ ### Release checklist * [ ] Bump version in `mix.exs` -* [ ] Compile a changelog +* [ ] Compile a changelog with the `tools/collect-changelog` script * [ ] Create an MR with an announcement to pleroma.social #### post-merge * [ ] Tag the release on the merge commit diff --git a/tools/collect-changelog b/tools/collect-changelog new file mode 100755 index 000000000..1e12d5640 --- /dev/null +++ b/tools/collect-changelog @@ -0,0 +1,27 @@ +#!/bin/sh + +collectType() { + local suffix="$1" + local header="$2" + local printed=0 + for file in changelog.d/*."$suffix"; do + if [ '!' -f "$file" ]; then + continue + fi + if [ "$printed" = 0 ]; then + echo + echo "### $header" + printed=1 + fi + # Normalize any trailing newlines/spaces, etc. + echo "- $(cat "$file")" + done +} + +collectType security Security +collectType change Changed +collectType add Added +collectType fix Fixed +collectType remove Removed + +rm changelog.d/* From c6cedbb8106a16527e48ac8ae03907e1d66c5a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 Oct 2023 00:07:18 +0200 Subject: [PATCH 093/271] InstanceV2: skip auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/web/mastodon_api/controllers/instance_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 3757a850a..3e664903a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:skip_auth when action in [:show, :peers]) + plug(:skip_auth when action in [:show, :show2, :peers]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation From ab894d98f4be5aadc033b71dacc499b85b579bc1 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 29 Oct 2023 12:59:03 -0400 Subject: [PATCH 094/271] Bundle 2.6.0 frontend --- priv/static/index.html | 2 +- .../static/css/5948.06d2a0d84620cba6a4fb.css | Bin 1335 -> 0 bytes .../css/5948.06d2a0d84620cba6a4fb.css.map | 1 - .../static/css/7586.0d43f70bc6240422f179.css | Bin 0 -> 1511 bytes .../css/7586.0d43f70bc6240422f179.css.map | 1 + .../static/css/7962.76663e78ad5ea0bb0b90.css | Bin 0 -> 19677 bytes .../css/7962.76663e78ad5ea0bb0b90.css.map | 1 + .../static/css/9114.8def3b2b7fe70b3b3712.css | Bin 19517 -> 0 bytes .../css/9114.8def3b2b7fe70b3b3712.css.map | 1 - .../static/css/9801.cfe503d4c949ae0c3813.css | Bin 0 -> 1884 bytes .../css/9801.cfe503d4c949ae0c3813.css.map | 1 + .../static/css/app.48e52505beba5b9ab69b.css | Bin 124954 -> 0 bytes .../css/app.48e52505beba5b9ab69b.css.map | 1 - .../static/css/app.c18a2c80794a1b699a61.css | Bin 0 -> 128030 bytes .../css/app.c18a2c80794a1b699a61.css.map | 1 + .../static/js/159.3a9274574f1e33801c4a.js.map | Bin 5445 -> 0 bytes ...3801c4a.js => 159.903e90c9de8ef6c67077.js} | Bin 1642 -> 1642 bytes .../static/js/159.903e90c9de8ef6c67077.js.map | Bin 0 -> 5535 bytes .../static/js/2724.e4840c73281069ba54ab.js | Bin 471273 -> 0 bytes .../js/2724.e4840c73281069ba54ab.js.map | Bin 3648078 -> 0 bytes .../static/js/3733.7060d1e6bca813125a0c.js | Bin 0 -> 473444 bytes ... 3733.7060d1e6bca813125a0c.js.LICENSE.txt} | 0 .../js/3733.7060d1e6bca813125a0c.js.map | Bin 0 -> 3584678 bytes ...c89c4958.js => 48.b5ecdbc517423af07ca4.js} | Bin 63226 -> 63304 bytes ...=> 48.b5ecdbc517423af07ca4.js.LICENSE.txt} | 4 ++-- .../static/js/48.b5ecdbc517423af07ca4.js.map | Bin 0 -> 306732 bytes .../static/js/48.d7e479b200a6c89c4958.js.map | Bin 307324 -> 0 bytes .../static/js/5948.2b7b4e97487f2539eb44.js | Bin 11022 -> 0 bytes .../js/5948.2b7b4e97487f2539eb44.js.map | Bin 30738 -> 0 bytes ...e4e5f8.js => 6464.eb9c90a1c948cde554e9.js} | Bin 3916 -> 3916 bytes .../js/6464.eb9c90a1c948cde554e9.js.map | Bin 0 -> 10548 bytes .../js/6464.fea96fa80a7373e4e5f8.js.map | Bin 10178 -> 0 bytes .../static/js/7586.981b2305a0019f6042a5.js | Bin 0 -> 12834 bytes .../js/7586.981b2305a0019f6042a5.js.map | Bin 0 -> 35169 bytes .../static/js/7962.e25d40b042f8ee7389c3.js | Bin 0 -> 179038 bytes .../js/7962.e25d40b042f8ee7389c3.js.map | Bin 0 -> 394938 bytes .../static/js/9060.24271e167e0471a1a732.js | Bin 0 -> 15495 bytes .../js/9060.24271e167e0471a1a732.js.map | Bin 0 -> 35326 bytes .../static/js/9114.e761a1c6846fea99aaf1.js | Bin 186399 -> 0 bytes .../js/9114.e761a1c6846fea99aaf1.js.map | Bin 399283 -> 0 bytes .../static/js/9801.99ace6b5dc657bf1a65b.js | Bin 0 -> 25711 bytes .../js/9801.99ace6b5dc657bf1a65b.js.map | Bin 0 -> 57596 bytes .../static/js/app.7c4b412b26221a7c8572.js | Bin 0 -> 875657 bytes .../static/js/app.7c4b412b26221a7c8572.js.map | Bin 0 -> 2207711 bytes .../static/js/app.8d2126d35dba9482db51.js | Bin 828121 -> 0 bytes .../static/js/app.8d2126d35dba9482db51.js.map | Bin 2083092 -> 0 bytes .../js/i18n/ar-json.4916f840147303aa65fe.js | Bin 0 -> 75033 bytes .../i18n/ar-json.4916f840147303aa65fe.js.map | Bin 0 -> 175929 bytes .../js/i18n/ar-json.d09609af3224232857d6.js | Bin 14319 -> 0 bytes .../i18n/ar-json.d09609af3224232857d6.js.map | Bin 33742 -> 0 bytes .../js/i18n/eo-json.6c62eef99e850912498b.js | Bin 0 -> 87510 bytes .../i18n/eo-json.6c62eef99e850912498b.js.map | Bin 0 -> 212971 bytes .../js/i18n/eo-json.d81690d5be30b23e516b.js | Bin 81295 -> 0 bytes .../i18n/eo-json.d81690d5be30b23e516b.js.map | Bin 198451 -> 0 bytes .../js/i18n/id-json.3e42564ce7a3a847ecb0.js | Bin 39564 -> 0 bytes .../i18n/id-json.3e42564ce7a3a847ecb0.js.map | Bin 99138 -> 0 bytes .../js/i18n/id-json.e5c9ee768155f88128b9.js | Bin 0 -> 44139 bytes .../i18n/id-json.e5c9ee768155f88128b9.js.map | Bin 0 -> 111136 bytes .../js/i18n/ko-json.4bd28b26a7390a09afc2.js | Bin 84194 -> 0 bytes .../i18n/ko-json.4bd28b26a7390a09afc2.js.map | Bin 199920 -> 0 bytes .../js/i18n/ko-json.9029d09084bb22d8b705.js | Bin 0 -> 87936 bytes .../i18n/ko-json.9029d09084bb22d8b705.js.map | Bin 0 -> 208897 bytes .../i18n/nan-TW-json.7f2789d8a461e86d1734.js | Bin 0 -> 57301 bytes .../nan-TW-json.7f2789d8a461e86d1734.js.map | Bin 0 -> 136688 bytes ...dac.js => zh-json.5831b903c3e6d281f122.js} | Bin 84380 -> 90880 bytes .../i18n/zh-json.5831b903c3e6d281f122.js.map | Bin 0 -> 223171 bytes .../i18n/zh-json.63e4c9fe0197374a5dac.js.map | Bin 208083 -> 0 bytes .../zh_Hant-json.bfa569654a5cd74767ce.js.map | Bin 148361 -> 0 bytes ...s => zh_Hant-json.f7e1d0f4b873c60d6396.js} | Bin 59401 -> 59689 bytes .../zh_Hant-json.f7e1d0f4b873c60d6396.js.map | Bin 0 -> 149004 bytes priv/static/sw-pleroma.js | Bin 182074 -> 183059 bytes priv/static/sw-pleroma.js.map | Bin 1245266 -> 1248352 bytes 72 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 priv/static/static/css/5948.06d2a0d84620cba6a4fb.css delete mode 100644 priv/static/static/css/5948.06d2a0d84620cba6a4fb.css.map create mode 100644 priv/static/static/css/7586.0d43f70bc6240422f179.css create mode 100644 priv/static/static/css/7586.0d43f70bc6240422f179.css.map create mode 100644 priv/static/static/css/7962.76663e78ad5ea0bb0b90.css create mode 100644 priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map delete mode 100644 priv/static/static/css/9114.8def3b2b7fe70b3b3712.css delete mode 100644 priv/static/static/css/9114.8def3b2b7fe70b3b3712.css.map create mode 100644 priv/static/static/css/9801.cfe503d4c949ae0c3813.css create mode 100644 priv/static/static/css/9801.cfe503d4c949ae0c3813.css.map delete mode 100644 priv/static/static/css/app.48e52505beba5b9ab69b.css delete mode 100644 priv/static/static/css/app.48e52505beba5b9ab69b.css.map create mode 100644 priv/static/static/css/app.c18a2c80794a1b699a61.css create mode 100644 priv/static/static/css/app.c18a2c80794a1b699a61.css.map delete mode 100644 priv/static/static/js/159.3a9274574f1e33801c4a.js.map rename priv/static/static/js/{159.3a9274574f1e33801c4a.js => 159.903e90c9de8ef6c67077.js} (96%) create mode 100644 priv/static/static/js/159.903e90c9de8ef6c67077.js.map delete mode 100644 priv/static/static/js/2724.e4840c73281069ba54ab.js delete mode 100644 priv/static/static/js/2724.e4840c73281069ba54ab.js.map create mode 100644 priv/static/static/js/3733.7060d1e6bca813125a0c.js rename priv/static/static/js/{2724.e4840c73281069ba54ab.js.LICENSE.txt => 3733.7060d1e6bca813125a0c.js.LICENSE.txt} (100%) create mode 100644 priv/static/static/js/3733.7060d1e6bca813125a0c.js.map rename priv/static/static/js/{48.d7e479b200a6c89c4958.js => 48.b5ecdbc517423af07ca4.js} (59%) rename priv/static/static/js/{48.d7e479b200a6c89c4958.js.LICENSE.txt => 48.b5ecdbc517423af07ca4.js.LICENSE.txt} (77%) create mode 100644 priv/static/static/js/48.b5ecdbc517423af07ca4.js.map delete mode 100644 priv/static/static/js/48.d7e479b200a6c89c4958.js.map delete mode 100644 priv/static/static/js/5948.2b7b4e97487f2539eb44.js delete mode 100644 priv/static/static/js/5948.2b7b4e97487f2539eb44.js.map rename priv/static/static/js/{6464.fea96fa80a7373e4e5f8.js => 6464.eb9c90a1c948cde554e9.js} (98%) create mode 100644 priv/static/static/js/6464.eb9c90a1c948cde554e9.js.map delete mode 100644 priv/static/static/js/6464.fea96fa80a7373e4e5f8.js.map create mode 100644 priv/static/static/js/7586.981b2305a0019f6042a5.js create mode 100644 priv/static/static/js/7586.981b2305a0019f6042a5.js.map create mode 100644 priv/static/static/js/7962.e25d40b042f8ee7389c3.js create mode 100644 priv/static/static/js/7962.e25d40b042f8ee7389c3.js.map create mode 100644 priv/static/static/js/9060.24271e167e0471a1a732.js create mode 100644 priv/static/static/js/9060.24271e167e0471a1a732.js.map delete mode 100644 priv/static/static/js/9114.e761a1c6846fea99aaf1.js delete mode 100644 priv/static/static/js/9114.e761a1c6846fea99aaf1.js.map create mode 100644 priv/static/static/js/9801.99ace6b5dc657bf1a65b.js create mode 100644 priv/static/static/js/9801.99ace6b5dc657bf1a65b.js.map create mode 100644 priv/static/static/js/app.7c4b412b26221a7c8572.js create mode 100644 priv/static/static/js/app.7c4b412b26221a7c8572.js.map delete mode 100644 priv/static/static/js/app.8d2126d35dba9482db51.js delete mode 100644 priv/static/static/js/app.8d2126d35dba9482db51.js.map create mode 100644 priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js create mode 100644 priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js.map delete mode 100644 priv/static/static/js/i18n/ar-json.d09609af3224232857d6.js delete mode 100644 priv/static/static/js/i18n/ar-json.d09609af3224232857d6.js.map create mode 100644 priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js create mode 100644 priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js.map delete mode 100644 priv/static/static/js/i18n/eo-json.d81690d5be30b23e516b.js delete mode 100644 priv/static/static/js/i18n/eo-json.d81690d5be30b23e516b.js.map delete mode 100644 priv/static/static/js/i18n/id-json.3e42564ce7a3a847ecb0.js delete mode 100644 priv/static/static/js/i18n/id-json.3e42564ce7a3a847ecb0.js.map create mode 100644 priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js create mode 100644 priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js.map delete mode 100644 priv/static/static/js/i18n/ko-json.4bd28b26a7390a09afc2.js delete mode 100644 priv/static/static/js/i18n/ko-json.4bd28b26a7390a09afc2.js.map create mode 100644 priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js create mode 100644 priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js.map create mode 100644 priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js create mode 100644 priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js.map rename priv/static/static/js/i18n/{zh-json.63e4c9fe0197374a5dac.js => zh-json.5831b903c3e6d281f122.js} (58%) create mode 100644 priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js.map delete mode 100644 priv/static/static/js/i18n/zh-json.63e4c9fe0197374a5dac.js.map delete mode 100644 priv/static/static/js/i18n/zh_Hant-json.bfa569654a5cd74767ce.js.map rename priv/static/static/js/i18n/{zh_Hant-json.bfa569654a5cd74767ce.js => zh_Hant-json.f7e1d0f4b873c60d6396.js} (61%) create mode 100644 priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js.map diff --git a/priv/static/index.html b/priv/static/index.html index 7dd5d0b78..52ff685c0 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/priv/static/static/css/5948.06d2a0d84620cba6a4fb.css b/priv/static/static/css/5948.06d2a0d84620cba6a4fb.css deleted file mode 100644 index b14e141435c66818830a5f2eb95c152e18c260ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1335 zcmbVL+m6~W5Pk1gM5IbwhmHN;}6HmacZ?YXAium_VOtJ))Le-Zj zbMefaGiNCCwJu0ulr0SP- z@9O6ZKvK;@QkF7zrI$rGqO^$-GjJW+--*_aKuQkreH z7Mz1Vk0;dFAdDtb`0(lManjo{+O#Rehgt#&wS`P0=6(Pop$yrD0ZtkuQe5wW2^~DL zG$9xjfn6OMhpd(d5dKE-L1y127@N`O_q@G9?|3AZX^>5~xtDaa&K28{t>9sup}Eb^ zh|Ys!+KZOkEdG|psezO5%CQ)w&wl6+LgD&IS?MB%fq5W86H|r!&<=Hz+TH$(0xnoG zI?l0Z{r643wQlt5P@)KrCD7b#_wNZmRn>uwR~kvdP4T*CCYI}|AoX8WcdK5fbj00J zj?&7Jn2zyDI?#T4N%#ZQr7?Jd&F}=T06b*Bj(df;c&qKP-F%c#*x|1GTR!QSx0g>= z4li+XUWi(N--ChU<#vjD)6PLfk_GrVDwr%rGZbO+kj&D%gy0u9fB(;#_`i.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n min-width: 10em;\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/7586.0d43f70bc6240422f179.css b/priv/static/static/css/7586.0d43f70bc6240422f179.css new file mode 100644 index 0000000000000000000000000000000000000000..7da2aa2ea553e16d597de1814c074b2788cdcfb5 GIT binary patch literal 1511 zcmbVM+iu%141M2M7#Ip9-K09VrnRwRAF#a)*h3!`i%zu3k|4=RJR|=;TCsDhmlS&u zOo~1v506Nht5ilZT^OxEc>>mITQez^iU<#dOEPqWO}TJJvMR|XY||Bp<;o|cIKE4N zmClR2LJW*kzK!3}UgU@_ys8UkmqI0U21RNZ=Rz%m5sjhPQ3u|m$B{yFCTqP0n@hb( zR)TX-m-i9X#tW^86wZIX_&MtAa9cMa#Q91B@s)u@Vb5IwL_+S9o&wGiL{ePOfdOrM zWTCy+MQH45(O6`;*f-&BfS*KmS-dt0oxJ7k*g6N7SrJI8a~u+J;twQq9Nm5nU7^@} z=Lw*_T$~Sw7^XnhtsI`?^$HQTWFxp=C1`Hai|M7_GUY@wuq1ru+7>5H!b{7-EuFYi zzZV5+g5;&j!Zk1lL@1(5pYGG47-O^DeJy|s77dPdJT?Ad-ASR<)q$VZK5r!``v`&N zdb4{w#XdG(Xdney;85ok?)FeG_no3`*e%U+xcO5=VT&^A zc27?^ON*OhbO}Ta!0(f8!lr2zaFFF*pptob9b`;qgBj{O`V`ILyNKY4q8h$iZvU6D z(akZ}?!&|V$H?i@X7HUEgAd~8k8fY*v!_QI^XVj?$BXQ7JdLO0aXy+q)66+qFmwO# E7sVehfdBvi literal 0 HcmV?d00001 diff --git a/priv/static/static/css/7586.0d43f70bc6240422f179.css.map b/priv/static/static/css/7586.0d43f70bc6240422f179.css.map new file mode 100644 index 000000000..f8f61fe6e --- /dev/null +++ b/priv/static/static/css/7586.0d43f70bc6240422f179.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/7586.0d43f70bc6240422f179.css","mappings":"AACA,uBAGE,mBAFA,aACA,YAEA,uBAEA,4BACE,YACA,iBCPJ,gBACE,gBAEA,2DAEE,qBACA,iBAEA,iEACE,mBAGF,mFACE,gBAIJ,qCAGE,cADA,kBADA,eAEA,CAGF,sCAOE,YADA,eALA,gBACA,qBAEA,wBADA,uCAEA,YAEA,CAEA,yBATF,sCAWI,YADA,eACA,EAGF,kDACE,YACA,kBAEA,uDACE,eAGF,6EACE,cAKN,iCACE,aACA,eACA,cAEA,mCACE,kBAGF,gDACE,aACA,YAKF,2CASE,8CAEA,yBAXF,2CAgBI","sources":["webpack://pleroma_fe/./src/components/async_component_error/async_component_error.vue","webpack://pleroma_fe/./src/components/settings_modal/settings_modal.scss"],"sourcesContent":["\n.async-component-error {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n\n .btn {\n margin: 0.5em;\n padding: 0.5em 2em;\n }\n}\n","@import \"src/variables\";\n\n.settings-modal {\n overflow: hidden;\n\n .setting-list,\n .option-list {\n list-style-type: none;\n padding-left: 2em;\n\n li {\n margin-bottom: 0.5em;\n }\n\n .suboptions {\n margin-top: 0.3em;\n }\n }\n\n .setting-description {\n margin-top: 0.2em;\n margin-bottom: 2em;\n font-size: 70%;\n }\n\n .settings-modal-panel {\n overflow: hidden;\n transition: transform;\n transition-timing-function: ease-in-out;\n transition-duration: 300ms;\n width: 1000px;\n max-width: 90vw;\n height: 90vh;\n\n @media all and (max-width: 800px) {\n max-width: 100vw;\n height: 100%;\n }\n\n >.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n }\n\n .btn:not(.dropdown-button) {\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n flex-wrap: wrap;\n line-height: 2;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css new file mode 100644 index 0000000000000000000000000000000000000000..2326ed9323c38b12859426c3d656dc350acc46fd GIT binary patch literal 19677 zcmbt6YjfK+w%_+xa5vkTI4jbWWZ5w_?OfZflXUBC^K2(Ooja5WNsK8{m!xbf8vplu z9smgNAxmjGO(YUHI5;@(b09BXuClxoxw~E^Ss2CXd^^k1(k*HP zH-ImXo_LTGR(UoRMS(Ny5#hSbiuHUhigLTkinxrkG{{8~mhqi<^o-YToUYd8Hj0Z? z5^jP?l1*>N_iTzh4%7} z#`Iu}o>pcnHxaW^&G*%8e38fVh4tpaEf!&v-4l+8CxCMD`6PVq_wYaO6ani|y!0we zMe?5J9gT6Vz=u2PeJ{IA?bvpC#G7y9w|J#`Sp&YYSh2zs&YJXvG9lMF<384Ow#C%^30pO)-cLeX;YjEWmh(QFi37PThE(}kGc zq9VM{!_^9Kr-5mf3N3cN(+9ox=-tQV!Y$U5Vw%S*gz73wr*Oe>EL*I!Smt-kbUcyg z87x|v%3P_f!VSSRk-jWkkejX4!feq{$<#+U-ntNZA;NsR(EHK2b-{JK$vU~!ghFYO zD-A$%sOdli_|{a&?RXdv-43nXMBdb9vn*d~d#-h*8^t;35cC6pLD$PlHIjyD$HfFI zabbM}f>PS0WP5O@rV+WxqIedI=uH~M)3Ai|Qk)p42^cOuq{(K#H9=`7Bd zxL2hcl{aLliV-M$sVb0x|91T1?|*Y%I9GXw#>D$lICl{Ic>N(hU6kdj2#$|uBAsv6 zVG6b<_2ytz))Ozzj;B27O95jENVeLL3+D{{{>WW{vjsk+a|N@UYY76!y#vPz7$tHb zlo7d8E`)RQ=7W>OK=wjNU_XYX2msJv;P(6O!2jTnf}tOr484<+lg}{d*pus%6UzR0 zrEZflA6G`oACuaXX2Nf3K5+flV1wBB24qd$doj6p6~>;as>`9s@lgngny=M`8{pK!-Yr^4~e8 z4bzg|@G;k4RMyfr$5$??@l@Fqj7T-H!}KmJdewJe0`QU^swbcm=k^n@Dq&I$K)J&K zTj^ZE>2T<_J2C_2??OUD)FA@L4b=>V6w%*~qGz+&ID_~FPr_J(kZu|7`@SCTjj9#y zVsTHq>ORJ7T_PWvVfgSI7U##(kEED))bI62!`@&t>Un3QqeeW`b}~^|EJOrErBL5@ z_`CB=h)N$bYpLCOqe>D~FkEej=X?B*yO4lki|7%{00sheh{|z6bVQ7Yl6!|kZ-{@s zQ%8)Ua`mu#8*14*wJc>4@tU+O;nwAwAJd~GEx8I%k<0-&07 zXDzg&1Okp?JPl@I=m9tjBCiBNKAi~g9z_eQ;mLto$D~8>59c4+sxop7X{m1}kbspY z0kV;xxkLD5lH&~Rm6k1v`x#Z`!ovXb011FTi)t$BwQ6_B>eaVZ29atzdFiw?Vf#&< zR2GX;V+v1Ptc7MM%~GAv7Ax!!pOy(Af%X`WY=ziP*LeY1HjrvCWRMkN(ezt{%Gog7}XGxpjzlkWI?%;Fl{o~^fj^CN#Qzv$W5I{wbELj zdnbM!JJi%lMbX85-MdQlw$l4TDBjzbCdGX#+Zc#wNbT4cAY?6kXP_ckXh6wXX?Xjl zbTKpG`^W!TiYN}9=T+Q!cKQ;M%v-Im%manBm9j%2lPU_ivIWrjIb?O72838jK!{Ka zp#A{!%uhl>jM_z}i;zlzw@ad;GBSR*mC(dCjF;g6E3>c2kH8f9O$m%s@>dhFRa-Eu z+l1X(=~j`m8m#l=c@&mmKwpnn>3odQ;ps^){&e~4yL2A=KomdUPcIB&8mf-a& zTY;aeH;9LN1D^{hvBY`x8a!1@Z%2+sSh>lS-ENMtt$g#nZTFk+NnjOLX@&(8sLqJ! z(c>^j5f7pz$bXx4G9*BA6oy8~{~Q8puc%6D)xla5C@2FYjY1rTRZ8Mf2JHEq6DoAR z(y$X8`0pIv9+E$LGV6tTQ%F0-nanj|Kw`bBqDHsO!;LOmkaa>o!W>Q1`)|2fiSIF` z_NP*gTn3Xx8nW5uEN>EGk%RJ5Qluf2$M6P_GAUCzYKqoUbqrA-+Ng8`r2{a@ZQ-(S zb=I1chD%_H1*XpOZ6|2(h;s0C-S7_76y|dj3uzOAs=vWc${38Ad$b{{$mNOhDKBr< zPi<~g*kDN0xfe{elqz%GFc*z7Dg|s3FEmRsgp*IWiXt<>5-}JBMHcpmNr0g=Smjr^ zUal~uQYbmm))Qqoyt9g^HS^oTx5INLFW*{gF2yAa5FV)RQ<|s&JZ4La_Pm_vTBHHnft9$@a++IoHD51v?NUt(#ua5$99`wl5wHWY#HAS*j0B$ZcN*YgT}MgZ#7kbs2F z0QxFZ?o)lCXlb4Gq+|+C>=NP1MlddKet=>Z@{GRFQc2SK$Iwq!05B$ogE9ehXjF%B4#ZK+K&%Jx6YVUM2WKwj@Pd^I;heWjDxw1>i&3(s zZk48J?g#8fP!)GTgLQ zpy1SYtn7g=mBPuRKcFj?-NR|0UP^skjH;oW)cNSMK%0iFs*E8T@``ogZ8t?)@oLFo z&HPzqQX9-IYv9c6*9&JeF8J;`0ypih(v$RIR!6eH& zs-~9Wmq~fqw?zGL3Mi;7{w{s^%FpeoHwtO>9h$hpTd8~laQvK8ReZGZ?FQZ438da} zSJj5Hpd=8;NZ55_~3f237vgB{F6#Eml4z@`sGJ5E* z2YCi%W%2wRtI9`X{TwUK*Z~cGA2$CSvMD7s_i+kMy%wKH%~iP=AcEA+e13ioIYQm! zOnH<5<4B0k=NOxUb$)&_0vvnKPESvr!RWi5VYnUts8=8D9yXjun=sE{4O*-lc*k&a zE856`XV&BPydEZt92fuY(cj>+8JkBxGeB=0S|Az$`re(`%11;VB}NVUjrJQpj&DC( z41p*+#9d`A-pQeD>Z)NJY15jTptf(Bn7~0tsw#zrW)^k$)VU?LZ%7j$hha%x0;t`S z953cJaJ1H@Hjqx={IU!WLVGCEqB2mQ0?0OPU7roSF%mbT9^kcu20~L=WBL z0S}ylaz;X4?J*`5ZS0Vf*p9p2np3q>d zcc}gfGX^x5r#oC-FR8+yfojcyI9iw;vI4PeqeRH*cT2YQS_Wc?Nf}*7EYj7G&AEaGD)s2ruAf*kf z{4NQ)OwSC_|B{gtwO+O2;H+5jfGZ`VEy`_kw{q(YS*Nno>Lh!+S)tmuf&isbQ1Yy+ z%p|+AADo8OstdanE6;@Nrh%Y51QyXOcRZ{js>OhXdg_9apo>CJvw*Ee8X}`ie%spMLDD^0mv6teqoV9G zbOpD;P?+uos~RlDsYW(Vq_-_|Ab4wE-P%mkyJCl@cB$D$9=P_iEFso_)sBhFs{B~T zF_|pWO$!Cn@ot5H>kq^#l9E_}0_Lq3pdgsG@M}7NMs!LlQr;%HhxUzC2$u(}t9%^1 zb0v9_MH{o5fZkc7wUY>0Uk9_o##$5!Pm!p~{Z`rvf@9U=+_=q*7nmJck0)%^WRx|$ zUfelT;fxW4ya#P)+hY0zNgZE@30p8|-=*TqjnD$^fF~hjv4wx`0ckU@A+Q zhf*ODYDrVN_CH_l;IP!f4rI97B!uf^6_YozT>T-Zxh5|~e>Uws3(up$YzX%{Ascn> zPeO4**)RM&6`?acbt&7?EE!!s_D(n|4|R-SMs#n6WyPU~_H zrOQmsJAiVh>DBN6?GxbFgLY!M8wMhnl#`Cr2`*_iz|aHsr10au>&dIYCeyY$d56IR zwIS~i?{*&>g=OslX{Ok7;X*)ZmF}Th$(8CJj8e{QSN8gCib^N%i3Fts^s(2YSmsH( z?8t@~2pt#ngAo>d4i5NEq@k$I>6Sua*AZGkWc{mND3Eeymf4J0dqCMRW1VVwxZNGW z^$O#ifdjoXLg`a%V>E_VV4%ETKBwVFK?==I(?y&_&$B2x>Vn-p!`9DqsibvHv|Ety zw-%@{0!NE`!manl=GvxPujTyR+>Hlg(S%WDJf?74%|M+$$~E#-0tN?v7N}~!q05bW zfaGh)pBO(~Vl+{(uR%ZV&|4Hq_zh02TqTaH?S zQ@6a~#u)Z-11cGwUlDE2!!rldb!@I}uD?Rvw3~&L%v@d%h-vEIs7Ly0`yEYOl z&`~P+ZUn4*klEaz%@d^P>^+0Z(90Y~2Bv~x)3{Q!2Z9ckH=wmE)HQYbStwI>YOU4# z0QJ@>_^C1QSORAF&>2SGG0V&Jorrq5(5^Xr#LM~5;ODy5`BdD1&VhQ)$9KPce|CO4 f@XnzAJrrjz!)PQze=_kW=e`FWmfkX4y*U1V(Ysua literal 0 HcmV?d00001 diff --git a/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map new file mode 100644 index 000000000..9d501f27a --- /dev/null +++ b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/7962.76663e78ad5ea0bb0b90.css","mappings":"AAEE,oBACE,gBACA,aCFF,qBACE,aCAJ,aACE,kBAEA,mBACE,cACA,WAGF,qBAME,wBCbW,CDcX,mCAGA,qBCTe,CDUf,gCACA,iBCCoB,sCDCpB,yBACA,0BACA,sCACA,8BAfA,OAGA,iBAaA,gBAjBA,kBAGA,QADA,SAgBA,UE7BJ,8BACE,gBACA,iBAEA,qCACE,WCLJ,6BACE,gBACA,iBAEA,oCACE,WCLJ,kBAIE,mBAFA,aADA,SAEA,8BAEA,wBAEA,yBACE,iBACA,gBACA,uBAGF,yBACE,WAGF,uCACE,iBCfF,4BAEE,mBADA,YACA,CAEA,8BACE,YAIJ,qCAKE,qDAAuD,CACvD,yDAA2D,CAC3D,6DAA+D,CAC/D,8CAA+C,CAP/C,wBJJgB,CIKhB,6CACA,qCAKgD,CAGlD,wBAEE,mBAIA,oEALA,aAEA,cAGA,CAEA,gCACE,OAIJ,kCAEE,UADA,cACA,CCtCF,2BACE,aACA,kBAEA,kCACE,eCNN,sBACE,YAEA,0CACE,YAGF,oCAGE,eADA,cADA,gBAEA,CAGF,0CACE,WAGF,wCAEE,aACA,sBAFA,WAEA,CAGF,0CACE,oBACA,eACA,WCzBJ,mBACE,qBACA,kBAGF,kBACE,gBACA,eACA,kBCRF,yBACE,qBACA,kBAGF,wBACE,gBACA,eACA,kBCRF,cACE,qBACA,kBAEA,8BACE,iBAIJ,eACE,gBACA,eACA,kBCTA,2BACE,YVWgB,CUVhB,4BAGF,gCACE,0CCNF,sDAKE,qBAHA,aACA,eACA,6BACA,CAGF,uBACE,YXGgB,CWFhB,4BAGF,yBACE,aAEA,eADA,sBACA,CAEA,kCACE,OACA,mBAEF,wCACA,+CAGE,qDAEE,eADA,UACA;AChCR;;;;;;;;EAQE,CAEF,mBACE,aAAc,CACd,WAAY,CACZ,aAAc,CACd,iBAAkB,CAEd,iBAAkB,CACtB,wBAAyB,CACtB,qBAAsB,CAEjB,gBACV,CAEA,uBAEY,0BAA2B,CACnC,aAAc,CACd,WAAY,CACZ,sBAAuB,CACvB,yBAA2B,CAC3B,wBAA0B,CAC1B,sBAAwB,CACxB,qBAAuB,CACvB,UACF,CAEF,qFAKE,QAAS,CACT,MAAO,CACP,iBAAkB,CAClB,OAAQ,CACR,KACF,CAEA,kCAEE,eACF,CAEA,kBACE,qBAAsB,CACtB,SACF,CAEA,eACE,qBAAsB,CACtB,UACF,CAEA,kBACE,aAAc,CACd,WAAY,CACZ,sBAAuB,CACvB,kCAAsC,CACtC,eAAgB,CAChB,UACF,CAEA,gBACE,oBAAqB,CACrB,aAAc,CACd,UAAY,CACZ,iBACF,CAEA,yBACI,uBAAwB,CACxB,oBAAqB,CACrB,gBAAsB,CACtB,MAAO,CACP,aAAmB,CACnB,UACF,CAEF,yBACI,qBAAsB,CACtB,sBAAuB,CACvB,WAAY,CACZ,cAAoB,CACpB,KAAM,CACN,eACF,CAEF,gBACE,aAAc,CACd,QAAS,CACT,QAAS,CACT,WAAa,CACb,iBAAkB,CAClB,OAAQ,CACR,OACF,CAEA,6CAEI,qBAAsB,CACtB,WAAY,CACZ,aAAc,CACd,iBACF,CAEF,uBACI,UAAW,CACX,SAAU,CACV,KAAM,CACN,SACF,CAEF,sBACI,UAAW,CACX,MAAO,CACP,QAAS,CACT,SACF,CAEF,2CAGE,aAAc,CACd,WAAY,CACZ,UAAY,CACZ,iBAAkB,CAClB,UACF,CAEA,cACE,qBAAsB,CACtB,MAAO,CACP,KACF,CAEA,cACE,qBACF,CAEA,qBACI,gBAAiB,CACjB,UAAW,CACX,KAAM,CACN,SACF,CAEF,qBACI,gBAAiB,CACjB,UAAW,CACX,MAAO,CACP,QACF,CAEF,qBACI,gBAAiB,CACjB,SAAU,CACV,KAAM,CACN,SACF,CAEF,qBACI,WAAY,CACZ,gBAAiB,CACjB,UAAW,CACX,MACF,CAEF,eACE,qBAAsB,CACtB,UAAW,CACX,WAAa,CACb,SACF,CAEA,uBACI,gBAAiB,CACjB,eAAgB,CAChB,UAAW,CACX,OACF,CAEF,uBACI,gBAAiB,CACjB,QAAS,CACT,gBAAiB,CACjB,QACF,CAEF,uBACI,gBAAiB,CACjB,SAAU,CACV,eAAgB,CAChB,OACF,CAEF,uBACI,WAAY,CACZ,eAAgB,CAChB,QAAS,CACT,gBACF,CAEF,wBACI,kBAAmB,CACnB,UAAW,CACX,QACF,CAEF,wBACI,kBAAmB,CACnB,SAAU,CACV,QACF,CAEF,wBACI,WAAY,CACZ,kBAAmB,CACnB,SACF,CAEF,wBACI,WAAY,CACZ,kBAAmB,CACnB,WAAY,CACZ,SAAU,CACV,UAAW,CACX,UACF,CAEF,yBAEA,wBACM,WAAY,CACZ,UACJ,CACE,CAEJ,yBAEA,wBACM,WAAY,CACZ,UACJ,CACE,CAEJ,0BAEA,wBACM,UAAW,CACX,WAAa,CACb,SACJ,CACE,CAEJ,+BACI,qBAAsB,CACtB,WAAY,CACZ,WAAY,CACZ,aAAc,CACd,WAAY,CACZ,SAAU,CACV,iBAAkB,CAClB,UAAW,CACX,UACF,CAEF,mBACE,SACF,CAEA,YACE,4QACF,CAEA,cACE,aAAc,CACd,QAAS,CACT,iBAAkB,CAClB,OACF,CAEA,gBACE,sBACF,CAEA,cACE,WACF,CAEA,cACE,gBACF,CAEA,qIAIE,kBACF,CClTE,yBACE,aAGF,+BACE,kBAEA,mCACE,cACA,eAIJ,+BACE,gBAEA,sCACE,eChBJ,kBACE,SAGF,8BACE,gBAGF,8BAEE,YADA,WACA,CAGF,wCACE,eAEA,kBADA,WACA,CAEA,4CACE,WAIJ,wBACE,gBACA,aAGF,2BACE,WAGF,uCAGE,aAFA,kBACA,WACA,CAGF,6BAIE,iBdnBqB,CcoBrB,sCAJA,cAEA,YADA,UAGA,CAGF,2BAME,gCAFA,iBd5BsB,Cc6BtB,uCAQA,eADA,gBAHA,aAEA,kBAJA,WANA,kBAEA,WAOA,kBARA,SAMA,WAKA,CAEA,iCACE,UAGF,+BACE,WAIJ,2BACE,WAEA,8BACE,gBAGF,oCACE,iBAIJ,gCACE,YAGF,0BAGE,eADA,cADA,gBAEA,CAEA,iCACE,WAIJ,8BAEE,aACA,sBAFA,WAEA,CAEA,qCACE,oBACA,eACA,WAIJ,8BACE,mBAGF,6BACE,aAEA,0CACE,cACA,mBACA,YAGF,2CAEE,kBACA,mBACA,eAHA,UAGA,CAIJ,6BACE,cACA,kBCpIF,2BACE,gBAGF,iEAEE,iBAEA,cACA,cAFA,SAEA,CCVJ,iBACE,aAEA,eADA,4BACA,CAGF,6BACE,cACA,mBACA,gBCRF,aACE,oBAEA,yBAIE,oBAHA,oBACA,WACA,cAEA,iBAEA,+BACE,gBAGA,YAFA,ajBHgB,CiBIhB,+BAGA,QAAO,CADP,SACA,CAEA,yCACE,aACA,cACA,UAWJ,sIAIE,mBAFA,aAGA,gBAFA,aAEA,CAGF,+CAEE,sBACA,kBAEA,2GAIE,sBADA,WADA,cAIA,WADA,kBAEA,UAGF,qDAEE,MAAK,CADL,KACA,CAGF,sDACE,SACA,QAKN,oBACE,cCpEF,gCAEE,MAAK,CADL,aACA,CCDJ,gBACE,aACA,eACA,uBACA,kBAEA,wEAEE,mBAGF,0CAEE,aADA,OAEA,eAIA,6DAEE,cADA,SACA,CAGF,sHAEE,aACA,OAEA,gKACE,WAIJ,2DACE,uBAGF,6HAIE,WAFA,SACA,UACA,CAGF,2DAEE,qBADA,qBACA,CAEA,iEAEE,YADA,SAjCG,CAqCL,6EAEE,wBADA,wBACA,CAIJ,0DAIE,mBAFA,sBAIA,0MACE,CAKF,kDADA,0BAEA,iBnBnDkB,CmBoDlB,qCAXA,aAFA,OAIA,sBASA,CAEA,yEAGE,wBnB7EO,CmB8EP,mCACA,kBnB9DgB,CmB+DhB,sCAJA,WADA,SAKA,CAKN,8BACE,OACA,gBAEA,0CACE,oBAEA,2DACE,OAGF,0GAGE,iBADA,aACA,CAGF,+CAEE,cADA,cACA,CCxGN,gCACE,eAKA,oCAEE,4BAA2B,CAD3B,yBACA,CAGF,kCAEE,2BAA0B,CAD1B,wBACA,CChBN,gBACE,aACA,yBAEA,kBADA,eACA,CAEA,uBACE,iBAGF,wBACE,qBAEA,iBADA,iBACA,CCbJ,mBACE,kBAGF,kBAGE,SACA,UAHA,kBAIA,WAHA,KAGA,CCRF,WACE,mBAEA,4BACE,iBAGF,gBACE,kBACA,mBAGF,0BAEE,qBADA,aAEA,kBAEA,iCACE,OAGF,+BACE,YAGF,uCACE,WAGF,iEAIE,MAAK,CADL,SADA,aAEA,CAEA,2FACE,cAGF,yFAGE,sBAFA,OACA,aACA,CAKF,mFAEE,WAKN,4BACE,eAGF,6IAKE,aAGF,yDAEE,sBAGF,4BAKE,eACA,8BALA,+BACE,UAOJ,gJAKE,iBAGF,uBAGE,qBAFA,aACA,8BAIA,kBADA,gBADA,UAEA,CAEA,yBACE,OAEA,kBAIJ,+BACE,aACA,sBAEA,oCAEE,YAEA,mBAHA,cAEA,aACA,CAKF,sCACE,OACA,iBAGF,8CAEE,mBADA,eACA,CAIJ,oDAIE,qBAFA,aAGA,eAFA,sBAEA,CAEA,wJAEE,mBAGF,kFACE,aAGF,wEACE,iBAIJ,8BACE,eAEA,uBADA,eACA,CAEA,2CACE,mBACA,cAIJ,8BAOE,kCACA,8CAEA,4BADA,sBANA,6BvBxJe,CuBwJf,8BvBxJe,CuBwJf,0BvBxJe,CuByJf,gCACA,aACA,WAIA,CAGE,2CAEE,aADA,2BACA,CAEA,oDACE,OAEA,uDACE,oBAGF,2DAEE,aADA,eACA,CAEA,6DACE,iBAMR,iDAGE,mBADA,aADA,cAEA,CAGF,8FAEE,0HACE,CAWF,WACA,uBAEA,iBADA,iBACA,CAGF,iDAOE,kBvB1MoB,CuB2MpB,0CAPA,YAEA,eAGA,iBAJA,iBAGA,gBADA,cAIA,CAGF,6CACE,YAGA,eADA,YAEA,iBAHA,UAGA,CAGF,8CAEE,qBADA,YACA,CAEA,wDAEE,qBADA,oBAGA,MAAK,CADL,gBACA,CAIJ,gDAGE,uBvBpPW,CuBoPX,iBvBpPW,CuBqPX,gCAHA,UAGA,CAGF,0CACE,cAKN,wBACE,gBAGF,+CAIE,aAEA,WADA,sBAFA,mBADA,cAIA,CAEA,yDACE,cAGF,mGACE,iBAGF,8HAGE,qBADA,YACA,CAIJ,uDAME,mBAFA,uBAFA,SACA,gBAEA,sCACA,CAGF,kFAGE,gBAGF,4BAGE,MAAK,CADL,cADA,aAEA,CAGF,4BACE,eAGF,kCACE,aAGF,0BAEE,qBADA,aAEA,mBAGE,wCACE,mBAON,gCACE,aACA,mBAEA,WAAU,CADV,4BACA,CAGA,qCACE,YAGA,eAFA,eACA,YAEA,UC1VN,uBACE,YAEA,qCACE,0CACA,qBACA,qBAEA,oFAEE,cACA,mBAEA,0GACE,gBAIJ,sDACE,aAEA,mEACE,SACA,kBAIJ,gDACE,mBAEA,kBADA,gBACA,CAGF,4CACE,eAGF,8CAGE,aADA,eADA,UAEA,CAGF,wGAEE,sBACA,SxBnCW","sources":["webpack://pleroma_fe/./src/components/importer/importer.vue","webpack://pleroma_fe/./src/components/exporter/exporter.vue","webpack://pleroma_fe/./src/components/autosuggest/autosuggest.vue","webpack://pleroma_fe/./src/_variables.scss","webpack://pleroma_fe/./src/components/block_card/block_card.vue","webpack://pleroma_fe/./src/components/mute_card/mute_card.vue","webpack://pleroma_fe/./src/components/domain_mute_card/domain_mute_card.vue","webpack://pleroma_fe/./src/components/selectable_list/selectable_list.vue","webpack://pleroma_fe/./src/hocs/with_subscription/with_subscription.scss","webpack://pleroma_fe/./src/components/settings_modal/tabs/mutes_and_blocks_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/helpers/modified_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/profile_setting_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/draft_buttons.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/security_tab/mfa.vue","webpack://pleroma_fe/./node_modules/cropperjs/dist/cropper.css","webpack://pleroma_fe/./src/components/image_cropper/image_cropper.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/profile_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/helpers/size_setting.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/general_tab.vue","webpack://pleroma_fe/./src/components/color_input/color_input.scss","webpack://pleroma_fe/./src/components/color_input/color_input.vue","webpack://pleroma_fe/./src/components/shadow_control/shadow_control.vue","webpack://pleroma_fe/./src/components/font_control/font_control.vue","webpack://pleroma_fe/./src/components/contrast_ratio/contrast_ratio.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/theme_tab/preview.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/theme_tab/theme_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/settings_modal_user_content.scss"],"sourcesContent":["\n.importer {\n &-uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n}\n","\n.exporter {\n &-processing {\n margin: 0.25em;\n }\n}\n","\n@import \"../../variables\";\n\n.autosuggest {\n position: relative;\n\n &-input {\n display: block;\n width: 100%;\n }\n\n &-results {\n position: absolute;\n left: 0;\n top: 100%;\n right: 0;\n max-height: 400px;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-style: solid;\n border-width: 1px;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n overflow-y: auto;\n z-index: 1;\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n","\n.block-card-content-container {\n margin-top: 0.5em;\n text-align: right;\n\n button {\n width: 10em;\n }\n}\n","\n.mute-card-content-container {\n margin-top: 0.5em;\n text-align: right;\n\n button {\n width: 10em;\n }\n}\n","\n.domain-mute-card {\n flex: 1 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0.6em 1em 0.6em 0;\n\n &-domain {\n margin-right: 1em;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n button {\n width: 10em;\n }\n\n .autosuggest-results & {\n padding-left: 1em;\n }\n}\n","\n@import \"../../variables\";\n\n.selectable-list {\n &-item-inner {\n display: flex;\n align-items: center;\n\n > * {\n min-width: 0;\n }\n }\n\n &-item-selected-inner {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n }\n\n &-header {\n display: flex;\n align-items: center;\n padding: 0.6em 0;\n border-bottom: 2px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n\n &-actions {\n flex: 1;\n }\n }\n\n &-checkbox-wrapper {\n padding: 0 10px;\n flex: none;\n }\n}\n",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 1rem;\n }\n }\n}\n",".mutes-and-blocks-tab {\n height: 100%;\n\n .usersearch-wrapper {\n padding: 1em;\n }\n\n .bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n }\n\n .bulk-action-button {\n width: 10em;\n }\n\n .domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n }\n\n .domain-mute-button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n}\n","\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ProfileSettingIndicator {\n display: inline-block;\n position: relative;\n}\n\n.profilesetting-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.DraftButtons {\n display: inline-block;\n position: relative;\n\n .button-default {\n margin-left: 0.5em;\n }\n}\n\n.draft-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n@import \"../../../../variables\";\n\n.mfa-backup-codes {\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .backup-codes {\n font-family: var(--postCodeFont, monospace);\n }\n}\n","\n@import \"../../../../variables\";\n\n.mfa-settings {\n .mfa-heading,\n .method-item {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: baseline;\n }\n\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .setup-otp {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n\n .qr-code {\n flex: 1;\n padding-right: 10px;\n }\n .verify { flex: 1; }\n .error { margin: 4px 0 0; }\n\n .confirm-otp-actions {\n button {\n width: 15em;\n margin-top: 5px;\n }\n }\n }\n}\n","/*!\n * Cropper.js v1.5.13\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2022-11-20T05:30:43.444Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n }\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 75%);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n }\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n }\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n .cropper-center::after {\n background-color: #eee;\n content: \" \";\n display: block;\n position: absolute;\n }\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n }\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n }\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n }\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n }\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n }\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n }\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n }\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n }\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n }\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n }\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n }\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n }\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n }\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n }\n\n@media (min-width: 768px) {\n\n.cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n }\n\n@media (min-width: 992px) {\n\n.cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n }\n\n@media (min-width: 1200px) {\n\n.cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n }\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: \" \";\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n }\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC\");\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n","\n.image-cropper {\n &-img-input {\n display: none;\n }\n\n &-image-container {\n position: relative;\n\n img {\n display: block;\n max-width: 100%;\n }\n }\n\n &-buttons-wrapper {\n margin-top: 10px;\n\n button {\n margin-top: 5px;\n }\n }\n}\n","@import \"../../../variables\";\n\n.profile-tab {\n .bio {\n margin: 0;\n }\n\n .visibility-tray {\n padding-top: 5px;\n }\n\n input[type=\"file\"] {\n padding: 5px;\n height: auto;\n }\n\n .banner-background-preview {\n max-width: 100%;\n width: 300px;\n position: relative;\n\n img {\n width: 100%;\n }\n }\n\n .uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n\n .name-changer {\n width: 100%;\n }\n\n .current-avatar-container {\n position: relative;\n width: 150px;\n height: 150px;\n }\n\n .current-avatar {\n display: block;\n width: 100%;\n height: 100%;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n }\n\n .reset-button {\n position: absolute;\n top: 0.2em;\n right: 0.2em;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n background-color: rgb(0 0 0 / 60%);\n opacity: 0.7;\n width: 1.5em;\n height: 1.5em;\n text-align: center;\n line-height: 1.5em;\n font-size: 1.5em;\n cursor: pointer;\n\n &:hover {\n opacity: 1;\n }\n\n svg {\n color: white;\n }\n }\n\n .oauth-tokens {\n width: 100%;\n\n th {\n text-align: left;\n }\n\n .actions {\n text-align: right;\n }\n }\n\n &-usersearch-wrapper {\n padding: 1em;\n }\n\n &-bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n\n button {\n width: 10em;\n }\n }\n\n &-domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n\n button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n }\n\n .setting-subitem {\n margin-left: 1.75em;\n }\n\n .profile-fields {\n display: flex;\n\n & > .emoji-input {\n flex: 1 1 auto;\n margin: 0 0.2em 0.5em;\n min-width: 0;\n }\n\n .delete-field {\n width: 20px;\n align-self: center;\n margin: 0 0.2em 0.5em;\n padding: 0 0.5em;\n }\n }\n\n .birthday-input {\n display: block;\n margin-bottom: 1em;\n }\n}\n","\n.SizeSetting {\n .number-input {\n max-width: 6.5em;\n }\n\n .css-unit-input,\n .css-unit-input select {\n margin-left: 0.5em;\n width: 4em;\n max-width: 4em;\n min-width: 4em;\n }\n}\n\n","\n.column-settings {\n display: flex;\n justify-content: space-evenly;\n flex-wrap: wrap;\n}\n\n.column-settings .size-label {\n display: block;\n margin-bottom: 0.5em;\n margin-top: 0.5em;\n}\n","@import \"../../variables\";\n\n.color-input {\n display: inline-flex;\n\n &-field.input {\n display: inline-flex;\n flex: 0 0 0;\n max-width: 9em;\n align-items: stretch;\n padding: 0.2em 8px;\n\n input {\n background: none;\n color: $fallback--lightText;\n color: var(--inputText, $fallback--lightText);\n border: none;\n padding: 0;\n margin: 0;\n\n &.textColor {\n flex: 1 0 3em;\n min-width: 3em;\n padding: 0;\n }\n\n &.nativeColor {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n }\n\n .computedIndicator,\n .transparentIndicator {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n\n .transparentIndicator {\n // forgot to install counter-strike source, ooops\n background-color: #f0f;\n position: relative;\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n background-color: #000;\n position: absolute;\n height: 50%;\n width: 50%;\n }\n\n &::after {\n top: 0;\n left: 0;\n }\n\n &::before {\n bottom: 0;\n right: 0;\n }\n }\n }\n\n .label {\n flex: 1 1 auto;\n }\n}\n","\n.color-control {\n input.text-input {\n max-width: 7em;\n flex: 1;\n }\n}\n","\n@import \"../../variables\";\n\n.shadow-control {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n margin-bottom: 1em;\n\n .shadow-preview-container,\n .shadow-tweak {\n margin: 5px 6px 0 0;\n }\n\n .shadow-preview-container {\n flex: 0;\n display: flex;\n flex-wrap: wrap;\n\n $side: 15em;\n\n input[type=\"number\"] {\n width: 5em;\n min-width: 2em;\n }\n\n .x-shift-control,\n .y-shift-control {\n display: flex;\n flex: 0;\n\n &[disabled=\"disabled\"] * {\n opacity: 0.5;\n }\n }\n\n .x-shift-control {\n align-items: flex-start;\n }\n\n .x-shift-control .wrap,\n input[type=\"range\"] {\n margin: 0;\n width: $side;\n height: 2em;\n }\n\n .y-shift-control {\n flex-direction: column;\n align-items: flex-end;\n\n .wrap {\n width: 2em;\n height: $side;\n }\n\n input[type=\"range\"] {\n transform-origin: 1em 1em;\n transform: rotate(90deg);\n }\n }\n\n .preview-window {\n flex: 1;\n background-color: #999;\n display: flex;\n align-items: center;\n justify-content: center;\n background-image:\n linear-gradient(45deg, #666 25%, transparent 25%),\n linear-gradient(-45deg, #666 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #666 75%),\n linear-gradient(-45deg, transparent 75%, #666 75%);\n background-size: 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n\n .preview-block {\n width: 33%;\n height: 33%;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n }\n\n .shadow-tweak {\n flex: 1;\n min-width: 280px;\n\n .id-control {\n align-items: stretch;\n\n .shadow-switcher {\n flex: 1;\n }\n\n .shadow-switcher,\n .btn {\n min-width: 1px;\n margin-right: 5px;\n }\n\n .btn {\n padding: 0 0.4em;\n margin: 0 0.1em;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.font-control {\n input.custom-font {\n min-width: 10em;\n }\n\n &.custom {\n /* TODO Should make proper joiners... */\n .font-switcher {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .custom-font {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n}\n","\n.contrast-ratio {\n display: flex;\n justify-content: flex-end;\n margin-top: -4px;\n margin-bottom: 5px;\n\n .label {\n margin-right: 1em;\n }\n\n .rating {\n display: inline-block;\n text-align: center;\n margin-left: 0.5em;\n }\n}\n","\n.preview-container {\n position: relative;\n}\n\n.underlay-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 10px;\n right: 10px;\n}\n","@import \"src/variables\";\n\n.theme-tab {\n padding-bottom: 2em;\n\n .preset-switcher {\n margin-right: 1em;\n }\n\n .btn {\n margin-left: 0.25em;\n margin-right: 0.25em;\n }\n\n .style-control {\n display: flex;\n align-items: baseline;\n margin-bottom: 5px;\n\n .label {\n flex: 1;\n }\n\n .opt {\n margin: 0.5em;\n }\n\n .color-input {\n flex: 0 0 0;\n }\n\n input,\n select {\n min-width: 3em;\n margin: 0;\n flex: 0;\n\n &[type=\"number\"] {\n min-width: 5em;\n }\n\n &[type=\"range\"] {\n flex: 1;\n min-width: 3em;\n align-self: flex-start;\n }\n }\n\n &.disabled {\n input,\n select {\n opacity: 0.5;\n }\n }\n }\n\n .reset-container {\n flex-wrap: wrap;\n }\n\n .fonts-container,\n .reset-container,\n .apply-container,\n .radius-container,\n .color-container, {\n display: flex;\n }\n\n .fonts-container,\n .radius-container {\n flex-direction: column;\n }\n\n .color-container {\n > h4 {\n width: 99%;\n }\n\n flex-wrap: wrap;\n justify-content: space-between;\n }\n\n .fonts-container,\n .color-container,\n .shadow-container,\n .radius-container,\n .presets-container {\n margin: 1em 1em 0;\n }\n\n .tab-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n width: 100%;\n min-height: 30px;\n margin-bottom: 1em;\n\n p {\n flex: 1;\n margin: 0;\n margin-right: 0.5em;\n }\n }\n\n .tab-header-buttons {\n display: flex;\n flex-direction: column;\n\n .btn {\n min-width: 1px;\n flex: 0 auto;\n padding: 0 1em;\n margin-bottom: 0.5em;\n }\n }\n\n .shadow-selector {\n .override {\n flex: 1;\n margin-left: 0.5em;\n }\n\n .select-container {\n margin-top: -4px;\n margin-bottom: -3px;\n }\n }\n\n .save-load,\n .save-load-options {\n display: flex;\n justify-content: center;\n align-items: baseline;\n flex-wrap: wrap;\n\n .presets,\n .import-export {\n margin-bottom: 0.5em;\n }\n\n .import-export {\n display: flex;\n }\n\n .override {\n margin-left: 0.5em;\n }\n }\n\n .save-load-options {\n flex-wrap: wrap;\n margin-top: 0.5em;\n justify-content: center;\n\n .keep-option {\n margin: 0 0.5em 0.5em;\n min-width: 25%;\n }\n }\n\n .preview-container {\n border-top: 1px dashed;\n border-bottom: 1px dashed;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n margin: 1em 0;\n padding: 1em;\n background-color: var(--wallpaper);\n background-image: var(--body-background-image);\n background-size: cover;\n background-position: 50% 50%;\n\n .dummy {\n .post {\n font-family: var(--postFont);\n display: flex;\n\n .content {\n flex: 1;\n\n h4 {\n margin-bottom: 0.25em;\n }\n\n .icons {\n margin-top: 0.5em;\n display: flex;\n\n i {\n margin-right: 1em;\n }\n }\n }\n }\n\n .after-post {\n margin-top: 1em;\n display: flex;\n align-items: center;\n }\n\n .avatar,\n .avatar-alt {\n background:\n linear-gradient(\n 135deg,\n #b8e1fc 0%,\n #a9d2f3 10%,\n #90bae4 25%,\n #90bcea 37%,\n #90bff0 50%,\n #6ba8e5 51%,\n #a2daf5 83%,\n #bdf3fd 100%\n );\n color: black;\n font-family: sans-serif;\n text-align: center;\n margin-right: 1em;\n }\n\n .avatar-alt {\n flex: 0 auto;\n margin-left: 28px;\n font-size: 12px;\n min-width: 20px;\n min-height: 20px;\n line-height: 20px;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .avatar {\n flex: 0 auto;\n width: 48px;\n height: 48px;\n font-size: 14px;\n line-height: 48px;\n }\n\n .actions {\n display: flex;\n align-items: baseline;\n\n .checkbox {\n display: inline-flex;\n align-items: baseline;\n margin-right: 1em;\n flex: 1;\n }\n }\n\n .separator {\n margin: 1em;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n .btn {\n min-width: 3em;\n }\n }\n }\n\n .radius-item {\n flex-basis: auto;\n }\n\n .radius-item,\n .color-item {\n min-width: 20em;\n margin: 5px 6px 0 0;\n display: flex;\n flex-direction: column;\n flex: 1 1 0;\n\n &.wide {\n min-width: 60%;\n }\n\n &:not(.wide):nth-child(2n+1) {\n margin-right: 7px;\n }\n\n .color,\n .opacity {\n display: flex;\n align-items: baseline;\n }\n }\n\n .theme-radius-rn,\n .theme-color-cl {\n border: 0;\n box-shadow: none;\n background: transparent;\n color: var(--faint, $fallback--faint);\n align-self: stretch;\n }\n\n .theme-color-cl,\n .theme-radius-in,\n .theme-color-in {\n margin-left: 4px;\n }\n\n .theme-radius-in {\n min-width: 1em;\n max-width: 7em;\n flex: 1;\n }\n\n .theme-radius-lb {\n max-width: 50em;\n }\n\n .theme-preview-content {\n padding: 20px;\n }\n\n .theme-warning {\n display: flex;\n align-items: baseline;\n margin-bottom: 0.5em;\n\n .buttons {\n .btn {\n margin-bottom: 0.5em;\n }\n }\n }\n}\n\n.extra-content {\n .apply-container {\n display: flex;\n flex-direction: row;\n justify-content: space-around;\n flex-grow: 1;\n\n /* stylelint-disable-next-line no-descending-specificity */\n .btn {\n flex-grow: 1;\n min-height: 2em;\n min-width: 0;\n max-width: 10em;\n padding: 0;\n }\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/9114.8def3b2b7fe70b3b3712.css b/priv/static/static/css/9114.8def3b2b7fe70b3b3712.css deleted file mode 100644 index 6c25e903087e00fc72c45b4dd1111867c9c00c2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19517 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 * {\n min-width: 0;\n }\n }\n\n &-item-selected-inner {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n }\n\n &-header {\n display: flex;\n align-items: center;\n padding: 0.6em 0;\n border-bottom: 2px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n\n &-actions {\n flex: 1;\n }\n }\n\n &-checkbox-wrapper {\n padding: 0 10px;\n flex: none;\n }\n}\n",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 1rem;\n }\n }\n}\n",".mutes-and-blocks-tab {\n height: 100%;\n\n .usersearch-wrapper {\n padding: 1em;\n }\n\n .bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n }\n\n .bulk-action-button {\n width: 10em;\n }\n\n .domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n }\n\n .domain-mute-button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n}\n","\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ServerSideIndicator {\n display: inline-block;\n position: relative;\n}\n\n.serverside-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n@import \"../../../../variables\";\n\n.mfa-backup-codes {\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .backup-codes {\n font-family: var(--postCodeFont, monospace);\n }\n}\n","\n@import \"../../../../variables\";\n\n.mfa-settings {\n .mfa-heading,\n .method-item {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: baseline;\n }\n\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .setup-otp {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n\n .qr-code {\n flex: 1;\n padding-right: 10px;\n }\n .verify { flex: 1; }\n .error { margin: 4px 0 0; }\n\n .confirm-otp-actions {\n button {\n width: 15em;\n margin-top: 5px;\n }\n }\n }\n}\n","/*!\n * Cropper.js v1.5.12\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2021-06-12T08:00:11.623Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 0.75);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n.cropper-center::after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n","\n.image-cropper {\n &-img-input {\n display: none;\n }\n\n &-image-container {\n position: relative;\n\n img {\n display: block;\n max-width: 100%;\n }\n }\n\n &-buttons-wrapper {\n margin-top: 10px;\n\n button {\n margin-top: 5px;\n }\n }\n}\n","@import \"../../../variables\";\n\n.profile-tab {\n .bio {\n margin: 0;\n }\n\n .visibility-tray {\n padding-top: 5px;\n }\n\n input[type=\"file\"] {\n padding: 5px;\n height: auto;\n }\n\n .banner-background-preview {\n max-width: 100%;\n width: 300px;\n position: relative;\n\n img {\n width: 100%;\n }\n }\n\n .uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n\n .name-changer {\n width: 100%;\n }\n\n .current-avatar-container {\n position: relative;\n width: 150px;\n height: 150px;\n }\n\n .current-avatar {\n display: block;\n width: 100%;\n height: 100%;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n }\n\n .reset-button {\n position: absolute;\n top: 0.2em;\n right: 0.2em;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n background-color: rgb(0 0 0 / 60%);\n opacity: 0.7;\n width: 1.5em;\n height: 1.5em;\n text-align: center;\n line-height: 1.5em;\n font-size: 1.5em;\n cursor: pointer;\n\n &:hover {\n opacity: 1;\n }\n\n svg {\n color: white;\n }\n }\n\n .oauth-tokens {\n width: 100%;\n\n th {\n text-align: left;\n }\n\n .actions {\n text-align: right;\n }\n }\n\n &-usersearch-wrapper {\n padding: 1em;\n }\n\n &-bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n\n button {\n width: 10em;\n }\n }\n\n &-domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n\n button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n }\n\n .setting-subitem {\n margin-left: 1.75em;\n }\n\n .profile-fields {\n display: flex;\n\n & > .emoji-input {\n flex: 1 1 auto;\n margin: 0 0.2em 0.5em;\n min-width: 0;\n }\n\n .delete-field {\n width: 20px;\n align-self: center;\n margin: 0 0.2em 0.5em;\n padding: 0 0.5em;\n }\n }\n\n .birthday-input {\n display: block;\n margin-bottom: 1em;\n }\n}\n","\n.css-unit-input,\n.css-unit-input select {\n margin-left: 0.5em;\n width: 4em;\n max-width: 4em;\n min-width: 4em;\n}\n","\n.column-settings {\n display: flex;\n justify-content: space-evenly;\n flex-wrap: wrap;\n}\n\n.column-settings .size-label {\n display: block;\n margin-bottom: 0.5em;\n margin-top: 0.5em;\n}\n","@import \"../../variables\";\n\n.color-input {\n display: inline-flex;\n\n &-field.input {\n display: inline-flex;\n flex: 0 0 0;\n max-width: 9em;\n align-items: stretch;\n padding: 0.2em 8px;\n\n input {\n background: none;\n color: $fallback--lightText;\n color: var(--inputText, $fallback--lightText);\n border: none;\n padding: 0;\n margin: 0;\n\n &.textColor {\n flex: 1 0 3em;\n min-width: 3em;\n padding: 0;\n }\n\n &.nativeColor {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n }\n\n .computedIndicator,\n .transparentIndicator {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n\n .transparentIndicator {\n // forgot to install counter-strike source, ooops\n background-color: #f0f;\n position: relative;\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n background-color: #000;\n position: absolute;\n height: 50%;\n width: 50%;\n }\n\n &::after {\n top: 0;\n left: 0;\n }\n\n &::before {\n bottom: 0;\n right: 0;\n }\n }\n }\n\n .label {\n flex: 1 1 auto;\n }\n}\n","\n.color-control {\n input.text-input {\n max-width: 7em;\n flex: 1;\n }\n}\n","\n@import \"../../variables\";\n\n.shadow-control {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n margin-bottom: 1em;\n\n .shadow-preview-container,\n .shadow-tweak {\n margin: 5px 6px 0 0;\n }\n\n .shadow-preview-container {\n flex: 0;\n display: flex;\n flex-wrap: wrap;\n\n $side: 15em;\n\n input[type=\"number\"] {\n width: 5em;\n min-width: 2em;\n }\n\n .x-shift-control,\n .y-shift-control {\n display: flex;\n flex: 0;\n\n &[disabled=\"disabled\"] * {\n opacity: 0.5;\n }\n }\n\n .x-shift-control {\n align-items: flex-start;\n }\n\n .x-shift-control .wrap,\n input[type=\"range\"] {\n margin: 0;\n width: $side;\n height: 2em;\n }\n\n .y-shift-control {\n flex-direction: column;\n align-items: flex-end;\n\n .wrap {\n width: 2em;\n height: $side;\n }\n\n input[type=\"range\"] {\n transform-origin: 1em 1em;\n transform: rotate(90deg);\n }\n }\n\n .preview-window {\n flex: 1;\n background-color: #999;\n display: flex;\n align-items: center;\n justify-content: center;\n background-image:\n linear-gradient(45deg, #666 25%, transparent 25%),\n linear-gradient(-45deg, #666 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #666 75%),\n linear-gradient(-45deg, transparent 75%, #666 75%);\n background-size: 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n\n .preview-block {\n width: 33%;\n height: 33%;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n }\n\n .shadow-tweak {\n flex: 1;\n min-width: 280px;\n\n .id-control {\n align-items: stretch;\n\n .shadow-switcher {\n flex: 1;\n }\n\n .shadow-switcher,\n .btn {\n min-width: 1px;\n margin-right: 5px;\n }\n\n .btn {\n padding: 0 0.4em;\n margin: 0 0.1em;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.font-control {\n input.custom-font {\n min-width: 10em;\n }\n\n &.custom {\n /* TODO Should make proper joiners... */\n .font-switcher {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .custom-font {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n}\n","\n.contrast-ratio {\n display: flex;\n justify-content: flex-end;\n margin-top: -4px;\n margin-bottom: 5px;\n\n .label {\n margin-right: 1em;\n }\n\n .rating {\n display: inline-block;\n text-align: center;\n margin-left: 0.5em;\n }\n}\n","\n.preview-container {\n position: relative;\n}\n\n.underlay-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 10px;\n right: 10px;\n}\n","@import \"src/variables\";\n\n.theme-tab {\n padding-bottom: 2em;\n\n .preset-switcher {\n margin-right: 1em;\n }\n\n .btn {\n margin-left: 0.25em;\n margin-right: 0.25em;\n }\n\n .style-control {\n display: flex;\n align-items: baseline;\n margin-bottom: 5px;\n\n .label {\n flex: 1;\n }\n\n .opt {\n margin: 0.5em;\n }\n\n .color-input {\n flex: 0 0 0;\n }\n\n input,\n select {\n min-width: 3em;\n margin: 0;\n flex: 0;\n\n &[type=\"number\"] {\n min-width: 5em;\n }\n\n &[type=\"range\"] {\n flex: 1;\n min-width: 3em;\n align-self: flex-start;\n }\n }\n\n &.disabled {\n input,\n select {\n opacity: 0.5;\n }\n }\n }\n\n .reset-container {\n flex-wrap: wrap;\n }\n\n .fonts-container,\n .reset-container,\n .apply-container,\n .radius-container,\n .color-container, {\n display: flex;\n }\n\n .fonts-container,\n .radius-container {\n flex-direction: column;\n }\n\n .color-container {\n > h4 {\n width: 99%;\n }\n\n flex-wrap: wrap;\n justify-content: space-between;\n }\n\n .fonts-container,\n .color-container,\n .shadow-container,\n .radius-container,\n .presets-container {\n margin: 1em 1em 0;\n }\n\n .tab-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n width: 100%;\n min-height: 30px;\n margin-bottom: 1em;\n\n p {\n flex: 1;\n margin: 0;\n margin-right: 0.5em;\n }\n }\n\n .tab-header-buttons {\n display: flex;\n flex-direction: column;\n\n .btn {\n min-width: 1px;\n flex: 0 auto;\n padding: 0 1em;\n margin-bottom: 0.5em;\n }\n }\n\n .shadow-selector {\n .override {\n flex: 1;\n margin-left: 0.5em;\n }\n\n .select-container {\n margin-top: -4px;\n margin-bottom: -3px;\n }\n }\n\n .save-load,\n .save-load-options {\n display: flex;\n justify-content: center;\n align-items: baseline;\n flex-wrap: wrap;\n\n .presets,\n .import-export {\n margin-bottom: 0.5em;\n }\n\n .import-export {\n display: flex;\n }\n\n .override {\n margin-left: 0.5em;\n }\n }\n\n .save-load-options {\n flex-wrap: wrap;\n margin-top: 0.5em;\n justify-content: center;\n\n .keep-option {\n margin: 0 0.5em 0.5em;\n min-width: 25%;\n }\n }\n\n .preview-container {\n border-top: 1px dashed;\n border-bottom: 1px dashed;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n margin: 1em 0;\n padding: 1em;\n background-color: var(--wallpaper);\n background-image: var(--body-background-image);\n background-size: cover;\n background-position: 50% 50%;\n\n .dummy {\n .post {\n font-family: var(--postFont);\n display: flex;\n\n .content {\n flex: 1;\n\n h4 {\n margin-bottom: 0.25em;\n }\n\n .icons {\n margin-top: 0.5em;\n display: flex;\n\n i {\n margin-right: 1em;\n }\n }\n }\n }\n\n .after-post {\n margin-top: 1em;\n display: flex;\n align-items: center;\n }\n\n .avatar,\n .avatar-alt {\n background:\n linear-gradient(\n 135deg,\n #b8e1fc 0%,\n #a9d2f3 10%,\n #90bae4 25%,\n #90bcea 37%,\n #90bff0 50%,\n #6ba8e5 51%,\n #a2daf5 83%,\n #bdf3fd 100%\n );\n color: black;\n font-family: sans-serif;\n text-align: center;\n margin-right: 1em;\n }\n\n .avatar-alt {\n flex: 0 auto;\n margin-left: 28px;\n font-size: 12px;\n min-width: 20px;\n min-height: 20px;\n line-height: 20px;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .avatar {\n flex: 0 auto;\n width: 48px;\n height: 48px;\n font-size: 14px;\n line-height: 48px;\n }\n\n .actions {\n display: flex;\n align-items: baseline;\n\n .checkbox {\n display: inline-flex;\n align-items: baseline;\n margin-right: 1em;\n flex: 1;\n }\n }\n\n .separator {\n margin: 1em;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n .btn {\n min-width: 3em;\n }\n }\n }\n\n .radius-item {\n flex-basis: auto;\n }\n\n .radius-item,\n .color-item {\n min-width: 20em;\n margin: 5px 6px 0 0;\n display: flex;\n flex-direction: column;\n flex: 1 1 0;\n\n &.wide {\n min-width: 60%;\n }\n\n &:not(.wide):nth-child(2n+1) {\n margin-right: 7px;\n }\n\n .color,\n .opacity {\n display: flex;\n align-items: baseline;\n }\n }\n\n .theme-radius-rn,\n .theme-color-cl {\n border: 0;\n box-shadow: none;\n background: transparent;\n color: var(--faint, $fallback--faint);\n align-self: stretch;\n }\n\n .theme-color-cl,\n .theme-radius-in,\n .theme-color-in {\n margin-left: 4px;\n }\n\n .theme-radius-in {\n min-width: 1em;\n max-width: 7em;\n flex: 1;\n }\n\n .theme-radius-lb {\n max-width: 50em;\n }\n\n .theme-preview-content {\n padding: 20px;\n }\n\n .theme-warning {\n display: flex;\n align-items: baseline;\n margin-bottom: 0.5em;\n\n .buttons {\n .btn {\n margin-bottom: 0.5em;\n }\n }\n }\n}\n\n.extra-content {\n .apply-container {\n display: flex;\n flex-direction: row;\n justify-content: space-around;\n flex-grow: 1;\n\n /* stylelint-disable-next-line no-descending-specificity */\n .btn {\n flex-grow: 1;\n min-height: 2em;\n min-width: 0;\n max-width: 10em;\n padding: 0;\n }\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n\n .number-input {\n max-width: 6em;\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/9801.cfe503d4c949ae0c3813.css b/priv/static/static/css/9801.cfe503d4c949ae0c3813.css new file mode 100644 index 0000000000000000000000000000000000000000..b27df4a19f6e80a806e1267d2e6ce2fa158028fd GIT binary patch literal 1884 zcmbtU+iKh}5Pk1g7($>4E5+VTVArOBmcA4kC<%QoT4}raG>OBnhm$8v;!dR;lm}bWA6Dlm;R(D)wR<}R)5g0t z{vANRT9*4Sd+)V!vl}oDis<$nO$6j@FMZbxA|dzZj?`2%HQs^ov-gY_WoTX3uZXhY zXaL>HRRHb^pC&8q3AObD$vmv;a`%?8) z&U_p3+Y-wl{EH>HR^Dpq`aX@&y?Q@t+q`%QcAqn9L61r1Hq-+p9Pw-$QO<1ml<0hA zm=M8xXm9jy;wbE%KyloluisMh2CaSBqRapDrFoNyXU7Azmv_ugdnEgmdzFS-* z>*aD4Uv&zlL7ZkfqlDfDnL7G?{P}QJ;d_CHMGs|4Vv6uyvMtELLajfadvbQsR5IrY zFHnj%)E`CtW4R#wj6OG$g1E}s)9_h;xVgRvL$$XY9+)v div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/app.48e52505beba5b9ab69b.css b/priv/static/static/css/app.48e52505beba5b9ab69b.css deleted file mode 100644 index ee1ea9cb439dd39f1b70fd7eb159d7381866a9b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 124954 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 F{=a;iI0FCx diff --git a/priv/static/static/css/app.48e52505beba5b9ab69b.css.map b/priv/static/static/css/app.48e52505beba5b9ab69b.css.map deleted file mode 100644 index a87315d2b..000000000 --- a/priv/static/static/css/app.48e52505beba5b9ab69b.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"static/css/app.48e52505beba5b9ab69b.css","mappings":"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","sources":["webpack://pleroma_fe/./src/components/modal/modal.vue","webpack://pleroma_fe/./node_modules/vue-virtual-scroller/dist/vue-virtual-scroller.css","webpack://pleroma_fe/./src/components/login_form/login_form.vue","webpack://pleroma_fe/./src/components/media_upload/media_upload.vue","webpack://pleroma_fe/./src/components/scope_selector/scope_selector.vue","webpack://pleroma_fe/./src/_variables.scss","webpack://pleroma_fe/./src/components/checkbox/checkbox.vue","webpack://pleroma_fe/./src/components/popover/popover.vue","webpack://pleroma_fe/./src/components/still-image/still-image.vue","webpack://pleroma_fe/./src/components/emoji_picker/emoji_picker.scss","webpack://pleroma_fe/./src/components/emoji_input/emoji_input.vue","webpack://pleroma_fe/./src/components/select/select.vue","webpack://pleroma_fe/./src/components/poll/poll_form.vue","webpack://pleroma_fe/./src/components/flash/flash.vue","webpack://pleroma_fe/./src/components/attachment/attachment.scss","webpack://pleroma_fe/./src/components/gallery/gallery.vue","webpack://pleroma_fe/./src/components/user_avatar/user_avatar.vue","webpack://pleroma_fe/./src/components/mention_link/mention_link.scss","webpack://pleroma_fe/./src/components/mentions_line/mentions_line.scss","webpack://pleroma_fe/./src/components/hashtag_link/hashtag_link.scss","webpack://pleroma_fe/./src/components/rich_content/rich_content.scss","webpack://pleroma_fe/./src/components/poll/poll.vue","webpack://pleroma_fe/./src/components/status_body/status_body.scss","webpack://pleroma_fe/./src/components/link-preview/link-preview.vue","webpack://pleroma_fe/./src/components/status_content/status_content.vue","webpack://pleroma_fe/./src/components/post_status_form/post_status_form.vue","webpack://pleroma_fe/./src/components/remote_follow/remote_follow.vue","webpack://pleroma_fe/./src/components/dialog_modal/dialog_modal.vue","webpack://pleroma_fe/./src/components/moderation_tools/moderation_tools.vue","webpack://pleroma_fe/./src/components/account_actions/account_actions.vue","webpack://pleroma_fe/./src/components/user_note/user_note.vue","webpack://pleroma_fe/./src/components/user_card/user_card.scss","webpack://pleroma_fe/./src/components/user_panel/user_panel.vue","webpack://pleroma_fe/./src/components/navigation/navigation_entry.vue","webpack://pleroma_fe/./src/components/navigation/navigation_pins.vue","webpack://pleroma_fe/./src/components/nav_panel/nav_panel.vue","webpack://pleroma_fe/./src/components/features_panel/features_panel.vue","webpack://pleroma_fe/./src/components/who_to_follow_panel/who_to_follow_panel.vue","webpack://pleroma_fe/./src/components/shout_panel/shout_panel.vue","webpack://pleroma_fe/./src/components/media_modal/media_modal.vue","webpack://pleroma_fe/./src/components/side_drawer/side_drawer.vue","webpack://pleroma_fe/./src/components/mobile_post_status_button/mobile_post_status_button.vue","webpack://pleroma_fe/./src/components/reply_button/reply_button.vue","webpack://pleroma_fe/./src/components/favorite_button/favorite_button.vue","webpack://pleroma_fe/./src/components/react_button/react_button.vue","webpack://pleroma_fe/./src/components/retweet_button/retweet_button.vue","webpack://pleroma_fe/./src/components/extra_buttons/extra_buttons.vue","webpack://pleroma_fe/./src/components/avatar_list/avatar_list.vue","webpack://pleroma_fe/./src/components/status_popover/status_popover.vue","webpack://pleroma_fe/./src/components/user_list_popover/user_list_popover.vue","webpack://pleroma_fe/./src/components/emoji_reactions/emoji_reactions.vue","webpack://pleroma_fe/./src/components/status/status.scss","webpack://pleroma_fe/./src/components/report/report.scss","webpack://pleroma_fe/./src/components/notification/notification.scss","webpack://pleroma_fe/./src/components/notifications/notifications.scss","webpack://pleroma_fe/./src/components/mobile_nav/mobile_nav.vue","webpack://pleroma_fe/./src/components/search_bar/search_bar.vue","webpack://pleroma_fe/./src/components/desktop_nav/desktop_nav.scss","webpack://pleroma_fe/./src/components/list/list.vue","webpack://pleroma_fe/./src/components/user_reporting_modal/user_reporting_modal.vue","webpack://pleroma_fe/./src/components/edit_status_modal/edit_status_modal.vue","webpack://pleroma_fe/./src/components/post_status_modal/post_status_modal.vue","webpack://pleroma_fe/./src/components/status_history_modal/status_history_modal.vue","webpack://pleroma_fe/./src/components/global_notice_list/global_notice_list.vue","webpack://pleroma_fe/./src/App.scss","webpack://pleroma_fe/./src/panel.scss","webpack://pleroma_fe/./src/components/thread_tree/thread_tree.vue","webpack://pleroma_fe/./src/components/conversation/conversation.vue","webpack://pleroma_fe/./src/components/timeline_menu/timeline_menu.vue","webpack://pleroma_fe/./src/components/timeline/timeline.scss","webpack://pleroma_fe/./src/components/tab_switcher/tab_switcher.scss","webpack://pleroma_fe/./src/components/chat_title/chat_title.vue","webpack://pleroma_fe/./src/components/chat_list_item/chat_list_item.scss","webpack://pleroma_fe/./src/components/basic_user_card/basic_user_card.vue","webpack://pleroma_fe/./src/components/chat_new/chat_new.scss","webpack://pleroma_fe/./src/components/chat_list/chat_list.vue","webpack://pleroma_fe/./src/components/chat_message/chat_message.scss","webpack://pleroma_fe/./src/components/chat/chat.scss","webpack://pleroma_fe/./src/components/follow_card/follow_card.vue","webpack://pleroma_fe/./src/hocs/with_load_more/with_load_more.scss","webpack://pleroma_fe/./src/components/user_profile/user_profile.vue","webpack://pleroma_fe/./src/components/search/search.vue","webpack://pleroma_fe/./src/components/interface_language_switcher/interface_language_switcher.vue","webpack://pleroma_fe/./src/components/registration/registration.vue","webpack://pleroma_fe/./src/components/password_reset/password_reset.vue","webpack://pleroma_fe/./src/components/follow_request_card/follow_request_card.vue","webpack://pleroma_fe/./src/components/terms_of_service_panel/terms_of_service_panel.vue","webpack://pleroma_fe/./src/components/staff_panel/staff_panel.vue","webpack://pleroma_fe/./src/components/mrf_transparency_panel/mrf_transparency_panel.scss","webpack://pleroma_fe/./src/components/lists_card/lists_card.vue","webpack://pleroma_fe/./src/components/lists/lists.vue","webpack://pleroma_fe/./src/components/lists_user_search/lists_user_search.vue","webpack://pleroma_fe/./src/components/panel_loading/panel_loading.vue","webpack://pleroma_fe/./src/components/lists_edit/lists_edit.vue","webpack://pleroma_fe/./src/components/announcement_editor/announcement_editor.vue","webpack://pleroma_fe/./src/components/announcement/announcement.vue","webpack://pleroma_fe/./src/components/announcements_page/announcements_page.vue"],"sourcesContent":["\n.modal-view {\n z-index: var(--ZI_modals);\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow: auto;\n pointer-events: none;\n animation-duration: 0.2s;\n animation-name: modal-background-fadein;\n opacity: 0;\n\n > * {\n pointer-events: initial;\n }\n\n &.modal-background {\n pointer-events: initial;\n background-color: rgb(0 0 0 / 50%);\n }\n\n &.open {\n opacity: 1;\n }\n}\n\n@keyframes modal-background-fadein {\n from {\n background-color: rgb(0 0 0 / 0%);\n }\n\n to {\n background-color: rgb(0 0 0 / 50%);\n }\n}\n",".vue-recycle-scroller{position:relative}.vue-recycle-scroller.direction-vertical:not(.page-mode){overflow-y:auto}.vue-recycle-scroller.direction-horizontal:not(.page-mode){overflow-x:auto}.vue-recycle-scroller.direction-horizontal{display:flex}.vue-recycle-scroller__slot{flex:auto 0 0}.vue-recycle-scroller__item-wrapper{flex:1;box-sizing:border-box;overflow:hidden;position:relative}.vue-recycle-scroller.ready .vue-recycle-scroller__item-view{position:absolute;top:0;left:0;will-change:transform}.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper{width:100%}.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper{height:100%}.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view{width:100%}.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view{height:100%}.resize-observer[data-v-b329ee4c]{position:absolute;top:0;left:0;z-index:-1;width:100%;height:100%;border:none;background-color:transparent;pointer-events:none;display:block;overflow:hidden;opacity:0}.resize-observer[data-v-b329ee4c] object{display:block;position:absolute;top:0;left:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1}","\n@import \"../../variables\";\n\n.login-form {\n display: flex;\n flex-direction: column;\n padding: 0.6em;\n\n .btn {\n min-height: 2em;\n width: 10em;\n }\n\n .register {\n flex: 1 1;\n }\n\n .login-bottom {\n margin-top: 1em;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n }\n\n .form-group {\n display: flex;\n flex-direction: column;\n padding: 0.3em 0.5em 0.6em;\n line-height: 24px;\n }\n\n .form-bottom {\n display: flex;\n padding: 0.5em;\n height: 32px;\n\n button {\n width: 10em;\n }\n\n p {\n margin: 0.35em;\n padding: 0.35em;\n display: flex;\n }\n }\n\n .error {\n text-align: center;\n animation-name: shakeError;\n animation-duration: 0.4s;\n animation-timing-function: ease-in-out;\n }\n}\n","\n@import \"../../variables\";\n\n.media-upload {\n cursor: pointer; // We use