From 675639225a905f5b0b2650cd3f20a4758fc3f868 Mon Sep 17 00:00:00 2001
From: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Fri, 28 Apr 2023 11:13:42 +0000
Subject: [PATCH 001/106] allow https: so that flash works across instances
without need for media proxy
---
lib/pleroma/web/plugs/http_security_plug.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index 34895c8d5..045384e08 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -104,7 +104,7 @@ defp csp_string do
{[img_src, " https:"], [media_src, " https:"]}
end
- connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
+ connect_src = ["connect-src 'self' blob: https: ", static_url, ?\s, websocket_url]
connect_src =
if Config.get(:env) == :dev do
From cd20d15bb8d2f97f8dd0850993041f15865cdda9 Mon Sep 17 00:00:00 2001
From: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Fri, 28 Apr 2023 11:19:14 +0000
Subject: [PATCH 002/106] changelog
---
changelog.d/3879.fix | 1 +
1 file changed, 1 insertion(+)
create mode 100644 changelog.d/3879.fix
diff --git a/changelog.d/3879.fix b/changelog.d/3879.fix
new file mode 100644
index 000000000..7c58cc3c2
--- /dev/null
+++ b/changelog.d/3879.fix
@@ -0,0 +1 @@
+fix not being able to fetch flash file from remote instance
\ No newline at end of file
From c0d11da2d8edc57ef88163c06a19aad3e28d14db Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Sun, 7 May 2023 15:16:30 +0300
Subject: [PATCH 003/106] conditionally set csp depnding on media-proxy state
---
lib/pleroma/web/plugs/http_security_plug.ex | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index 045384e08..df46cfa0c 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -93,18 +93,26 @@ defp csp_string do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
+ connect_src = ["connect-src 'self' blob:", static_url, ?\s, websocket_url]
# Strict multimedia CSP enforcement only when MediaProxy is enabled
- {img_src, media_src} =
+ {img_src, media_src, connect_src} =
if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
sources = build_csp_multimedia_source_list()
- {[img_src, sources], [media_src, sources]}
+ {
+ [img_src, sources],
+ [media_src, sources],
+ [connect_src, sources]
+ }
else
- {[img_src, " https:"], [media_src, " https:"]}
+ {
+ [img_src, " https:"],
+ [media_src, " https:"],
+ [connect_src, " https:"]
+ }
end
- connect_src = ["connect-src 'self' blob: https: ", static_url, ?\s, websocket_url]
connect_src =
if Config.get(:env) == :dev do
From f8ef4924ecab5ba6851eee82845624bc15f868de Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Sun, 7 May 2023 15:24:09 +0300
Subject: [PATCH 004/106] fix whitespace
---
lib/pleroma/web/plugs/http_security_plug.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index df46cfa0c..a3166bc96 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -93,7 +93,7 @@ defp csp_string do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
- connect_src = ["connect-src 'self' blob:", static_url, ?\s, websocket_url]
+ connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src, connect_src} =
From f50fd9278fd36e6bd3ae36bb7f5033d9fd8a84ac Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Sun, 7 May 2023 15:29:19 +0300
Subject: [PATCH 005/106] reduce redundant reduntancy reduction
---
lib/pleroma/web/plugs/http_security_plug.ex | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index a3166bc96..b189d5bfd 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -93,7 +93,7 @@ defp csp_string do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
- connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
+ connect_src = "connect-src 'self' blob:"
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src, connect_src} =
@@ -103,7 +103,7 @@ defp csp_string do
{
[img_src, sources],
[media_src, sources],
- [connect_src, sources]
+ [connect_src, sources, ?\s, websocket_url]
}
else
{
From 2a07411b0cb14ea26966659605d95074b02a8538 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Sun, 7 May 2023 15:34:17 +0300
Subject: [PATCH 006/106] keep the websocket url for all modes
---
lib/pleroma/web/plugs/http_security_plug.ex | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index b189d5bfd..b3dc8a3a6 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -93,7 +93,7 @@ defp csp_string do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
- connect_src = "connect-src 'self' blob:"
+ connect_src = ["connect-src 'self' blob: ", ?\s, websocket_url]
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src, connect_src} =
@@ -103,7 +103,7 @@ defp csp_string do
{
[img_src, sources],
[media_src, sources],
- [connect_src, sources, ?\s, websocket_url]
+ [connect_src, sources]
}
else
{
From 18a0c923d0da4c8fb6e33b383dabd1d06bb22968 Mon Sep 17 00:00:00 2001
From: Mark Felder
Date: Thu, 3 Aug 2023 13:08:37 -0400
Subject: [PATCH 007/106] 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 008/106] 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 009/106] 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 010/106] 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 011/106] 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 012/106] 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 013/106] 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 014/106] 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 015/106] 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 016/106] 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 017/106] 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 018/106] 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 d838d1990bf23d452c1cc830629e42e51dbd7047 Mon Sep 17 00:00:00 2001
From: Haelwenn
Date: Wed, 16 Aug 2023 13:34:32 +0000
Subject: [PATCH 019/106] Apply lanodan's suggestion(s) to 1 file(s)
---
lib/pleroma/web/plugs/http_security_plug.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index b3dc8a3a6..a3166bc96 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -93,7 +93,7 @@ defp csp_string do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
- connect_src = ["connect-src 'self' blob: ", ?\s, websocket_url]
+ connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src, connect_src} =
From 3d09bc320efcc68beb9b57fba23b6b9f3dc17905 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 30 Aug 2023 20:34:16 -0400
Subject: [PATCH 020/106] Make lint happy
---
lib/pleroma/web/plugs/http_security_plug.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index a3166bc96..5093414c4 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -100,6 +100,7 @@ defp csp_string do
if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
sources = build_csp_multimedia_source_list()
+
{
[img_src, sources],
[media_src, sources],
@@ -113,7 +114,6 @@ defp csp_string do
}
end
-
connect_src =
if Config.get(:env) == :dev do
[connect_src, " http://localhost:3035/"]
From 3c5ecca37718a1eba05be1f379b8f47362079c65 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 30 Aug 2023 20:37:45 -0400
Subject: [PATCH 021/106] Skip changelog
---
changelog.d/lint.skip | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 changelog.d/lint.skip
diff --git a/changelog.d/lint.skip b/changelog.d/lint.skip
new file mode 100644
index 000000000..e69de29bb
From 1afde067b12ad0062c1820091ea9b0a680819281 Mon Sep 17 00:00:00 2001
From: Mint
Date: Sat, 2 Sep 2023 01:43:25 +0300
Subject: [PATCH 022/106] CommonAPI: Prevent users from accessing media of
other users
---
.../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 | 27 ++++++------
test/pleroma/web/common_api/utils_test.exs | 43 +++++++++++++------
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, 82 insertions(+), 31 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 77b3fa5d2..82c7f70d2 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 b9fe0224c..0f394e951 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,22 +45,23 @@ 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
+ defp get_attachment(media_id, user) do
with %Object{data: data} = object <- Repo.get(Object, media_id),
- %{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data do
+ %{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data,
+ :ok <- Object.authorize_access(object, user) do
object
else
_ -> nil
diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs
index ca5b92683..4ce039d64 100644
--- a/test/pleroma/web/common_api/utils_test.exs
+++ b/test/pleroma/web/common_api/utils_test.exs
@@ -586,46 +586,61 @@ 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(:attachment)
+ user = insert(:user)
+ object = insert(:attachment, %{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(:attachment)
+ user = insert(:user)
+ object = insert(:attachment, %{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(:attachment)
- assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data]
+ user = insert(:user)
+ object = insert(:attachment, %{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
test "checks that the object is of upload type" do
object = insert(:note)
- assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == []
+ assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, nil) == []
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 968e11a14..0d76d6581 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 535a5ecad04c9c49105a77e7025fe9f4b4d23ba6 Mon Sep 17 00:00:00 2001
From: Mint
Date: Sat, 2 Sep 2023 01:43:25 +0300
Subject: [PATCH 023/106] 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 024/106] 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 31eb3dc24587ad3715da9fe00886867a6c0bf0c4 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 16:41:30 -0600
Subject: [PATCH 025/106] ObjectValidators: accept "quoteUrl" field
---
.../web/activity_pub/object_validators/common_fields.ex | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index d580208df..835ed97b7 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -27,7 +27,7 @@ defmacro activity_fields do
end
end
- # All objects except Answer and CHatMessage
+ # All objects except Answer and ChatMessage
defmacro object_fields do
quote bind_quoted: binding() do
field(:content, :string)
@@ -58,6 +58,7 @@ defmacro status_object_fields do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inReplyTo, ObjectValidators.ObjectID)
+ field(:quoteUrl, ObjectValidators.ObjectID)
field(:url, ObjectValidators.BareUri)
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
From 7deda1fa18cb95a588ba66950ac45262c467a7f4 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 17:30:49 -0600
Subject: [PATCH 026/106] Quote post: add fixtures
---
.../quote_post/fedibird_quote_post.json | 52 +++++++++++++++++++
.../quote_post/misskey_quote_post.json | 46 ++++++++++++++++
2 files changed, 98 insertions(+)
create mode 100644 test/fixtures/quote_post/fedibird_quote_post.json
create mode 100644 test/fixtures/quote_post/misskey_quote_post.json
diff --git a/test/fixtures/quote_post/fedibird_quote_post.json b/test/fixtures/quote_post/fedibird_quote_post.json
new file mode 100644
index 000000000..ebf383356
--- /dev/null
+++ b/test/fixtures/quote_post/fedibird_quote_post.json
@@ -0,0 +1,52 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "expiry": "toot:expiry"
+ }
+ ],
+ "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-01-22T02:07:16Z",
+ "url": "https://fedibird.com/@noellabo/107663670404015196",
+ "attributedTo": "https://fedibird.com/users/noellabo",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://fedibird.com/users/noellabo/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/noellabo/statuses/107663670404015196",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-01-22:objectId=107663670404038002:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/107663670404038002",
+ "quoteURL": "https://misskey.io/notes/8vsn2izjwh",
+ "_misskey_quote": "https://misskey.io/notes/8vsn2izjwh",
+ "_misskey_content": "いつの生まれだシトリン",
+ "content": "いつの生まれだシトリン QT: https:// misskey.io/notes/8vsn2izjwh
",
+ "contentMap": {
+ "ja": "いつの生まれだシトリン QT: https:// misskey.io/notes/8vsn2izjwh
"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/fixtures/quote_post/misskey_quote_post.json b/test/fixtures/quote_post/misskey_quote_post.json
new file mode 100644
index 000000000..59f677ca9
--- /dev/null
+++ b/test/fixtures/quote_post/misskey_quote_post.json
@@ -0,0 +1,46 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey.io/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "id": "https://misskey.io/notes/8vs6ylpfez",
+ "type": "Note",
+ "attributedTo": "https://misskey.io/users/7rkrarq81i",
+ "summary": null,
+ "content": "投稿者の設定によるね Fanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある RE: https://misskey.io/notes/8vs6wxufd0
",
+ "_misskey_content": "投稿者の設定によるね\nFanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある",
+ "_misskey_quote": "https://misskey.io/notes/8vs6wxufd0",
+ "quoteUrl": "https://misskey.io/notes/8vs6wxufd0",
+ "published": "2022-01-21T16:38:30.243Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://misskey.io/users/7rkrarq81i/followers"
+ ],
+ "inReplyTo": null,
+ "attachment": [],
+ "sensitive": false,
+ "tag": []
+}
From 795736af16dca77929725e7dd55f5de04a796fdb Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 18:03:22 -0600
Subject: [PATCH 027/106] ObjectValidators: improve quoteUrl compatibility
---
.../article_note_page_validator.ex | 16 +++++++++++++++
.../article_note_page_validator_test.exs | 20 +++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index 2670e3f17..40bb67934 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -76,6 +76,21 @@ def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment
def fix_attachments(data), do: data
+ defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
+
+ # Fix for Fedibird
+ # https://github.com/fedibird/mastodon/issues/9
+ defp fix_quote_url(%{"quoteURL" => quote_url} = data) do
+ Map.put(data, "quoteUrl", quote_url)
+ end
+
+ # Misskey fallback
+ defp fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
+ Map.put(data, "quoteUrl", quote_url)
+ end
+
+ defp fix_quote_url(data), do: data
+
defp fix(data) do
data
|> CommonFixes.fix_actor()
@@ -84,6 +99,7 @@ defp fix(data) do
|> fix_tag()
|> fix_replies()
|> fix_attachments()
+ |> fix_quote_url()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c7a62be18..c3cde00b5 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -116,4 +116,24 @@ test "a Note without replies/first/items validates" do
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+
+ test "Fedibird quote post" do
+ insert(:user, ap_id: "https://fedibird.com/users/noellabo")
+
+ data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
+ chg = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert chg.valid?
+ assert chg.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
+ end
+
+ test "Misskey quote post" do
+ insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
+
+ data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+ chg = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert chg.valid?
+ assert chg.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
+ end
end
From b022d6635dad4b2769fbf1fd4b97f77a4cc646b4 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 18:46:58 -0600
Subject: [PATCH 028/106] Transmogrifier: fetch quoted post
---
.../web/activity_pub/transmogrifier.ex | 17 +++++
test/fixtures/tesla_mock/aimu@misskey.io.json | 64 +++++++++++++++++++
.../tesla_mock/misskey.io_8vs6wxufd0.json | 44 +++++++++++++
.../web/activity_pub/transmogrifier_test.exs | 22 +++++++
test/support/http_request_mock.ex | 18 ++++++
5 files changed, 165 insertions(+)
create mode 100644 test/fixtures/tesla_mock/aimu@misskey.io.json
create mode 100644 test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 0e6c429f9..c466271ca 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -166,6 +166,22 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object
+ def fix_quote(object, options \\ [])
+
+ def fix_quote(%{"quoteUrl" => quote_url} = object, options)
+ when not is_nil(quote_url) do
+ with {:ok, quoted_object} <- get_obj_helper(quote_url, options),
+ %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
+ Map.put(object, "quoteUrl", quoted_object.data["id"])
+ else
+ e ->
+ Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
+ object
+ end
+ end
+
+ def fix_quote(object, _options), do: object
+
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
@@ -454,6 +470,7 @@ def handle_incoming(
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
+ |> fix_quote(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
diff --git a/test/fixtures/tesla_mock/aimu@misskey.io.json b/test/fixtures/tesla_mock/aimu@misskey.io.json
new file mode 100644
index 000000000..9ff4cb6d0
--- /dev/null
+++ b/test/fixtures/tesla_mock/aimu@misskey.io.json
@@ -0,0 +1,64 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey.io/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "type": "Person",
+ "id": "https://misskey.io/users/83ssedkv53",
+ "inbox": "https://misskey.io/users/83ssedkv53/inbox",
+ "outbox": "https://misskey.io/users/83ssedkv53/outbox",
+ "followers": "https://misskey.io/users/83ssedkv53/followers",
+ "following": "https://misskey.io/users/83ssedkv53/following",
+ "sharedInbox": "https://misskey.io/inbox",
+ "endpoints": {
+ "sharedInbox": "https://misskey.io/inbox"
+ },
+ "url": "https://misskey.io/@aimu",
+ "preferredUsername": "aimu",
+ "name": "あいむ",
+ "summary": "わずかな作曲要素 巣穴で独り言 Twitter https://twitter.com/aimu_53 Soundcloud https://soundcloud.com/aimu-53
",
+ "icon": {
+ "type": "Image",
+ "url": "https://s3.arkjp.net/misskey/webpublic-3f7e93c0-34f5-443c-acc0-f415cb2342b4.jpg",
+ "sensitive": false,
+ "name": null
+ },
+ "image": {
+ "type": "Image",
+ "url": "https://s3.arkjp.net/misskey/webpublic-2db63d1d-490b-488b-ab62-c93c285f26b6.png",
+ "sensitive": false,
+ "name": null
+ },
+ "tag": [],
+ "manuallyApprovesFollowers": false,
+ "discoverable": true,
+ "publicKey": {
+ "id": "https://misskey.io/users/83ssedkv53#main-key",
+ "type": "Key",
+ "owner": "https://misskey.io/users/83ssedkv53",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ylhePJ6qGHmwHSBP17b\nIosxGaiFKvgDBgZdm8vzvKeRSqJV9uLHfZL3pO/Zt02EwaZd2GohZAtBZEF8DbMA\n3s93WAesvyGF9mjGrYYKlhp/glwyrrrbf+RdD0DLtyDwRRlrxp3pS2lLmv5Tp1Zl\npH+UKpOnNrpQqjHI5P+lEc9bnflzbRrX+UiyLNsVAP80v4wt7SZfT/telrU6mDru\n998UdfhUo7bDKeDsHG1PfLpyhhtfdoZub4kBpkyacHiwAd+CdCjR54Eu7FDwVK3p\nY3JcrT2q5stgMqN1m4QgSL4XAADIotWwDYttTJejM1n9dr+6VWv5bs0F2Q/6gxOp\nu5DQZLk4Q+64U4LWNox6jCMOq3fYe0g7QalJIHnanYQQo+XjoH6S1Aw64gQ3Ip2Y\nZBmZREAOR7GMFVDPFnVnsbCHnIAv16TdgtLgQBAihkWEUuPqITLi8PMu6kMr3uyq\nYkObEfH0TNTcqaiVpoXv791GZLEUV5ROl0FSUANLNkHZZv29xZ5JDOBOR1rNBLyH\ngVtW8rpszYqOXwzX23hh4WsVXfB7YgNvIijwjiaWbzsecleaENGEnLNMiVKVumTj\nmtyTeFJpH0+OaSrUYpemRRJizmqIjklKsNwUEwUb2WcUUg92o56T2obrBkooabZe\nwgSXSKTOcjsR/ju7+AuIyvkCAwEAAQ==\n-----END PUBLIC KEY-----\n"
+ },
+ "isCat": true,
+ "vcard:bday": "5353-05-03"
+}
diff --git a/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json
new file mode 100644
index 000000000..323ca10ed
--- /dev/null
+++ b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json
@@ -0,0 +1,44 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey.io/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "id": "https://misskey.io/notes/8vs6wxufd0",
+ "type": "Note",
+ "attributedTo": "https://misskey.io/users/83ssedkv53",
+ "summary": null,
+ "content": "Fantiaこれできないように過去のやつは従量課金だった気がする
",
+ "_misskey_content": "Fantiaこれできないように過去のやつは従量課金だった気がする",
+ "published": "2022-01-21T16:37:12.663Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://misskey.io/users/83ssedkv53/followers"
+ ],
+ "inReplyTo": null,
+ "attachment": [],
+ "sensitive": false,
+ "tag": []
+}
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 3e0c8dc65..2c8e5ba21 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -136,6 +136,28 @@ test "it drops link tags" do
tag = object.data["tag"] |> List.first()
assert tag["type"] == "Mention"
end
+
+ test "it accepts quote posts" do
+ insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
+
+ object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Create",
+ "actor" => "https://misskey.io/users/7rkrarq81i",
+ "object" => object
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ # Object was created in the database
+ object = Object.normalize(activity)
+ assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0"
+
+ # It fetched the quoted post
+ assert Object.normalize("https://misskey.io/notes/8vs6wxufd0")
+ end
end
describe "prepare outgoing" do
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index b0cf613ac..78a367024 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1380,6 +1380,15 @@ def get("https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
}}
end
+ def get("https://misskey.io/users/83ssedkv53", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/aimu@misskey.io.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get("https://gleasonator.com/users/macgirvin", _, _, _) do
{:ok,
%Tesla.Env{
@@ -1446,6 +1455,15 @@ def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _,
}}
end
+ def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
From cc4badaf60462fdb8bb57225437e3dd360ee0dfb Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 19:14:39 -0600
Subject: [PATCH 029/106] Transmogrifier: fix quoteUrl here too
---
.../web/activity_pub/transmogrifier.ex | 23 +++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index c466271ca..f5771e75e 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -166,9 +166,9 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object
- def fix_quote(object, options \\ [])
+ def fix_quote_url(object, options \\ [])
- def fix_quote(%{"quoteUrl" => quote_url} = object, options)
+ def fix_quote_url(%{"quoteUrl" => quote_url} = object, options)
when not is_nil(quote_url) do
with {:ok, quoted_object} <- get_obj_helper(quote_url, options),
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
@@ -180,7 +180,22 @@ def fix_quote(%{"quoteUrl" => quote_url} = object, options)
end
end
- def fix_quote(object, _options), do: object
+ # Fix for Fedibird
+ # https://github.com/fedibird/mastodon/issues/9
+ def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do
+ object
+ |> Map.put("quoteUrl", quote_url)
+ |> fix_quote_url(options)
+ end
+
+ # Misskey fallback
+ def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do
+ object
+ |> Map.put("quoteUrl", quote_url)
+ |> fix_quote_url(options)
+ end
+
+ def fix_quote_url(object, _options), do: object
defp prepare_in_reply_to(in_reply_to) do
cond do
@@ -470,7 +485,7 @@ def handle_incoming(
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
- |> fix_quote(fetch_options)
+ |> fix_quote_url(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
From ce5eb3172321f0ef2ae85d7819b44cc8241a5581 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 19:47:08 -0600
Subject: [PATCH 030/106] StatusView: show quoted posts through the API,
probably
---
.../web/mastodon_api/views/status_view.ex | 42 ++++++++++++++++++-
1 file changed, 41 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index dea22f9c2..b966a84d0 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -57,6 +57,27 @@ defp get_replied_to_activities(activities) do
end)
end
+ defp get_quoted_activities([]), do: %{}
+
+ defp get_quoted_activities(activities) do
+ activities
+ |> Enum.map(fn
+ %{data: %{"type" => "Create"}} = activity ->
+ object = Object.normalize(activity, fetch: false)
+ object && object.data["quoteUrl"] != "" && object.data["quoteUrl"]
+
+ _ ->
+ nil
+ end)
+ |> Enum.filter(& &1)
+ |> Activity.create_by_object_ap_id_with_object()
+ |> Repo.all()
+ |> Enum.reduce(%{}, fn activity, acc ->
+ object = Object.normalize(activity, fetch: false)
+ if object, do: Map.put(acc, object.data["id"], activity), else: acc
+ end)
+ end
+
# DEPRECATED This field seems to be a left-over from the StatusNet era.
# If your application uses `pleroma.conversation_id`: this field is deprecated.
# It is currently stubbed instead by doing a CRC32 of the context, and
@@ -97,6 +118,7 @@ def render("index.json", opts) do
# length(activities_with_links) * timeout
fetch_rich_media_for_activities(activities)
replied_to_activities = get_replied_to_activities(activities)
+ quoted_activities = get_quoted_activities(activities)
parent_activities =
activities
@@ -129,6 +151,7 @@ def render("index.json", opts) do
opts =
opts
|> Map.put(:replied_to_activities, replied_to_activities)
+ |> Map.put(:quoted_activities, quoted_activities)
|> Map.put(:parent_activities, parent_activities)
|> Map.put(:relationships, relationships_opt)
@@ -277,7 +300,6 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
end
reply_to = get_reply_to(activity, opts)
-
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
history_len =
@@ -290,6 +312,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
# Here the implicit index of the current content is 0
chrono_order = history_len - 1
+ quote_activity = get_quote(activity, opts)
+
content =
object
|> render_content()
@@ -398,6 +422,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
conversation_id: get_context_id(activity),
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
+ quote_id: quote_activity && to_string(quote_activity.id),
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
@@ -633,6 +658,21 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
end
end
+ def get_quote(activity, %{quoted_activities: quoted_activities}) do
+ object = Object.normalize(activity, fetch: false)
+ quoted_activities[object.data["quoteUrl"]]
+ end
+
+ def get_quote(%{data: %{"object" => _object}} = activity, _) do
+ object = Object.normalize(activity, fetch: false)
+
+ if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do
+ Activity.get_create_by_object_ap_id(object.data["quoteUrl"])
+ else
+ nil
+ end
+ end
+
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
url = object.data["url"] || object.data["id"]
From 0d9c443e51c85d9ded3e20954c9620f7a9d2361e Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 20:05:58 -0600
Subject: [PATCH 031/106] StatusView: render the whole quoted status
---
lib/pleroma/web/api_spec/schemas/status.ex | 5 +++++
lib/pleroma/web/mastodon_api/views/status_view.ex | 10 +++++++++-
.../web/mastodon_api/views/status_view_test.exs | 1 +
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index bc29cf4a6..39241aa39 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -193,6 +193,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
nullable: true,
description: "The `acct` property of User entity for replied user (if any)"
},
+ quote: %Schema{
+ allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
+ nullable: true,
+ description: "Quoted status (if any)"
+ },
local: %Schema{
type: :boolean,
description: "`true` if the post was made on the local instance"
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index b966a84d0..5bde1ce04 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -314,6 +314,14 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
quote_activity = get_quote(activity, opts)
+ quote_post =
+ if quote_activity do
+ quote_rendering_opts = Map.put(opts, :activity, quote_activity)
+ render("show.json", quote_rendering_opts)
+ else
+ nil
+ end
+
content =
object
|> render_content()
@@ -422,7 +430,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
conversation_id: get_context_id(activity),
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
- quote_id: quote_activity && to_string(quote_activity.id),
+ quote: quote_post,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index b93335190..b10b0f0b9 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -326,6 +326,7 @@ test "a note activity" do
conversation_id: convo_id,
context: object_data["context"],
in_reply_to_account_acct: nil,
+ quote: nil,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
From 6ac19c3999c543e5a26bbf04932a6a7aaa447b99 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 21:27:05 -0600
Subject: [PATCH 032/106] ActivityDraft: create quote posts
---
lib/pleroma/web/common_api/activity_draft.ex | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 63ed48a27..375aabc91 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -22,6 +22,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
+ quote_post: nil,
visibility: nil,
expires_at: nil,
extra: nil,
@@ -53,6 +54,7 @@ def create(user, params) do
|> poll()
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
+ |> with_valid("e_post/1)
|> with_valid(&visibility/1)
|> content()
|> with_valid(&to_and_cc/1)
@@ -132,6 +134,18 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}}
defp in_reply_to(draft), do: draft
+ defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft
+
+ defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do
+ %__MODULE__{draft | quote_post: Activity.get_by_id(id)}
+ end
+
+ defp quote_post(%{params: %{quote_id: %Activity{} = quote_post}} = draft) do
+ %__MODULE__{draft | quote_post: quote_post}
+ end
+
+ defp quote_post(draft), do: draft
+
defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
From d4fea8b5595e9e6cd37bdb1cee21285f905693f1 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 22:15:54 -0600
Subject: [PATCH 033/106] ActivityDraft: allow quoting
---
lib/pleroma/web/activity_pub/builder.ex | 11 +++++++++++
.../web/api_spec/operations/status_operation.ex | 7 ++++++-
test/pleroma/web/common_api_test.exs | 12 ++++++++++++
3 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 8eab3a241..eb0bb0e33 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -217,6 +217,7 @@ def note(%ActivityDraft{} = draft) do
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
}
|> add_in_reply_to(draft.in_reply_to)
+ |> add_quote(draft.quote_post)
|> Map.merge(draft.extra)
{:ok, data, []}
@@ -232,6 +233,16 @@ defp add_in_reply_to(object, in_reply_to) do
end
end
+ defp add_quote(object, nil), do: object
+
+ defp add_quote(object, quote_post) do
+ with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
+ Map.put(object, "quoteUrl", quote_object.data["id"])
+ else
+ _ -> object
+ end
+ end
+
def chat_message(actor, recipient, content, opts \\ []) do
basic = %{
"id" => Utils.generate_object_id(),
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 5d6e82f3c..8fa3b0890 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -581,7 +581,12 @@ defp create_request do
type: :string,
description:
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
- }
+ },
+ quote_id: %Schema{
+ nullable: true,
+ allOf: [FlakeID],
+ description: "ID of the status being quoted, if any"
+ },
},
example: %{
"status" => "What time is it?",
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 0d76d6581..09df27acb 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -796,6 +796,18 @@ test "it can handle activities that expire" do
scheduled_at: expires_at
)
end
+
+ test "it allows allows quote posting" do
+ user = insert(:user)
+
+ {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id})
+
+ quoted = Object.normalize(quoted)
+ quote_post = Object.normalize(quote_post)
+
+ assert quote_post.data["quoteUrl"] == quoted.data["id"]
+ end
end
describe "reactions" do
From c20e90e898affc9d00d8b7d6b71f11157f5c7837 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 22:29:13 -0600
Subject: [PATCH 034/106] BuilderTest: build quote post
---
.../pleroma/web/activity_pub/builder_test.exs | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/test/pleroma/web/activity_pub/builder_test.exs b/test/pleroma/web/activity_pub/builder_test.exs
index eb175a1be..52058a0a3 100644
--- a/test/pleroma/web/activity_pub/builder_test.exs
+++ b/test/pleroma/web/activity_pub/builder_test.exs
@@ -44,5 +44,34 @@ test "returns note data" do
assert {:ok, ^expected, []} = Builder.note(draft)
end
+
+ test "quote post" do
+ user = insert(:user)
+ note = insert(:note)
+
+ draft = %ActivityDraft{
+ user: user,
+ context: "2hu",
+ content_html: "This is :moominmamma: note ",
+ quote_post: note,
+ extra: %{}
+ }
+
+ expected = %{
+ "actor" => user.ap_id,
+ "attachment" => [],
+ "content" => "This is :moominmamma: note ",
+ "context" => "2hu",
+ "sensitive" => false,
+ "type" => "Note",
+ "quoteUrl" => note.data["id"],
+ "cc" => [],
+ "summary" => nil,
+ "tag" => [],
+ "to" => []
+ }
+
+ assert {:ok, ^expected, []} = Builder.note(draft)
+ end
end
end
From 3a8b5d90df6de502debaee4d670211bcf64ad1db Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 22:35:08 -0600
Subject: [PATCH 035/106] StatusControllerTest: test creating a quote post
---
.../controllers/status_controller_test.exs | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
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 76c289ee7..ba49256b5 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -125,6 +125,25 @@ test "posting a status", %{conn: conn} do
)
end
+ test "posting a quote post", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, %{id: activity_id}} = CommonAPI.post(user, %{status: "yolo"})
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "indeed",
+ "quote_id" => activity_id
+ })
+
+ assert %{"id" => id, "pleroma" => %{"quote" => %{"id" => ^activity_id}}} =
+ json_response_and_validate_schema(conn, 200)
+
+ assert Activity.get_by_id(id)
+ end
+
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
From cbd1760efac872c00edad15f352ffe4d2e0e1e12 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 22:41:57 -0600
Subject: [PATCH 036/106] TransmogrifierTest: prepare an outgoing quote post
---
.../pleroma/web/activity_pub/transmogrifier_test.exs | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 2c8e5ba21..824398e38 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -372,6 +372,18 @@ test "Updates of Notes are handled" do
}
} = prepared["object"]
end
+
+ test "it prepares a quote post" do
+ user = insert(:user)
+
+ {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"})
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quote_id: quoted_post.id})
+
+ {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data)
+
+ quoted_post = Object.normalize(quoted_post)
+ assert modified["object"]["quoteUrl"] == quoted_post.data["id"]
+ end
end
describe "actor rewriting" do
From 96009739173e5e48a636bb964855eb7aea11c828 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 22:57:42 -0600
Subject: [PATCH 037/106] mix format
---
lib/pleroma/web/api_spec/operations/status_operation.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 8fa3b0890..c133a3aac 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -586,7 +586,7 @@ defp create_request do
nullable: true,
allOf: [FlakeID],
description: "ID of the status being quoted, if any"
- },
+ }
},
example: %{
"status" => "What time is it?",
From f4ccdfd5033e7b1136ae0fe4e41dba78d83e80cf Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 23:02:44 -0600
Subject: [PATCH 038/106] Fix typos
---
.../article_note_page_validator_test.exs | 12 ++++++------
test/pleroma/web/common_api_test.exs | 2 +-
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c3cde00b5..dec2e28c9 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -121,19 +121,19 @@ test "Fedibird quote post" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
- chg = ArticleNotePageValidator.cast_and_validate(data)
+ cng = ArticleNotePageValidator.cast_and_validate(data)
- assert chg.valid?
- assert chg.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
end
test "Misskey quote post" do
insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
- chg = ArticleNotePageValidator.cast_and_validate(data)
+ cng = ArticleNotePageValidator.cast_and_validate(data)
- assert chg.valid?
- assert chg.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 09df27acb..734e6dd82 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -797,7 +797,7 @@ test "it can handle activities that expire" do
)
end
- test "it allows allows quote posting" do
+ test "it allows quote posting" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
From 5716f88a1d8424cf7c62a0491b3bf9607dc9aa3f Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 23:09:33 -0600
Subject: [PATCH 039/106] InstanceView: add "quote_posting" feature
---
lib/pleroma/web/mastodon_api/views/instance_view.ex | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index efd2a0af6..1b01d7371 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -69,6 +69,7 @@ def features do
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"editing",
+ "quote_posting",
if Config.get([:activitypub, :blockers_visible]) do
"blockers_visible"
end,
From db46abce477b83b252e0ad87f74ac9266e9cb118 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sat, 22 Jan 2022 23:29:55 -0600
Subject: [PATCH 040/106] @context: add quoteUrl
---
priv/static/schemas/litepub-0.1.jsonld | 1 +
1 file changed, 1 insertion(+)
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 650118475..5d8244a11 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -26,6 +26,7 @@
"@id": "litepub:listMessage",
"@type": "@id"
},
+ "quoteUrl": "as:quoteUrl",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
From 80ab2572a4d5590d738cc763a87156b3f79362fb Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sun, 23 Jan 2022 13:55:25 -0600
Subject: [PATCH 041/106] Return quote_url through the API, don't render quotes
more than 1 level deep
---
lib/pleroma/web/api_spec/schemas/status.ex | 6 +++++
.../web/mastodon_api/views/status_view.ex | 7 +++++-
.../controllers/status_controller_test.exs | 9 +++++---
.../mastodon_api/views/status_view_test.exs | 23 +++++++++++++++++++
4 files changed, 41 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 39241aa39..f4ee9b38c 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -198,6 +198,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
nullable: true,
description: "Quoted status (if any)"
},
+ quote_url: %Schema{
+ type: :string,
+ format: :uri,
+ nullable: true,
+ description: "URL of the quoted status"
+ },
local: %Schema{
type: :boolean,
description: "`true` if the post was made on the local instance"
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 5bde1ce04..06adfb221 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -316,7 +316,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
quote_post =
if quote_activity do
- quote_rendering_opts = Map.put(opts, :activity, quote_activity)
+ quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
render("show.json", quote_rendering_opts)
else
nil
@@ -431,6 +431,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
quote: quote_post,
+ quote_url: object.data["quoteUrl"],
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
@@ -666,6 +667,10 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
end
end
+ def get_quote(_activity, %{show_quote: false}) do
+ nil
+ end
+
def get_quote(activity, %{quoted_activities: quoted_activities}) do
object = Object.normalize(activity, fetch: false)
quoted_activities[object.data["quoteUrl"]]
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 ba49256b5..de3b52e26 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -128,7 +128,8 @@ test "posting a status", %{conn: conn} do
test "posting a quote post", %{conn: conn} do
user = insert(:user)
- {:ok, %{id: activity_id}} = CommonAPI.post(user, %{status: "yolo"})
+ {:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"})
+ %{data: %{"id" => quote_url}} = Object.normalize(activity)
conn =
conn
@@ -138,8 +139,10 @@ test "posting a quote post", %{conn: conn} do
"quote_id" => activity_id
})
- assert %{"id" => id, "pleroma" => %{"quote" => %{"id" => ^activity_id}}} =
- json_response_and_validate_schema(conn, 200)
+ assert %{
+ "id" => id,
+ "pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url}
+ } = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index b10b0f0b9..f50b02799 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -327,6 +327,7 @@ test "a note activity" do
context: object_data["context"],
in_reply_to_account_acct: nil,
quote: nil,
+ quote_url: nil,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
@@ -423,6 +424,28 @@ test "a reply" do
assert status.in_reply_to_id == to_string(note.id)
end
+ test "a quote post" do
+ post = insert(:note_activity)
+ user = insert(:user)
+
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id})
+ {:ok, quoted_quote_post} = CommonAPI.post(user, %{status: "yo", quote_id: quote_post.id})
+
+ status = StatusView.render("show.json", %{activity: quoted_quote_post})
+
+ assert status.pleroma.quote.id == to_string(quote_post.id)
+ assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"]
+
+ # Quotes don't go more than one level deep
+ refute status.pleroma.quote.pleroma.quote
+ assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"]
+
+ # In an index
+ [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity})
+
+ assert status.pleroma.quote.id == to_string(quote_post.id)
+ end
+
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
From 54a989793878c63900d2c6de7b4ffc8fbd8fe8c6 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sun, 23 Jan 2022 15:46:44 -0600
Subject: [PATCH 042/106] ActivityDraft: mention the OP of a quoted post
---
lib/pleroma/web/common_api/activity_draft.ex | 19 +++++++++++--------
test/pleroma/web/common_api_test.exs | 12 ++++++++++++
2 files changed, 23 insertions(+), 8 deletions(-)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 375aabc91..588aba55e 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -137,11 +137,11 @@ defp in_reply_to(draft), do: draft
defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft
defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do
- %__MODULE__{draft | quote_post: Activity.get_by_id(id)}
- end
-
- defp quote_post(%{params: %{quote_id: %Activity{} = quote_post}} = draft) do
- %__MODULE__{draft | quote_post: quote_post}
+ with %Activity{actor: actor_ap_id} = activity <- Activity.get_by_id(id) do
+ %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]}
+ else
+ _ -> draft
+ end
end
defp quote_post(draft), do: draft
@@ -178,12 +178,15 @@ defp poll(draft) do
end
end
- defp content(draft) do
+ defp content(%{mentions: mentions} = draft) do
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
+ mentioned_ap_ids =
+ Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end)
+
mentions =
- mentioned_users
- |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
+ mentions
+ |> Kernel.++(mentioned_ap_ids)
|> Utils.get_addressed_users(draft.params[:to])
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 734e6dd82..051e770d7 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -807,6 +807,18 @@ test "it allows quote posting" do
quote_post = Object.normalize(quote_post)
assert quote_post.data["quoteUrl"] == quoted.data["id"]
+
+ # The OP is mentioned
+ assert quoted.data["actor"] in quote_post.data["to"]
+ end
+
+ test "quote posting with explicit addressing doesn't mention the OP" do
+ user = insert(:user)
+
+ {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []})
+
+ assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()]
end
end
From 1f19dd76f66ca657ddfe79a51e59b8997a4c6321 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sun, 23 Jan 2022 16:03:46 -0600
Subject: [PATCH 043/106] ActivityDraft: mix format, defensive actor ID
---
lib/pleroma/web/common_api/activity_draft.ex | 16 ++++++++++------
test/pleroma/web/common_api_test.exs | 4 +++-
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 588aba55e..95534f3cb 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Web.CommonAPI.Utils
import Pleroma.Web.Gettext
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
defstruct valid?: true,
errors: [],
@@ -134,13 +135,16 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}}
defp in_reply_to(draft), do: draft
- defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft
+ defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do
+ case Activity.get_by_id(id) do
+ %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) ->
+ %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]}
- defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do
- with %Activity{actor: actor_ap_id} = activity <- Activity.get_by_id(id) do
- %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]}
- else
- _ -> draft
+ %Activity{} = activity ->
+ %__MODULE__{draft | quote_post: activity}
+
+ _ ->
+ draft
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 051e770d7..960d0cf16 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -816,7 +816,9 @@ test "quote posting with explicit addressing doesn't mention the OP" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
- {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []})
+
+ {:ok, quote_post} =
+ CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []})
assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()]
end
From 36a5578d2b16ba9f771fff55daafa85ec606a6be Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Mon, 24 Jan 2022 15:34:23 -0600
Subject: [PATCH 044/106] Scrubber.Default: allow span.quote-inline for quote
post compatibility
---
priv/scrubbers/default.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index d1215d2e0..56324a9fa 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -60,7 +60,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:u, ["lang"])
Meta.allow_tag_with_these_attributes(:ul, ["lang"])
- Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline"])
+ Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline", "quote-inline"])
Meta.allow_tag_with_these_attributes(:span, ["lang"])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
From 57ef1d121101d785c043ef6aaf2d33bb9be3ec3b Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Mon, 24 Jan 2022 16:44:35 -0600
Subject: [PATCH 045/106] Add InlineQuotePolicy to force quote URLs inline
---
config/config.exs | 2 +
docs/configuration/cheatsheet.md | 4 ++
.../activity_pub/mrf/inline_quote_policy.ex | 53 ++++++++++++++++++
.../mrf/inline_quote_policy_test.exs | 56 +++++++++++++++++++
4 files changed, 115 insertions(+)
create mode 100644 lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
create mode 100644 test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
diff --git a/config/config.exs b/config/config.exs
index ebcbf8b49..56cc34db5 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -434,6 +434,8 @@
config :pleroma, :mrf_follow_bot, follower_nickname: nil
+config :pleroma, :mrf_inline_quote, prefix: "RT"
+
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index f43cde114..32cc5811a 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -160,6 +160,7 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
+ * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
@@ -267,6 +268,9 @@ Notes:
* `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
* `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
+#### :mrf_inline_quote
+* `prefix`: Prefix before the link (default: `RT`)
+
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
new file mode 100644
index 000000000..0f1dc9f42
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
+ @moduledoc "Force a quote line into the message content."
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ defp build_inline_quote(prefix, url) do
+ " #{prefix}: #{url} "
+ end
+
+ defp filter_object(%{"quoteUrl" => quote_url} = object) do
+ content = object["content"] || ""
+
+ if content =~ quote_url do
+ object
+ else
+ prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
+ content = content <> build_inline_quote(prefix, quote_url)
+ Map.put(object, "content", content)
+ end
+ end
+
+ @impl true
+ def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
+ {:ok, Map.put(activity, "object", filter_object(object))}
+ end
+
+ @impl true
+ def filter(object), do: {:ok, object}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_inline_quote,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
+ label: "MRF Inline Quote",
+ description: "Force quote post URLs inline",
+ children: [
+ %{
+ key: :prefix,
+ type: :string,
+ description: "Prefix before the link",
+ suggestions: ["RT", "QT", "RE", "RN"]
+ }
+ ]
+ }
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
new file mode 100644
index 000000000..81dc06dda
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -0,0 +1,56 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do
+ alias Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy
+ use Pleroma.DataCase
+
+ test "adds quote URL to post content" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post
",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "Nice post
RT: https://gleasonator.com/objects/1234 "
+ end
+
+ test "ignores Misskey quote posts" do
+ object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://misskey.io/users/7rkrarq81i",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
+
+ test "ignores Fedibird quote posts" do
+ object = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
+
+ # Normally the ObjectValidator will fix this before it reaches MRF
+ object = Map.put(object, "quoteUrl", object["quoteURL"])
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://fedibird.com/users/noellabo",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
+end
From 59326247aa754991add9170e204257a8bf94c40f Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Wed, 26 Jan 2022 11:21:49 -0600
Subject: [PATCH 046/106] CommonAPI: disallow quoting private posts through the
API
---
lib/pleroma/web/common_api/activity_draft.ex | 15 ++++++++++-
.../web/common_api/activity_draft_test.exs | 26 +++++++++++++++++++
test/pleroma/web/common_api_test.exs | 14 ++++++++++
3 files changed, 54 insertions(+), 1 deletion(-)
create mode 100644 test/pleroma/web/common_api/activity_draft_test.exs
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 95534f3cb..d4875765c 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
@@ -57,6 +58,7 @@ def create(user, params) do
|> with_valid(&in_reply_to_conversation/1)
|> with_valid("e_post/1)
|> with_valid(&visibility/1)
+ |> with_valid("ing_visibility/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
@@ -136,7 +138,7 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}}
defp in_reply_to(draft), do: draft
defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do
- case Activity.get_by_id(id) do
+ case Activity.get_by_id_with_object(id) do
%Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) ->
%__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]}
@@ -165,6 +167,17 @@ defp visibility(%{params: params} = draft) do
end
end
+ defp quoting_visibility(%{quote_post: %Activity{}} = draft) do
+ with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
+ visibility when visibility in ~w(public unlisted) <- Visibility.get_visibility(object) do
+ draft
+ else
+ _ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
+ end
+ end
+
+ defp quoting_visibility(draft), do: draft
+
defp expires_at(draft) do
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs
new file mode 100644
index 000000000..8a09fc710
--- /dev/null
+++ b/test/pleroma/web/common_api/activity_draft_test.exs
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.ActivityDraft
+
+ import Pleroma.Factory
+
+ test "create/2 with a quote post" do
+ user = insert(:user)
+
+ {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
+ {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
+ {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
+ {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
+
+ {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id})
+ {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id})
+ end
+end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 960d0cf16..c4eba8b9c 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -822,6 +822,20 @@ test "quote posting with explicit addressing doesn't mention the OP" do
assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()]
end
+
+ test "quote posting visibility" do
+ user = insert(:user)
+
+ {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
+ {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
+ {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
+ {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
+
+ {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id})
+ {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id})
+ end
end
describe "reactions" do
From 6f11f11519f9c735f6b059c250f4bf01e09b305f Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Wed, 26 Jan 2022 11:49:31 -0600
Subject: [PATCH 047/106] StatusView: fix quote visibility
---
.../web/mastodon_api/views/status_view.ex | 2 +-
.../mastodon_api/views/status_view_test.exs | 41 +++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 06adfb221..7360d1093 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -315,7 +315,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
quote_activity = get_quote(activity, opts)
quote_post =
- if quote_activity do
+ if visible_for_user?(quote_activity, opts[:for]) do
quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
render("show.json", quote_rendering_opts)
else
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index f50b02799..f41ef580d 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -446,6 +446,47 @@ test "a quote post" do
assert status.pleroma.quote.id == to_string(quote_post.id)
end
+ test "quoted private post" do
+ user = insert(:user)
+
+ # Insert a private post
+ private = insert(:followers_only_note_activity, user: user)
+ private_object = Object.normalize(private)
+
+ # Create a public post quoting the private post
+ quote_private =
+ insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => private_object.data["id"]}))
+
+ status = StatusView.render("show.json", %{activity: quote_private})
+
+ # The quote isn't rendered
+ refute status.pleroma.quote
+ assert status.pleroma.quote_url == private_object.data["id"]
+
+ # After following the user, the quote is rendered
+ follower = insert(:user)
+ CommonAPI.follow(follower, user)
+
+ status = StatusView.render("show.json", %{activity: quote_private, for: follower})
+ assert status.pleroma.quote.id == to_string(private.id)
+ end
+
+ test "quoted direct message" do
+ # Insert a direct message
+ direct = insert(:direct_note_activity)
+ direct_object = Object.normalize(direct)
+
+ # Create a public post quoting the direct message
+ quote_direct =
+ insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => direct_object.data["id"]}))
+
+ status = StatusView.render("show.json", %{activity: quote_direct})
+
+ # The quote isn't rendered
+ refute status.pleroma.quote
+ assert status.pleroma.quote_url == direct_object.data["id"]
+ end
+
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
From 74e0a4555f583a6962ad116bf6e54f06e42fe465 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Wed, 26 Jan 2022 11:52:50 -0600
Subject: [PATCH 048/106] StatusView: add `quote_visible` param
---
lib/pleroma/web/api_spec/schemas/status.ex | 4 ++++
lib/pleroma/web/mastodon_api/views/status_view.ex | 1 +
test/pleroma/web/mastodon_api/views/status_view_test.exs | 4 ++++
3 files changed, 9 insertions(+)
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index f4ee9b38c..5d0eedb08 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -204,6 +204,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
nullable: true,
description: "URL of the quoted status"
},
+ quote_visible: %Schema{
+ type: :boolean,
+ description: "`true` if the quoted post is visible to the user"
+ },
local: %Schema{
type: :boolean,
description: "`true` if the post was made on the local instance"
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 7360d1093..2aa44b0f6 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -432,6 +432,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
quote: quote_post,
quote_url: object.data["quoteUrl"],
+ quote_visible: visible_for_user?(quote_activity, opts[:for]),
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index f41ef580d..ed0a87558 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -328,6 +328,7 @@ test "a note activity" do
in_reply_to_account_acct: nil,
quote: nil,
quote_url: nil,
+ quote_visible: false,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
@@ -462,6 +463,7 @@ test "quoted private post" do
# The quote isn't rendered
refute status.pleroma.quote
assert status.pleroma.quote_url == private_object.data["id"]
+ refute status.pleroma.quote_visible
# After following the user, the quote is rendered
follower = insert(:user)
@@ -469,6 +471,7 @@ test "quoted private post" do
status = StatusView.render("show.json", %{activity: quote_private, for: follower})
assert status.pleroma.quote.id == to_string(private.id)
+ assert status.pleroma.quote_visible
end
test "quoted direct message" do
@@ -485,6 +488,7 @@ test "quoted direct message" do
# The quote isn't rendered
refute status.pleroma.quote
assert status.pleroma.quote_url == direct_object.data["id"]
+ refute status.pleroma.quote_visible
end
test "contains mentions" do
From bee7e419597615ac6852942fe563166feba3fe73 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Thu, 27 Jan 2022 14:28:06 -0600
Subject: [PATCH 049/106] InlineQuotePolicy: don't add line breaks to markdown
posts
---
.../activity_pub/mrf/inline_quote_policy.ex | 12 ++++++++---
.../mrf/inline_quote_policy_test.exs | 21 ++++++++++++++++++-
2 files changed, 29 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index 0f1dc9f42..46013fc5e 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
@moduledoc "Force a quote line into the message content."
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
- defp build_inline_quote(prefix, url) do
- " #{prefix}: #{url} "
+ defp build_inline_quote(prefix, url, br) do
+ "#{String.duplicate(" ", br)}#{prefix}: #{url} "
end
defp filter_object(%{"quoteUrl" => quote_url} = object) do
@@ -17,7 +17,13 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do
object
else
prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
- content = content <> build_inline_quote(prefix, quote_url)
+
+ inline_quote =
+ if String.ends_with?(content, "
"),
+ do: build_inline_quote(prefix, quote_url, 0),
+ else: build_inline_quote(prefix, quote_url, 2)
+
+ content = content <> inline_quote
Map.put(object, "content", content)
end
end
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
index 81dc06dda..8e75aaaab 100644
--- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -9,6 +9,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do
test "adds quote URL to post content" do
quote_url = "https://gleasonator.com/objects/1234"
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "Nice post RT: https://gleasonator.com/objects/1234 "
+ end
+
+ test "doesn't add line breaks to markdown posts" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
activity = %{
"type" => "Create",
"actor" => "https://gleasonator.com/users/alex",
@@ -22,7 +41,7 @@ test "adds quote URL to post content" do
{:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
assert filtered ==
- "Nice post
RT: https://gleasonator.com/objects/1234 "
+ "Nice post
RT: https://gleasonator.com/objects/1234 "
end
test "ignores Misskey quote posts" do
From 93e4972b50f9bb0a07a7074fbab2aedbdc0cc4eb Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Thu, 27 Jan 2022 15:01:20 -0600
Subject: [PATCH 050/106] Add InlineQuotePolicy as a default MRF
---
config/config.exs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/config/config.exs b/config/config.exs
index 56cc34db5..9149e925a 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -860,7 +860,11 @@
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
config :pleroma, :mrf,
- policies: [Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy],
+ policies: [
+ Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy,
+ Pleroma.Web.ActivityPub.MRF.TagPolicy,
+ Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy
+ ],
transparency: true,
transparency_exclusions: []
From cf8e4258830e3f362ab1e54238d622f6b2056502 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Fri, 28 Jan 2022 12:33:07 -0600
Subject: [PATCH 051/106] StatusView: return quote post inside a reblog
---
lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++++++----
.../web/mastodon_api/views/status_view_test.exs | 12 ++++++++++++
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 2aa44b0f6..ba4a8f3eb 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -668,13 +668,15 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
end
end
- def get_quote(_activity, %{show_quote: false}) do
- nil
- end
+ def get_quote(_activity, %{show_quote: false}), do: nil
def get_quote(activity, %{quoted_activities: quoted_activities}) do
object = Object.normalize(activity, fetch: false)
- quoted_activities[object.data["quoteUrl"]]
+
+ with nil <- quoted_activities[object.data["quoteUrl"]] do
+ # For when a quote post is inside an Announce
+ Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"])
+ end
end
def get_quote(%{data: %{"object" => _object}} = activity, _) do
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index ed0a87558..6d3a72970 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -491,6 +491,18 @@ test "quoted direct message" do
refute status.pleroma.quote_visible
end
+ test "repost of quote post" do
+ post = insert(:note_activity)
+ user = insert(:user)
+
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id})
+ {:ok, repost} = CommonAPI.repeat(quote_post.id, user)
+
+ [status] = StatusView.render("index.json", %{activities: [repost], as: :activity})
+
+ assert status.reblog.pleroma.quote.id == to_string(post.id)
+ end
+
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
From 3c8319fe9f7a7c793f8fdc347be2015190981e33 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Fri, 28 Jan 2022 14:06:32 -0600
Subject: [PATCH 052/106] Transmogrifier: federate quotes with _misskey_quote
field
---
lib/pleroma/web/activity_pub/transmogrifier.ex | 9 +++++++++
priv/static/schemas/litepub-0.1.jsonld | 2 ++
test/pleroma/web/activity_pub/transmogrifier_test.exs | 4 ++++
3 files changed, 15 insertions(+)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index f5771e75e..163ae54fa 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -660,6 +660,14 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
def set_reply_to_uri(obj), do: obj
+ # Misskey quotes
+ # Despite being underscored, it's potentially more reliable for interop.
+ def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
+ Map.put(object, "_misskey_quote", quote_url)
+ end
+
+ def set_quote_url(obj), do: obj
+
@doc """
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
Based on Mastodon's ActivityPub::NoteSerializer#replies.
@@ -714,6 +722,7 @@ def prepare_object(object) do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
+ |> set_quote_url
|> set_replies
|> strip_internal_fields
|> strip_internal_tags
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 5d8244a11..8559e744d 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -17,6 +17,7 @@
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
+ "misskey": "https://misskey-hub.net/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
@@ -27,6 +28,7 @@
"@type": "@id"
},
"quoteUrl": "as:quoteUrl",
+ "_misskey_quote": "misskey:_misskey_quote",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 824398e38..8c7e0a4c9 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -382,7 +382,11 @@ test "it prepares a quote post" do
{:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data)
quoted_post = Object.normalize(quoted_post)
+
assert modified["object"]["quoteUrl"] == quoted_post.data["id"]
+
+ # Add Misskey's quote as a fallback
+ assert modified["object"]["_misskey_quote"] == quoted_post.data["id"]
end
end
From 817e308c0d9e42dfc39742c554e4c7a8da6f1c50 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Fri, 28 Jan 2022 15:55:52 -0600
Subject: [PATCH 053/106] Handle Fedibird's new quoteUri field
---
.../article_note_page_validator.ex | 8 ++-
.../web/activity_pub/transmogrifier.ex | 19 +++++--
priv/static/schemas/litepub-0.1.jsonld | 2 +
.../quote_post/fedibird_quote_uri.json | 54 +++++++++++++++++++
.../article_note_page_validator_test.exs | 10 ++++
.../web/activity_pub/transmogrifier_test.exs | 9 ++--
6 files changed, 92 insertions(+), 10 deletions(-)
create mode 100644 test/fixtures/quote_post/fedibird_quote_uri.json
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index 40bb67934..0b435b251 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -78,7 +78,13 @@ def fix_attachments(data), do: data
defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
- # Fix for Fedibird
+ # Fedibird
+ # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
+ defp fix_quote_url(%{"quoteUri" => quote_url} = data) do
+ Map.put(data, "quoteUrl", quote_url)
+ end
+
+ # Old Fedibird (bug)
# https://github.com/fedibird/mastodon/issues/9
defp fix_quote_url(%{"quoteURL" => quote_url} = data) do
Map.put(data, "quoteUrl", quote_url)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 163ae54fa..01e135fc1 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -180,7 +180,15 @@ def fix_quote_url(%{"quoteUrl" => quote_url} = object, options)
end
end
- # Fix for Fedibird
+ # Fedibird
+ # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
+ def fix_quote_url(%{"quoteUri" => quote_url} = object, options) do
+ object
+ |> Map.put("quoteUrl", quote_url)
+ |> fix_quote_url(options)
+ end
+
+ # Old Fedibird (bug)
# https://github.com/fedibird/mastodon/issues/9
def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do
object
@@ -660,10 +668,13 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
def set_reply_to_uri(obj), do: obj
- # Misskey quotes
- # Despite being underscored, it's potentially more reliable for interop.
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
- Map.put(object, "_misskey_quote", quote_url)
+ Map.merge(object, %{
+ # Fedibird quote
+ "quoteUri" => quote_url,
+ # Misskey quote
+ "_misskey_quote" => quote_url
+ })
end
def set_quote_url(obj), do: obj
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 8559e744d..3d68e0714 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -18,6 +18,7 @@
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
"misskey": "https://misskey-hub.net/ns#",
+ "fedibird": "http://fedibird.com/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
@@ -28,6 +29,7 @@
"@type": "@id"
},
"quoteUrl": "as:quoteUrl",
+ "quoteUri": "fedibird:quoteUri",
"_misskey_quote": "misskey:_misskey_quote",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
diff --git a/test/fixtures/quote_post/fedibird_quote_uri.json b/test/fixtures/quote_post/fedibird_quote_uri.json
new file mode 100644
index 000000000..7c328fdb9
--- /dev/null
+++ b/test/fixtures/quote_post/fedibird_quote_uri.json
@@ -0,0 +1,54 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "fedibird": "http://fedibird.com/ns#",
+ "quoteUri": "fedibird:quoteUri",
+ "expiry": "fedibird:expiry"
+ }
+ ],
+ "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-01-28T09:17:30Z",
+ "url": "https://fedibird.com/@noellabo/107699335988346142",
+ "attributedTo": "https://fedibird.com/users/noellabo",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://fedibird.com/users/noellabo/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/noellabo/statuses/107699335988346142",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-01-28:objectId=107699335988345290:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/107699335988345290",
+ "quoteUri": "https://fedibird.com/users/yamako/statuses/107699333438289729",
+ "_misskey_quote": "https://fedibird.com/users/yamako/statuses/107699333438289729",
+ "_misskey_content": "美味しそう",
+ "content": "美味しそう QT: https:// fedibird.com/@yamako/107699333 438289729
",
+ "contentMap": {
+ "ja": "美味しそう QT: https:// fedibird.com/@yamako/107699333 438289729
"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index dec2e28c9..a4ba38e6a 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -127,6 +127,16 @@ test "Fedibird quote post" do
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
end
+ test "Fedibird quote post with quoteUri field" do
+ insert(:user, ap_id: "https://fedibird.com/users/noellabo")
+
+ data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!()
+ cng = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729"
+ end
+
test "Misskey quote post" do
insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 8c7e0a4c9..1838f96bb 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -381,12 +381,11 @@ test "it prepares a quote post" do
{:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data)
- quoted_post = Object.normalize(quoted_post)
+ %{data: %{"id" => quote_id}} = Object.normalize(quoted_post)
- assert modified["object"]["quoteUrl"] == quoted_post.data["id"]
-
- # Add Misskey's quote as a fallback
- assert modified["object"]["_misskey_quote"] == quoted_post.data["id"]
+ assert modified["object"]["quoteUrl"] == quote_id
+ assert modified["object"]["quoteUri"] == quote_id
+ assert modified["object"]["_misskey_quote"] == quote_id
end
end
From 4075eecca0033e4487fa9d5d5bb75384597c3e79 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Fri, 28 Jan 2022 16:07:17 -0600
Subject: [PATCH 054/106] InlineQuotePolicy: improve the way Markdown quotes
are displayed by other software
---
.../web/activity_pub/mrf/inline_quote_policy.ex | 13 +++++++------
.../activity_pub/mrf/inline_quote_policy_test.exs | 4 ++--
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index 46013fc5e..7de4935f2 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
@moduledoc "Force a quote line into the message content."
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
- defp build_inline_quote(prefix, url, br) do
- "#{String.duplicate(" ", br)}#{prefix}: #{url} "
+ defp build_inline_quote(prefix, url) do
+ " #{prefix}: #{url} "
end
defp filter_object(%{"quoteUrl" => quote_url} = object) do
@@ -18,12 +18,13 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do
else
prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
- inline_quote =
+ content =
if String.ends_with?(content, ""),
- do: build_inline_quote(prefix, quote_url, 0),
- else: build_inline_quote(prefix, quote_url, 2)
+ do:
+ String.trim_trailing(content, "") <>
+ build_inline_quote(prefix, quote_url) <> "",
+ else: content <> build_inline_quote(prefix, quote_url)
- content = content <> inline_quote
Map.put(object, "content", content)
end
end
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
index 8e75aaaab..2291c1dac 100644
--- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -22,7 +22,7 @@ test "adds quote URL to post content" do
{:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
assert filtered ==
- "Nice post RT: https://gleasonator.com/objects/1234 "
+ "Nice post RT: https://gleasonator.com/objects/1234 "
end
test "doesn't add line breaks to markdown posts" do
@@ -41,7 +41,7 @@ test "doesn't add line breaks to markdown posts" do
{:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
assert filtered ==
- "Nice post
RT: https://gleasonator.com/objects/1234 "
+ "Nice post RT: https://gleasonator.com/objects/1234
"
end
test "ignores Misskey quote posts" do
From 79fca39faf6d084eabb6be44a2263431943b8dd4 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Fri, 28 Jan 2022 17:53:19 -0600
Subject: [PATCH 055/106] Actually, don't send _misskey_quote anymore
---
lib/pleroma/web/activity_pub/transmogrifier.ex | 11 +++++------
priv/static/schemas/litepub-0.1.jsonld | 2 --
test/pleroma/web/activity_pub/transmogrifier_test.exs | 1 -
3 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 01e135fc1..6c6cd712b 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -668,13 +668,12 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
def set_reply_to_uri(obj), do: obj
+ @doc """
+ Fedibird compatibility
+ https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
+ """
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
- Map.merge(object, %{
- # Fedibird quote
- "quoteUri" => quote_url,
- # Misskey quote
- "_misskey_quote" => quote_url
- })
+ Map.put(object, "quoteUri", quote_url)
end
def set_quote_url(obj), do: obj
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 3d68e0714..b499a96f5 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -17,7 +17,6 @@
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
- "misskey": "https://misskey-hub.net/ns#",
"fedibird": "http://fedibird.com/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
@@ -30,7 +29,6 @@
},
"quoteUrl": "as:quoteUrl",
"quoteUri": "fedibird:quoteUri",
- "_misskey_quote": "misskey:_misskey_quote",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 1838f96bb..4a192cdc0 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -385,7 +385,6 @@ test "it prepares a quote post" do
assert modified["object"]["quoteUrl"] == quote_id
assert modified["object"]["quoteUri"] == quote_id
- assert modified["object"]["_misskey_quote"] == quote_id
end
end
From f9697e68c2b75a77575b9b7c89d08a5687bfd7b4 Mon Sep 17 00:00:00 2001
From: Alex Gleason
Date: Sun, 30 Jan 2022 10:57:29 -0600
Subject: [PATCH 056/106] InlineQuotePolicy: skip objects which already have an
.inline-quote span
---
.../activity_pub/mrf/inline_quote_policy.ex | 13 ++++-
.../quote_post/fedibird_quote_mismatched.json | 54 +++++++++++++++++++
.../mrf/inline_quote_policy_test.exs | 17 ++++++
3 files changed, 83 insertions(+), 1 deletion(-)
create mode 100644 test/fixtures/quote_post/fedibird_quote_mismatched.json
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index 7de4935f2..c78675caf 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -10,10 +10,21 @@ defp build_inline_quote(prefix, url) do
" #{prefix}: #{url} "
end
+ defp has_inline_quote?(content, quote_url) do
+ cond do
+ # Does the quote URL exist in the content?
+ content =~ quote_url -> true
+ # Does the content already have a .quote-inline span?
+ content =~ "" -> true
+ # No inline quote found
+ true -> false
+ end
+ end
+
defp filter_object(%{"quoteUrl" => quote_url} = object) do
content = object["content"] || ""
- if content =~ quote_url do
+ if has_inline_quote?(content, quote_url) do
object
else
prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
diff --git a/test/fixtures/quote_post/fedibird_quote_mismatched.json b/test/fixtures/quote_post/fedibird_quote_mismatched.json
new file mode 100644
index 000000000..8dee5daff
--- /dev/null
+++ b/test/fixtures/quote_post/fedibird_quote_mismatched.json
@@ -0,0 +1,54 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "fedibird": "http://fedibird.com/ns#",
+ "quoteUri": "fedibird:quoteUri",
+ "expiry": "fedibird:expiry"
+ }
+ ],
+ "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-01-30T15:44:50Z",
+ "url": "https://fedibird.com/@noellabo/107712183700212249",
+ "attributedTo": "https://fedibird.com/users/noellabo",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://fedibird.com/users/noellabo/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/noellabo/statuses/107712183700212249",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-01-30:objectId=107712183700170473:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/107712183700170473",
+ "quoteUri": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434",
+ "_misskey_quote": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434",
+ "_misskey_content": "揺れていたようだ",
+ "content": "揺れていたようだ QT: https:// unnerv.jp/@UN_NERV/10771217684 9067434
",
+ "contentMap": {
+ "ja": "揺れていたようだ QT: https:// unnerv.jp/@UN_NERV/10771217684 9067434
"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies",
+ "items": []
+ }
+ }
+}
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
index 2291c1dac..44ee91d4b 100644
--- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -72,4 +72,21 @@ test "ignores Fedibird quote posts" do
{:ok, filtered} = InlineQuotePolicy.filter(activity)
assert filtered == activity
end
+
+ test "skips objects which already have an .inline-quote span" do
+ object =
+ File.read!("test/fixtures/quote_post/fedibird_quote_mismatched.json") |> Jason.decode!()
+
+ # Normally the ObjectValidator will fix this before it reaches MRF
+ object = Map.put(object, "quoteUrl", object["quoteUri"])
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://fedibird.com/users/noellabo",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
end
From b0a7e795e799d3c8d750ab909657ec7b3d0bfd58 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Mon, 10 Jul 2023 17:57:09 -0400
Subject: [PATCH 057/106] Unify logic for normalizing quoteUri
---
.../article_note_page_validator.ex | 23 +---------
.../object_validators/common_fixes.ex | 21 ++++++++++
.../web/activity_pub/transmogrifier.ex | 42 ++++++-------------
priv/scrubbers/default.ex | 7 +++-
4 files changed, 40 insertions(+), 53 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index 0b435b251..1b5b2e8fb 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -76,27 +76,6 @@ def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment
def fix_attachments(data), do: data
- defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
-
- # Fedibird
- # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
- defp fix_quote_url(%{"quoteUri" => quote_url} = data) do
- Map.put(data, "quoteUrl", quote_url)
- end
-
- # Old Fedibird (bug)
- # https://github.com/fedibird/mastodon/issues/9
- defp fix_quote_url(%{"quoteURL" => quote_url} = data) do
- Map.put(data, "quoteUrl", quote_url)
- end
-
- # Misskey fallback
- defp fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
- Map.put(data, "quoteUrl", quote_url)
- end
-
- defp fix_quote_url(data), do: data
-
defp fix(data) do
data
|> CommonFixes.fix_actor()
@@ -105,7 +84,7 @@ defp fix(data) do
|> fix_tag()
|> fix_replies()
|> fix_attachments()
- |> fix_quote_url()
+ |> CommonFixes.fix_quote_url()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index add46d561..cc2ad9116 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -76,4 +76,25 @@ def fix_object_action_recipients(data, %Object{data: %{"actor" => actor}}) do
Map.put(data, "to", to)
end
+
+ def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
+
+ # Fedibird
+ # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
+ def fix_quote_url(%{"quoteUri" => quote_url} = data) do
+ Map.put(data, "quoteUrl", quote_url)
+ end
+
+ # Old Fedibird (bug)
+ # https://github.com/fedibird/mastodon/issues/9
+ def fix_quote_url(%{"quoteURL" => quote_url} = data) do
+ Map.put(data, "quoteUrl", quote_url)
+ end
+
+ # Misskey fallback
+ def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
+ Map.put(data, "quoteUrl", quote_url)
+ end
+
+ def fix_quote_url(data), do: data
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 6c6cd712b..86d3ac60f 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -166,45 +166,27 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object
- def fix_quote_url(object, options \\ [])
+ def fix_quote_url_and_maybe_fetch(object, options \\ []) do
+ quote_url =
+ case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
+ %{"quoteUrl" => quote_url} -> quote_url
+ _ -> nil
+ end
- def fix_quote_url(%{"quoteUrl" => quote_url} = object, options)
- when not is_nil(quote_url) do
- with {:ok, quoted_object} <- get_obj_helper(quote_url, options),
+ with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
+ {:ok, quoted_object} <- get_obj_helper(quote_url, options),
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
Map.put(object, "quoteUrl", quoted_object.data["id"])
else
+ {:quoting?, _} ->
+ object
+
e ->
Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
object
end
end
- # Fedibird
- # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
- def fix_quote_url(%{"quoteUri" => quote_url} = object, options) do
- object
- |> Map.put("quoteUrl", quote_url)
- |> fix_quote_url(options)
- end
-
- # Old Fedibird (bug)
- # https://github.com/fedibird/mastodon/issues/9
- def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do
- object
- |> Map.put("quoteUrl", quote_url)
- |> fix_quote_url(options)
- end
-
- # Misskey fallback
- def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do
- object
- |> Map.put("quoteUrl", quote_url)
- |> fix_quote_url(options)
- end
-
- def fix_quote_url(object, _options), do: object
-
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
@@ -493,7 +475,7 @@ def handle_incoming(
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
- |> fix_quote_url(fetch_options)
+ |> fix_quote_url_and_maybe_fetch(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 56324a9fa..4e7950547 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -60,7 +60,12 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:u, ["lang"])
Meta.allow_tag_with_these_attributes(:ul, ["lang"])
- Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline", "quote-inline"])
+ Meta.allow_tag_with_this_attribute_values(:span, "class", [
+ "h-card",
+ "recipients-inline",
+ "quote-inline"
+ ])
+
Meta.allow_tag_with_these_attributes(:span, ["lang"])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
From 9bcec87aba5ce4de6b61b5a95d6832da9dfa0fd8 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Mon, 10 Jul 2023 18:27:23 -0400
Subject: [PATCH 058/106] Allow local quote and private self-quote
---
lib/pleroma/web/common_api/activity_draft.ex | 14 +++++++++++++-
.../pleroma/web/common_api/activity_draft_test.exs | 9 ++++++++-
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index d4875765c..32921aa5a 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -167,9 +167,21 @@ defp visibility(%{params: params} = draft) do
end
end
+ defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
+ true
+ end
+
+ defp can_quote?(draft, object, "private") do
+ draft.user.ap_id == object.data["actor"]
+ end
+
+ defp can_quote?(_, _, _) do
+ false
+ end
+
defp quoting_visibility(%{quote_post: %Activity{}} = draft) do
with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
- visibility when visibility in ~w(public unlisted) <- Visibility.get_visibility(object) do
+ true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
draft
else
_ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs
index 8a09fc710..02bc6cf3b 100644
--- a/test/pleroma/web/common_api/activity_draft_test.exs
+++ b/test/pleroma/web/common_api/activity_draft_test.exs
@@ -12,15 +12,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do
test "create/2 with a quote post" do
user = insert(:user)
+ another_user = insert(:user)
{:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
{:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
{:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
+ {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"})
{:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
{:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id})
- {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id})
+ {:error, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: private.id})
{:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: local.id})
+ {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: local.id})
{:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id})
+ {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: public.id})
end
end
From d244c9d2984d21887f50737597fc03d2d0dd1601 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Mon, 10 Jul 2023 18:28:13 -0400
Subject: [PATCH 059/106] Add changelog
---
changelog.d/quote.add | 1 +
1 file changed, 1 insertion(+)
create mode 100644 changelog.d/quote.add
diff --git a/changelog.d/quote.add b/changelog.d/quote.add
new file mode 100644
index 000000000..1c368ae75
--- /dev/null
+++ b/changelog.d/quote.add
@@ -0,0 +1 @@
+Implement quotes
From 762794eed9e6fc8a03d1416e2a6e080b00df9e10 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Mon, 10 Jul 2023 19:43:18 -0400
Subject: [PATCH 060/106] Fix CommonAPITest
---
test/pleroma/web/common_api_test.exs | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index c4eba8b9c..70a5b6fed 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -825,16 +825,23 @@ test "quote posting with explicit addressing doesn't mention the OP" do
test "quote posting visibility" do
user = insert(:user)
+ another_user = insert(:user)
{:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
{:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
{:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
+ {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"})
{:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
{:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id})
- {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id})
+ {:error, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: private.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: unlisted.id})
+ {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: local.id})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: local.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id})
end
end
From 163e5637335f9454688d3cc83530f82fc640a5b9 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 09:30:43 -0400
Subject: [PATCH 061/106] Allow more flexibility in InlineQuotePolicy
---
config/config.exs | 2 +-
config/description.exs | 18 ++++++++++++++
docs/configuration/cheatsheet.md | 2 +-
.../activity_pub/mrf/inline_quote_policy.ex | 12 ++++++----
priv/scrubbers/default.ex | 1 +
.../mrf/inline_quote_policy_test.exs | 24 +++++++++++++++++--
6 files changed, 50 insertions(+), 9 deletions(-)
diff --git a/config/config.exs b/config/config.exs
index 9149e925a..e8ae31542 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -434,7 +434,7 @@
config :pleroma, :mrf_follow_bot, follower_nickname: nil
-config :pleroma, :mrf_inline_quote, prefix: "RT"
+config :pleroma, :mrf_inline_quote, template: "RT: {url}"
config :pleroma, :rich_media,
enabled: true,
diff --git a/config/description.exs b/config/description.exs
index d18649ae8..079d187d5 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2994,6 +2994,24 @@
}
]
},
+ %{
+ group: :pleroma,
+ key: :mrf_inline_quote,
+ tab: :mrf,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
+ label: "MRF Inline Quote Policy",
+ type: :group,
+ description: "Force quote url to appear in post content.",
+ children: [
+ %{
+ key: :template,
+ type: :string,
+ description:
+ "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.",
+ suggestions: ["RT: {url}"]
+ }
+ ]
+ },
%{
group: :pleroma,
key: :modules,
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 32cc5811a..a17f8735a 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -269,7 +269,7 @@ Notes:
* `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
#### :mrf_inline_quote
-* `prefix`: Prefix before the link (default: `RT`)
+* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `RT: {url}`
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index c78675caf..a0eefefc0 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
@moduledoc "Force a quote line into the message content."
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
- defp build_inline_quote(prefix, url) do
- " #{prefix}: #{url} "
+ defp build_inline_quote(template, url) do
+ quote_line = String.replace(template, "{url}", "#{url} ")
+
+ " #{quote_line} "
end
defp has_inline_quote?(content, quote_url) do
@@ -27,14 +29,14 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do
if has_inline_quote?(content, quote_url) do
object
else
- prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
+ template = Pleroma.Config.get([:mrf_inline_quote, :template])
content =
if String.ends_with?(content, ""),
do:
String.trim_trailing(content, "") <>
- build_inline_quote(prefix, quote_url) <> "",
- else: content <> build_inline_quote(prefix, quote_url)
+ build_inline_quote(template, quote_url) <> "",
+ else: content <> build_inline_quote(template, quote_url)
Map.put(object, "content", content)
end
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 4e7950547..24a76263b 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -38,6 +38,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:abbr, ["title", "lang"])
Meta.allow_tag_with_these_attributes(:b, ["lang"])
+ Meta.allow_tag_with_these_attributes(:bdi, [])
Meta.allow_tag_with_these_attributes(:blockquote, ["lang"])
Meta.allow_tag_with_these_attributes(:br, ["lang"])
Meta.allow_tag_with_these_attributes(:code, ["lang"])
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
index 44ee91d4b..d5762766f 100644
--- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -22,7 +22,27 @@ test "adds quote URL to post content" do
{:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
assert filtered ==
- "Nice post RT: https://gleasonator.com/objects/1234 "
+ "Nice postRT: https://gleasonator.com/objects/1234 "
+ end
+
+ test "adds quote URL to post content, custom template" do
+ clear_config([:mrf_inline_quote, :template], "{url}'s quoting")
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "Nice posthttps://gleasonator.com/objects/1234 's quoting "
end
test "doesn't add line breaks to markdown posts" do
@@ -41,7 +61,7 @@ test "doesn't add line breaks to markdown posts" do
{:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
assert filtered ==
- "Nice post RT: https://gleasonator.com/objects/1234
"
+ "Nice postRT: https://gleasonator.com/objects/1234
"
end
test "ignores Misskey quote posts" do
From e9cd004ba1b904d92b1c07446bbf03dc070cce6a Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 11:09:10 -0400
Subject: [PATCH 062/106] Parse object link as quoteUrl
---
lib/pleroma/constants.ex | 7 +++++
.../audio_image_video_validator.ex | 1 +
.../object_validators/common_fixes.ex | 27 +++++++++++++++++++
.../object_validators/question_validator.ex | 1 +
.../quote_post/fep-e232-tag-example.json | 17 ++++++++++++
.../article_note_page_validator_test.exs | 12 +++++++++
6 files changed, 65 insertions(+)
create mode 100644 test/fixtures/quote_post/fep-e232-tag-example.json
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 6befc6897..9d764ec25 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -83,4 +83,11 @@ defmodule Pleroma.Constants do
)
const(upload_object_types, do: ["Document", "Image"])
+
+ const(activity_json_mime_types,
+ do: [
+ "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
+ "application/activity+json"
+ ]
+ )
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex
index 79ff76104..65ac6bb93 100644
--- a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex
@@ -99,6 +99,7 @@ defp fix(data) do
data
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
+ |> CommonFixes.fix_quote_url()
|> Transmogrifier.fix_emoji()
|> fix_url()
|> fix_content()
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index cc2ad9116..65b8d9a2c 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ require Pleroma.Constants
+
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
@@ -96,5 +98,30 @@ def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
Map.put(data, "quoteUrl", quote_url)
end
+ def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
+ tag = Enum.find(tags, &is_object_link_tag/1)
+
+ if not is_nil(tag) do
+ data
+ |> Map.put("quoteUrl", tag["href"])
+ else
+ data
+ end
+ end
+
def fix_quote_url(data), do: data
+
+ # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
+ defp is_object_link_tag(
+ %{
+ "type" => "Link",
+ "mediaType" => media_type,
+ "href" => href
+ } = tag
+ )
+ when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do
+ true
+ end
+
+ defp is_object_link_tag(_), do: false
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
index ce3305142..621085e6c 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -62,6 +62,7 @@ defp fix(data) do
data
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
+ |> CommonFixes.fix_quote_url()
|> Transmogrifier.fix_emoji()
|> fix_closed()
end
diff --git a/test/fixtures/quote_post/fep-e232-tag-example.json b/test/fixtures/quote_post/fep-e232-tag-example.json
new file mode 100644
index 000000000..23c7fb5ac
--- /dev/null
+++ b/test/fixtures/quote_post/fep-e232-tag-example.json
@@ -0,0 +1,17 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "type": "Note",
+ "content": "This is a quote: RE: https://server.example/objects/123",
+ "tag": [
+ {
+ "type": "Link",
+ "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
+ "href": "https://server.example/objects/123",
+ "name": "RE: https://server.example/objects/123"
+ }
+ ],
+ "id": "https://server.example/objects/1",
+ "to": "https://server.example/users/1",
+ "attributedTo": "https://server.example/users/1",
+ "actor": "https://server.example/users/1"
+}
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index a4ba38e6a..73141cac1 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -146,4 +146,16 @@ test "Misskey quote post" do
assert cng.valid?
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
end
+
+ test "Parse tag as quote" do
+ # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
+
+ insert(:user, ap_id: "https://server.example/users/1")
+
+ data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!()
+ cng = ArticleNotePageValidator.cast_and_validate(data)
+
+ assert cng.valid?
+ assert cng.changes.quoteUrl == "https://server.example/objects/123"
+ end
end
From 479a6f11dbbba0c945c08883956ffab198f91688 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 14:08:24 -0400
Subject: [PATCH 063/106] Keep incoming Link tag
---
.../object_validators/tag_validator.ex | 14 +++++++++++++-
.../article_note_page_validator_test.exs | 7 +++++++
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
index cfd510c19..47cf7b415 100644
--- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
@@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
import Ecto.Changeset
+ require Pleroma.Constants
+
@primary_key false
embedded_schema do
# Common
field(:type, :string)
field(:name, :string)
- # Mention, Hashtag
+ # Mention, Hashtag, Link
field(:href, ObjectValidators.Uri)
+ # Link
+ field(:mediaType, :string)
+
# Emoji
embeds_one :icon, IconObjectValidator, primary_key: false do
field(:type, :string)
@@ -68,6 +73,13 @@ def changeset(struct, %{"type" => "Emoji"} = data) do
|> validate_required([:type, :name, :icon])
end
+ def changeset(struct, %{"type" => "Link"} = data) do
+ struct
+ |> cast(data, [:type, :name, :mediaType, :href])
+ |> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types())
+ |> validate_required([:type, :href, :mediaType])
+ end
+
def changeset(struct, %{"type" => _} = data) do
struct
|> cast(data, [])
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index 73141cac1..4703c3801 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -157,5 +157,12 @@ test "Parse tag as quote" do
assert cng.valid?
assert cng.changes.quoteUrl == "https://server.example/objects/123"
+
+ assert Enum.at(cng.changes.tag, 0).changes == %{
+ type: "Link",
+ mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
+ href: "https://server.example/objects/123",
+ name: "RE: https://server.example/objects/123"
+ }
end
end
From e349e92a441840bbbdbf13cacd307e65f85a38ff Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 14:27:29 -0400
Subject: [PATCH 064/106] Add mrf to force link tag of quoting posts
---
docs/configuration/cheatsheet.md | 1 +
lib/pleroma/constants.ex | 4 +
.../mrf/quote_to_link_tag_policy.ex | 49 +++++++++++++
.../object_validators/common_fixes.ex | 16 ++--
.../mrf/quote_to_link_tag_policy_test.exs | 73 +++++++++++++++++++
5 files changed, 134 insertions(+), 9 deletions(-)
create mode 100644 lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
create mode 100644 test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index a17f8735a..a4cae4dbb 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -161,6 +161,7 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
+ * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 9d764ec25..ed60bcc37 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -84,6 +84,10 @@ defmodule Pleroma.Constants do
const(upload_object_types, do: ["Document", "Image"])
+ const(activity_json_canonical_mime_type,
+ do: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+ )
+
const(activity_json_mime_types,
do: [
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
diff --git a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
new file mode 100644
index 000000000..f1c573d1b
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
+ @moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)"
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
+
+ require Pleroma.Constants
+
+ @impl Pleroma.Web.ActivityPub.MRF.Policy
+ def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
+ {:ok, Map.put(activity, "object", filter_object(object))}
+ end
+
+ @impl Pleroma.Web.ActivityPub.MRF.Policy
+ def filter(object), do: {:ok, object}
+
+ @impl Pleroma.Web.ActivityPub.MRF.Policy
+ def describe, do: {:ok, %{}}
+
+ @impl Pleroma.Web.ActivityPub.MRF.Policy
+ def history_awareness, do: :auto
+
+ defp filter_object(%{"quoteUrl" => quote_url} = object) do
+ tags = object["tag"] || []
+
+ if Enum.any?(tags, fn tag ->
+ CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url
+ end) do
+ object
+ else
+ object
+ |> Map.put(
+ "tag",
+ tags ++
+ [
+ %{
+ "type" => "Link",
+ "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(),
+ "href" => quote_url
+ }
+ ]
+ )
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index 65b8d9a2c..4d9be0bdd 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -112,16 +112,14 @@ def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
def fix_quote_url(data), do: data
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
- defp is_object_link_tag(
- %{
- "type" => "Link",
- "mediaType" => media_type,
- "href" => href
- } = tag
- )
- when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do
+ def is_object_link_tag(%{
+ "type" => "Link",
+ "mediaType" => media_type,
+ "href" => href
+ })
+ when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do
true
end
- defp is_object_link_tag(_), do: false
+ def is_object_link_tag(_), do: false
end
diff --git a/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs
new file mode 100644
index 000000000..96b49b6a0
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicyTest do
+ alias Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy
+
+ use Pleroma.DataCase
+
+ require Pleroma.Constants
+
+ test "Add quote url to Link tag" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity)
+
+ assert object["tag"] == [
+ %{
+ "type" => "Link",
+ "href" => quote_url,
+ "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type()
+ }
+ ]
+ end
+
+ test "Add quote url to Link tag, append to the end" do
+ quote_url = "https://gleasonator.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post",
+ "quoteUrl" => quote_url,
+ "tag" => [%{"type" => "Hashtag", "name" => "#foo"}]
+ }
+ }
+
+ {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity)
+
+ assert [_, tag] = object["tag"]
+
+ assert tag == %{
+ "type" => "Link",
+ "href" => quote_url,
+ "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type()
+ }
+ end
+
+ test "Bypass posts without quoteUrl" do
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://gleasonator.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "Nice post"
+ }
+ }
+
+ assert {:ok, ^activity} = QuoteToLinkTagPolicy.filter(activity)
+ end
+end
From 8b98a98dfb4ab3db9b4f2347713d86ed9c87b61b Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 14:37:12 -0400
Subject: [PATCH 065/106] Make InlineQuotePolicy history aware
---
lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index a0eefefc0..aaa209aa1 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -53,6 +53,9 @@ def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
+ @impl Pleroma.Web.ActivityPub.MRF.Policy
+ def history_awareness, do: :auto
+
@impl true
def config_description do
%{
From 8596f926543126efdb4b8cfe70beab6812824398 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 14:58:20 -0400
Subject: [PATCH 066/106] Fix TransmogrifierTest
---
test/pleroma/web/activity_pub/transmogrifier_test.exs | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 4a192cdc0..5e58d75db 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -123,7 +123,7 @@ test "it fixes both the Create and object contexts in a reply" do
assert activity.data["context"] == object.data["context"]
end
- test "it drops link tags" do
+ test "it keeps link tags" do
insert(:user, ap_id: "https://example.org/users/alice")
message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!()
@@ -131,10 +131,7 @@ test "it drops link tags" do
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity)
- assert length(object.data["tag"]) == 1
-
- tag = object.data["tag"] |> List.first()
- assert tag["type"] == "Mention"
+ assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"]
end
test "it accepts quote posts" do
From 87353e5ad12799d12507253fe9a0363fd9f0c817 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 22:07:16 -0400
Subject: [PATCH 067/106] Fix config descriptions for mrf inline quote
---
config/description.exs | 18 ------------------
.../activity_pub/mrf/inline_quote_policy.ex | 12 +++++++-----
2 files changed, 7 insertions(+), 23 deletions(-)
diff --git a/config/description.exs b/config/description.exs
index 079d187d5..d18649ae8 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2994,24 +2994,6 @@
}
]
},
- %{
- group: :pleroma,
- key: :mrf_inline_quote,
- tab: :mrf,
- related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
- label: "MRF Inline Quote Policy",
- type: :group,
- description: "Force quote url to appear in post content.",
- children: [
- %{
- key: :template,
- type: :string,
- description:
- "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.",
- suggestions: ["RT: {url}"]
- }
- ]
- },
%{
group: :pleroma,
key: :modules,
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index aaa209aa1..171b22c5e 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -61,14 +61,16 @@ def config_description do
%{
key: :mrf_inline_quote,
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
- label: "MRF Inline Quote",
- description: "Force quote post URLs inline",
+ label: "MRF Inline Quote Policy",
+ type: :group,
+ description: "Force quote url to appear in post content.",
children: [
%{
- key: :prefix,
+ key: :template,
type: :string,
- description: "Prefix before the link",
- suggestions: ["RT", "QT", "RE", "RN"]
+ description:
+ "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.",
+ suggestions: ["RT: {url}"]
}
]
}
From 875b46d97d910ffd2c33ac26ed8dfe38f7672f52 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 23:29:23 -0400
Subject: [PATCH 068/106] Do not mention original poster when quoting
---
lib/pleroma/web/common_api/activity_draft.ex | 3 ---
test/pleroma/web/common_api_test.exs | 4 ++--
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 32921aa5a..ca1329284 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -139,9 +139,6 @@ defp in_reply_to(draft), do: draft
defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do
case Activity.get_by_id_with_object(id) do
- %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) ->
- %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]}
-
%Activity{} = activity ->
%__MODULE__{draft | quote_post: activity}
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 70a5b6fed..a98b16d4b 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -808,8 +808,8 @@ test "it allows quote posting" do
assert quote_post.data["quoteUrl"] == quoted.data["id"]
- # The OP is mentioned
- assert quoted.data["actor"] in quote_post.data["to"]
+ # The OP is not mentioned
+ refute quoted.data["actor"] in quote_post.data["to"]
end
test "quote posting with explicit addressing doesn't mention the OP" do
From a8b2f9205d16465a3b11d3802c966db3da908c5d Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 23:47:31 -0400
Subject: [PATCH 069/106] Expose quote_id parameter on the api
---
lib/pleroma/web/api_spec/schemas/status.ex | 5 +++++
lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++++++++++
.../web/mastodon_api/views/status_view_test.exs | 3 +++
3 files changed, 18 insertions(+)
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 5d0eedb08..07f03134a 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -198,6 +198,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
nullable: true,
description: "Quoted status (if any)"
},
+ quote_id: %Schema{
+ nullable: true,
+ allOf: [FlakeID],
+ description: "ID of the status being quoted, if any"
+ },
quote_url: %Schema{
type: :string,
format: :uri,
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index ba4a8f3eb..3d3039751 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -312,6 +312,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
# Here the implicit index of the current content is 0
chrono_order = history_len - 1
+ quote_id = get_quote_id(activity)
+
quote_activity = get_quote(activity, opts)
quote_post =
@@ -431,6 +433,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
quote: quote_post,
+ quote_id: quote_id,
quote_url: object.data["quoteUrl"],
quote_visible: visible_for_user?(quote_activity, opts[:for]),
content: %{"text/plain" => content_plaintext},
@@ -689,6 +692,13 @@ def get_quote(%{data: %{"object" => _object}} = activity, _) do
end
end
+ defp get_quote_id(activity) do
+ case get_quote(activity, %{}) do
+ %Activity{id: id} -> id
+ _ -> nil
+ end
+ end
+
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
url = object.data["url"] || object.data["id"]
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index 6d3a72970..221244d4e 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -327,6 +327,7 @@ test "a note activity" do
context: object_data["context"],
in_reply_to_account_acct: nil,
quote: nil,
+ quote_id: nil,
quote_url: nil,
quote_visible: false,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
@@ -435,10 +436,12 @@ test "a quote post" do
status = StatusView.render("show.json", %{activity: quoted_quote_post})
assert status.pleroma.quote.id == to_string(quote_post.id)
+ assert status.pleroma.quote_id == to_string(quote_post.id)
assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"]
# Quotes don't go more than one level deep
refute status.pleroma.quote.pleroma.quote
+ assert status.pleroma.quote.pleroma.quote_id == to_string(post.id)
assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"]
# In an index
From 08608afca5566f712acdc14b7c43976d6d071106 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Wed, 12 Jul 2023 23:56:54 -0400
Subject: [PATCH 070/106] Fix quote_visible attribute
---
.../web/mastodon_api/views/status_view.ex | 19 +++++++------------
.../mastodon_api/views/status_view_test.exs | 2 ++
2 files changed, 9 insertions(+), 12 deletions(-)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 3d3039751..d070262cc 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -312,12 +312,16 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
# Here the implicit index of the current content is 0
chrono_order = history_len - 1
- quote_id = get_quote_id(activity)
-
quote_activity = get_quote(activity, opts)
+ quote_id =
+ case quote_activity do
+ %Activity{id: id} -> id
+ _ -> nil
+ end
+
quote_post =
- if visible_for_user?(quote_activity, opts[:for]) do
+ if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do
quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
render("show.json", quote_rendering_opts)
else
@@ -671,8 +675,6 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
end
end
- def get_quote(_activity, %{show_quote: false}), do: nil
-
def get_quote(activity, %{quoted_activities: quoted_activities}) do
object = Object.normalize(activity, fetch: false)
@@ -692,13 +694,6 @@ def get_quote(%{data: %{"object" => _object}} = activity, _) do
end
end
- defp get_quote_id(activity) do
- case get_quote(activity, %{}) do
- %Activity{id: id} -> id
- _ -> nil
- end
- end
-
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
url = object.data["url"] || object.data["id"]
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index 221244d4e..baa9b32f5 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -438,11 +438,13 @@ test "a quote post" do
assert status.pleroma.quote.id == to_string(quote_post.id)
assert status.pleroma.quote_id == to_string(quote_post.id)
assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"]
+ assert status.pleroma.quote_visible
# Quotes don't go more than one level deep
refute status.pleroma.quote.pleroma.quote
assert status.pleroma.quote.pleroma.quote_id == to_string(post.id)
assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"]
+ assert status.pleroma.quote.pleroma.quote_visible
# In an index
[status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity})
From 2b5636bf127b1987736c281d346a5771c8168c09 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Fri, 31 Mar 2023 21:47:37 -0400
Subject: [PATCH 071/106] Allow unified streaming endpoint
---
.../web/mastodon_api/websocket_handler.ex | 17 ++++++++++++-----
lib/pleroma/web/streamer.ex | 8 ++++++--
.../integration/mastodon_websocket_test.exs | 5 ++++-
test/pleroma/web/streamer_test.exs | 4 ++++
4 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 88444106d..97a1f1401 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -32,8 +32,15 @@ def init(%{qs: qs} = req, state) do
req
end
+ topics =
+ if topic do
+ [topic]
+ else
+ []
+ end
+
{:cowboy_websocket, req,
- %{user: user, topic: topic, oauth_token: oauth_token, count: 0, timer: nil},
+ %{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil},
%{idle_timeout: @timeout}}
else
{:error, :bad_topic} ->
@@ -50,10 +57,10 @@ def init(%{qs: qs} = req, state) do
def websocket_init(state) do
Logger.debug(
- "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}"
+ "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}"
)
- Streamer.add_socket(state.topic, state.oauth_token)
+ Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
{:ok, %{state | timer: timer()}}
end
@@ -109,10 +116,10 @@ def terminate(_reason, _req, []), do: :ok
def terminate(reason, _req, state) do
Logger.debug(
- "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic || "?"}: #{inspect(reason)}"
+ "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}"
)
- Streamer.remove_socket(state.topic)
+ Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
:ok
end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index b9a04cc76..6b5fdbf0c 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -59,10 +59,14 @@ defp can_access_stream(user, oauth_token, kind) do
end
@doc "Expand and authorizes a stream"
- @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) ::
- {:ok, topic :: String.t()} | {:error, :bad_topic}
+ @spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, Map.t()) ::
+ {:ok, topic :: String.t() | nil} | {:error, :bad_topic}
def get_topic(stream, user, oauth_token, params \\ %{})
+ def get_topic(nil = _stream, _user, _oauth_token, _params) do
+ {:ok, nil}
+ end
+
# Allow all public steams if the instance allows unauthenticated access.
# Otherwise, only allow users with valid oauth tokens.
def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 9be0445c0..3b120c10e 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -33,7 +33,6 @@ def start_socket(qs \\ nil, headers \\ []) do
test "refuses invalid requests" do
capture_log(fn ->
- assert {:error, %WebSockex.RequestError{code: 404}} = start_socket()
assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk")
Process.sleep(30)
end)
@@ -49,6 +48,10 @@ test "requires authentication and a valid token for protected streams" do
end)
end
+ test "allows unified stream" do
+ assert {:ok, _} = start_socket()
+ end
+
test "allows public streams without authentication" do
assert {:ok, _} = start_socket("?stream=public")
assert {:ok, _} = start_socket("?stream=public:local")
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index 7ab0e379b..da97b87d8 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -22,6 +22,10 @@ defmodule Pleroma.Web.StreamerTest do
setup do: clear_config([:instance, :skip_thread_containment])
describe "get_topic/_ (unauthenticated)" do
+ test "allows no stream" do
+ assert {:ok, nil} = Streamer.get_topic(nil, nil, nil)
+ end
+
test "allows public" do
assert {:ok, "public"} = Streamer.get_topic("public", nil, nil)
assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil)
From 273cda63ad79b61f4d37e4b7603694908e894e4f Mon Sep 17 00:00:00 2001
From: tusooa
Date: Fri, 31 Mar 2023 22:55:52 -0400
Subject: [PATCH 072/106] Allow subscribing to streams
---
.../web/mastodon_api/websocket_handler.ex | 74 +++++++++++++++
lib/pleroma/web/views/streamer_view.ex | 18 ++++
.../integration/mastodon_websocket_test.exs | 90 +++++++++++++++++++
3 files changed, 182 insertions(+)
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 97a1f1401..a42a9a63c 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Streamer
+ alias Pleroma.Web.StreamerView
@behaviour :cowboy_websocket
@@ -73,6 +74,16 @@ def websocket_handle(:pong, state) do
# We only receive pings for now
def websocket_handle(:ping, state), do: {:ok, state}
+ def websocket_handle({:text, text}, state) do
+ with {:ok, %{} = event} <- Jason.decode(text) do
+ handle_client_event(event, state)
+ else
+ _ ->
+ Logger.error("#{__MODULE__} received non-JSON event: #{inspect(text)}")
+ {:ok, state}
+ end
+ end
+
def websocket_handle(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
@@ -144,4 +155,67 @@ defp authenticate_request(access_token, sec_websocket) do
defp timer do
Process.send_after(self(), :tick, @tick)
end
+
+ defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
+ with {_, {:ok, topic}} <-
+ {:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
+ {_, false} <- {:subscribed, topic in state.topics} do
+ Streamer.add_socket(topic, state.oauth_token)
+
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})}
+ ], %{state | topics: [topic | state.topics]}}
+ else
+ {:subscribed, true} ->
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})}
+ ], state}
+
+ {:topic, {:error, error}} ->
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{
+ type: "subscribe",
+ result: "error",
+ error: error
+ })}
+ ], state}
+ end
+ end
+
+ defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params, state) do
+ with {_, {:ok, topic}} <-
+ {:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
+ {_, true} <- {:subscribed, topic in state.topics} do
+ Streamer.remove_socket(topic)
+
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})}
+ ], %{state | topics: List.delete(state.topics, topic)}}
+ else
+ {:subscribed, false} ->
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})}
+ ], state}
+
+ {:topic, {:error, error}} ->
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{
+ type: "unsubscribe",
+ result: "error",
+ error: error
+ })}
+ ], state}
+ end
+ end
+
+ defp handle_client_event(params, state) do
+ Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
+ {[], state}
+ end
end
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 6a55242b0..19f098783 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -135,4 +135,22 @@ def render("conversation.json", %Participation{} = participation) do
}
|> Jason.encode!()
end
+
+ def render("pleroma_respond.json", %{type: type, result: result} = params) do
+ %{
+ event: "pleroma.respond",
+ payload:
+ %{
+ result: result,
+ type: type
+ }
+ |> Map.merge(maybe_error(params))
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
+ defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"}
+ defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"}
+ defp maybe_error(_), do: %{}
end
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 3b120c10e..9db0f714f 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -31,6 +31,13 @@ def start_socket(qs \\ nil, headers \\ []) do
WebsocketClient.start_link(self(), path, headers)
end
+ defp decode_json(json) do
+ with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
+ {:ok, payload} <- Jason.decode(payload_text) do
+ {:ok, %{"event" => event, "payload" => payload}}
+ end
+ end
+
test "refuses invalid requests" do
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk")
@@ -79,6 +86,89 @@ test "receives well formatted events" do
assert json == view_json
end
+ describe "subscribing via WebSocket" do
+ test "can subscribe" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
+
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, json} = Jason.decode(raw_json)
+
+ assert "update" == json["event"]
+ assert json["payload"]
+ assert {:ok, json} = Jason.decode(json["payload"])
+
+ view_json =
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert json == view_json
+ end
+
+ test "won't double subscribe" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "ignored"}
+ }} = decode_json(raw_json)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
+
+ assert_receive {:text, _}, 1_000
+ refute_receive {:text, _}, 1_000
+ end
+
+ test "can unsubscribe" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "unsubscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "unsubscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
+ refute_receive {:text, _}, 1_000
+ end
+ end
+
describe "with a valid user token" do
setup do
{:ok, app} =
From 21395aa5090f2a53bdbe0ef5fac46693d16025ed Mon Sep 17 00:00:00 2001
From: tusooa
Date: Fri, 31 Mar 2023 23:19:57 -0400
Subject: [PATCH 073/106] Allow authenticating via client-sent events
---
.../web/mastodon_api/websocket_handler.ex | 36 +++++++++
lib/pleroma/web/views/streamer_view.ex | 1 +
.../integration/mastodon_websocket_test.exs | 81 +++++++++++++++++++
3 files changed, 118 insertions(+)
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index a42a9a63c..6233c3340 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -214,6 +214,42 @@ defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params
end
end
+ defp handle_client_event(
+ %{"type" => "pleroma.authenticate", "token" => access_token} = _params,
+ state
+ ) do
+ with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
+ {:ok, user, oauth_token} <- authenticate_request(access_token, nil) do
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma.authenticate",
+ result: "success"
+ })}
+ ], %{state | user: user, oauth_token: oauth_token}}
+ else
+ {:auth, _, _} ->
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma.authenticate",
+ result: "error",
+ error: :already_authenticated
+ })}
+ ], state}
+
+ _ ->
+ {[
+ {:text,
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma.authenticate",
+ result: "error",
+ error: :unauthorized
+ })}
+ ], state}
+ end
+ end
+
defp handle_client_event(params, state) do
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
{[], state}
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 19f098783..0cdcb1918 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -152,5 +152,6 @@ def render("pleroma_respond.json", %{type: type, result: result} = params) do
defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"}
defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"}
+ defp maybe_error(%{error: :already_authenticated}), do: %{error: "already_authenticated"}
defp maybe_error(_), do: %{}
end
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 9db0f714f..827c7b5b0 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -224,6 +224,87 @@ test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
end)
end
+ test "accepts valid token on client-sent event", %{token: token} do
+ assert {:ok, pid} = start_socket()
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "pleroma.authenticate", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+ end
+
+ test "rejects invalid token on client-sent event" do
+ assert {:ok, pid} = start_socket()
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{
+ "type" => "pleroma.authenticate",
+ "result" => "error",
+ "error" => "unauthorized"
+ }
+ }} = decode_json(raw_json)
+ end
+
+ test "rejects new authenticate request if already logged-in", %{token: token} do
+ assert {:ok, pid} = start_socket()
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "pleroma.authenticate", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{
+ "type" => "pleroma.authenticate",
+ "result" => "error",
+ "error" => "already_authenticated"
+ }
+ }} = decode_json(raw_json)
+ end
+
test "disconnect when token is revoked", %{app: app, user: user, token: token} do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
From 7d005e8c93b22dc3d7be1a66dd2d404b7f54306a Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 01:25:13 -0400
Subject: [PATCH 074/106] Return stream attribute in server-sent events
---
lib/pleroma/constants.ex | 4 ++
.../web/mastodon_api/websocket_handler.ex | 4 +-
lib/pleroma/web/streamer.ex | 27 +++++----
lib/pleroma/web/views/streamer_view.ex | 42 +++++++++++---
.../integration/mastodon_websocket_test.exs | 33 +++++++++++
test/pleroma/web/streamer_test.exs | 58 +++++++++----------
6 files changed, 119 insertions(+), 49 deletions(-)
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index ed60bcc37..77bc4bfac 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -94,4 +94,8 @@ defmodule Pleroma.Constants do
"application/activity+json"
]
)
+
+ const(public_streams,
+ do: ["public", "public:local", "public:media", "public:local:media"]
+ )
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 6233c3340..2707673ba 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -89,11 +89,11 @@ def websocket_handle(frame, state) do
{:ok, state}
end
- def websocket_info({:render_with_user, view, template, item}, state) do
+ def websocket_info({:render_with_user, view, template, item, topic}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do
- websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
+ websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
else
{:ok, state}
end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 6b5fdbf0c..70e46617a 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.Streamer do
require Logger
+ require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry
- @public_streams ["public", "public:local", "public:media", "public:local:media"]
+ @public_streams Pleroma.Constants.public_streams()
@local_streams ["public:local", "public:local:media"]
@user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
@@ -223,8 +224,8 @@ defp do_stream("direct", item) do
end
defp do_stream("follow_relationship", item) do
- text = StreamerView.render("follow_relationships_update.json", item)
user_topic = "user:#{item.follower.id}"
+ text = StreamerView.render("follow_relationships_update.json", item, user_topic)
Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
@@ -270,9 +271,11 @@ defp do_stream("list", item) do
defp do_stream(topic, %Notification{} = item)
when topic in ["user", "user:notification"] do
- Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list ->
+ user_topic = "#{topic}:#{item.user_id}"
+
+ Registry.dispatch(@registry, user_topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
- send(pid, {:render_with_user, StreamerView, "notification.json", item})
+ send(pid, {:render_with_user, StreamerView, "notification.json", item, user_topic})
end)
end)
end
@@ -281,7 +284,7 @@ defp do_stream(topic, {user, %MessageReference{} = cm_ref})
when topic in ["user", "user:pleroma_chat"] do
topic = "#{topic}:#{user.id}"
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
@@ -309,7 +312,7 @@ defp do_stream(topic, item) do
end
defp push_to_socket(topic, %Participation{} = participation) do
- rendered = StreamerView.render("conversation.json", participation)
+ rendered = StreamerView.render("conversation.json", participation, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _} ->
@@ -337,12 +340,15 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"])
|> Map.put(:object, item.object)
- anon_render = StreamerView.render("status_update.json", create_activity)
+ anon_render = StreamerView.render("status_update.json", create_activity, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, auth?} ->
if auth? do
- send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity})
+ send(
+ pid,
+ {:render_with_user, StreamerView, "status_update.json", create_activity, topic}
+ )
else
send(pid, {:text, anon_render})
end
@@ -351,12 +357,13 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
end
defp push_to_socket(topic, item) do
- anon_render = StreamerView.render("update.json", item)
+ Logger.debug("topic=#{topic}")
+ anon_render = StreamerView.render("update.json", item, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, auth?} ->
if auth? do
- send(pid, {:render_with_user, StreamerView, "update.json", item})
+ send(pid, {:render_with_user, StreamerView, "update.json", item, topic})
else
send(pid, {:text, anon_render})
end
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 0cdcb1918..f591da9a6 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -11,8 +11,11 @@ defmodule Pleroma.Web.StreamerView do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.NotificationView
- def render("update.json", %Activity{} = activity, %User{} = user) do
+ require Pleroma.Constants
+
+ def render("update.json", %Activity{} = activity, %User{} = user, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
@@ -25,8 +28,9 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
|> Jason.encode!()
end
- def render("status_update.json", %Activity{} = activity, %User{} = user) do
+ def render("status_update.json", %Activity{} = activity, %User{} = user, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "status.update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
@@ -39,8 +43,9 @@ def render("status_update.json", %Activity{} = activity, %User{} = user) do
|> Jason.encode!()
end
- def render("notification.json", %Notification{} = notify, %User{} = user) do
+ def render("notification.json", %Notification{} = notify, %User{} = user, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "notification",
payload:
NotificationView.render(
@@ -52,8 +57,9 @@ def render("notification.json", %Notification{} = notify, %User{} = user) do
|> Jason.encode!()
end
- def render("update.json", %Activity{} = activity) do
+ def render("update.json", %Activity{} = activity, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
@@ -65,8 +71,9 @@ def render("update.json", %Activity{} = activity) do
|> Jason.encode!()
end
- def render("status_update.json", %Activity{} = activity) do
+ def render("status_update.json", %Activity{} = activity, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "status.update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
@@ -78,7 +85,7 @@ def render("status_update.json", %Activity{} = activity) do
|> Jason.encode!()
end
- def render("chat_update.json", %{chat_message_reference: cm_ref}) do
+ def render("chat_update.json", %{chat_message_reference: cm_ref}, topic) do
# Explicitly giving the cmr for the object here, so we don't accidentally
# send a later 'last_message' that was inserted between inserting this and
# streaming it out
@@ -93,6 +100,7 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
)
%{
+ stream: render("stream.json", %{topic: topic}),
event: "pleroma:chat_update",
payload:
representation
@@ -101,8 +109,9 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|> Jason.encode!()
end
- def render("follow_relationships_update.json", item) do
+ def render("follow_relationships_update.json", item, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "pleroma:follow_relationships_update",
payload:
%{
@@ -123,8 +132,9 @@ def render("follow_relationships_update.json", item) do
|> Jason.encode!()
end
- def render("conversation.json", %Participation{} = participation) do
+ def render("conversation.json", %Participation{} = participation, topic) do
%{
+ stream: render("stream.json", %{topic: topic}),
event: "conversation",
payload:
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
@@ -150,6 +160,22 @@ def render("pleroma_respond.json", %{type: type, result: result} = params) do
|> Jason.encode!()
end
+ def render("stream.json", %{topic: "user:pleroma_chat:" <> _}), do: ["user:pleroma_chat"]
+ def render("stream.json", %{topic: "user:notification:" <> _}), do: ["user:notification"]
+ def render("stream.json", %{topic: "user:" <> _}), do: ["user"]
+ def render("stream.json", %{topic: "direct:" <> _}), do: ["direct"]
+ def render("stream.json", %{topic: "list:" <> id}), do: ["list", id]
+ def render("stream.json", %{topic: "hashtag:" <> tag}), do: ["hashtag", tag]
+
+ def render("stream.json", %{topic: "public:remote:media:" <> instance}),
+ do: ["public:remote:media", instance]
+
+ def render("stream.json", %{topic: "public:remote:" <> instance}),
+ do: ["public:remote", instance]
+
+ def render("stream.json", %{topic: stream}) when stream in Pleroma.Constants.public_streams(),
+ do: [stream]
+
defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"}
defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"}
defp maybe_error(%{error: :already_authenticated}), do: %{error: "already_authenticated"}
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 827c7b5b0..d2e98df3b 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -116,6 +116,39 @@ test "can subscribe" do
assert json == view_json
end
+ test "can subscribe to multiple streams" do
+ user = insert(:user)
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma.respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber #mew"})
+
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, %{"stream" => stream1}} = Jason.decode(raw_json)
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, %{"stream" => stream2}} = Jason.decode(raw_json)
+
+ streams = [stream1, stream2]
+ assert ["hashtag", "mew"] in streams
+ assert ["public"] in streams
+ end
+
test "won't double subscribe" do
user = insert(:user)
{:ok, pid} = start_socket()
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index da97b87d8..cc57a2989 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -246,7 +246,7 @@ test "it streams the user's post in the 'user' stream", %{user: user, token: oau
Streamer.get_topic_and_add_socket("user", user, oauth_token)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user, activity)
end
@@ -257,7 +257,7 @@ test "it streams boosts of the user in the 'user' stream", %{user: user, token:
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
{:ok, announce} = CommonAPI.repeat(activity.id, user)
- assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _}
refute Streamer.filtered_by_user?(user, announce)
end
@@ -310,7 +310,7 @@ test "it streams boosts of mastodon user in the 'user' stream", %{
{:ok, %Pleroma.Activity{data: _data, local: false} = announce} =
Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data)
- assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _}
refute Streamer.filtered_by_user?(user, announce)
end
@@ -322,7 +322,7 @@ test "it sends notify to in the 'user' stream", %{
Streamer.get_topic_and_add_socket("user", user, oauth_token)
Streamer.stream("user", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, _}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -334,7 +334,7 @@ test "it sends notify to in the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
Streamer.stream("user:notification", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, _}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -355,7 +355,7 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{
Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token)
Streamer.stream("user:pleroma_chat", {user, cm_ref})
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:pleroma_chat:#{user.id}")
assert text =~ "hey cirno"
assert_receive {:text, ^text}
@@ -373,7 +373,7 @@ test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_t
Streamer.get_topic_and_add_socket("user", user, oauth_token)
Streamer.stream("user", {user, cm_ref})
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:#{user.id}")
assert text =~ "hey cirno"
assert_receive {:text, ^text}
@@ -394,7 +394,7 @@ test "it sends chat message notifications to the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
Streamer.stream("user:notification", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, _}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -440,7 +440,7 @@ test "it sends favorite to 'user:notification' stream'", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
{:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
assert notif.activity.id == favorite_activity.id
refute Streamer.filtered_by_user?(user, notif)
end
@@ -469,7 +469,7 @@ test "it sends follow activities to the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
{:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
assert notif.activity.id == follow_activity.id
refute Streamer.filtered_by_user?(user, notif)
end
@@ -534,7 +534,7 @@ test "it streams edits in the 'user' stream", %{user: user, token: oauth_token}
{:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"})
create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
- assert_receive {:render_with_user, _, "status_update.json", ^create}
+ assert_receive {:render_with_user, _, "status_update.json", ^create, _}
refute Streamer.filtered_by_user?(user, edited)
end
@@ -545,7 +545,7 @@ test "it streams own edits in the 'user' stream", %{user: user, token: oauth_tok
{:ok, edited} = CommonAPI.update(user, activity, %{status: "mew mew"})
create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
- assert_receive {:render_with_user, _, "status_update.json", ^create}
+ assert_receive {:render_with_user, _, "status_update.json", ^create, _}
refute Streamer.filtered_by_user?(user, edited)
end
end
@@ -558,7 +558,7 @@ test "it sends to public (authenticated)" do
Streamer.get_topic_and_add_socket("public", user, oauth_token)
{:ok, activity} = CommonAPI.post(other_user, %{status: "Test"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(other_user, activity)
end
@@ -658,7 +658,7 @@ test "it filters to user if recipients invalid and thread containment is enabled
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
assert Streamer.filtered_by_user?(user, activity)
end
@@ -680,7 +680,7 @@ test "it sends message if recipients invalid and thread containment is disabled"
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user, activity)
end
@@ -703,7 +703,7 @@ test "it sends message if recipients invalid and thread containment is enabled b
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user, activity)
end
end
@@ -717,7 +717,7 @@ test "it filters messages involving blocked users", %{user: user, token: oauth_t
Streamer.get_topic_and_add_socket("public", user, oauth_token)
{:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
assert Streamer.filtered_by_user?(user, activity)
end
@@ -734,17 +734,17 @@ test "it filters messages transitively involving blocked users", %{
{:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_one}
+ assert_receive {:render_with_user, _, _, ^activity_one, _}
assert Streamer.filtered_by_user?(blocker, activity_one)
{:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_two}
+ assert_receive {:render_with_user, _, _, ^activity_two, _}
assert Streamer.filtered_by_user?(blocker, activity_two)
{:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_three}
+ assert_receive {:render_with_user, _, _, ^activity_three, _}
assert Streamer.filtered_by_user?(blocker, activity_three)
end
end
@@ -805,7 +805,7 @@ test "it sends wanted private posts to list", %{user: user_a, token: user_a_toke
visibility: "private"
})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
refute Streamer.filtered_by_user?(user_a, activity)
end
end
@@ -823,7 +823,7 @@ test "it filters muted reblogs", %{user: user1, token: user1_token} do
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2)
- assert_receive {:render_with_user, _, _, ^announce_activity}
+ assert_receive {:render_with_user, _, _, ^announce_activity, _}
assert Streamer.filtered_by_user?(user1, announce_activity)
end
@@ -839,7 +839,7 @@ test "it filters reblog notification for reblog-muted actors", %{
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
assert Streamer.filtered_by_user?(user1, notif)
end
@@ -855,7 +855,7 @@ test "it send non-reblog notification for reblog-muted actors", %{
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, _}
refute Streamer.filtered_by_user?(user1, notif)
end
end
@@ -870,7 +870,7 @@ test "it filters posts from muted threads" do
{:ok, activity} = CommonAPI.post(user, %{status: "super hot take"})
{:ok, _} = CommonAPI.add_mute(user2, activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, _}
assert Streamer.filtered_by_user?(user2, activity)
end
end
@@ -912,7 +912,7 @@ test "it doesn't send conversation update to the 'direct' stream when the last m
})
create_activity_id = create_activity.id
- assert_receive {:render_with_user, _, _, ^create_activity}
+ assert_receive {:render_with_user, _, _, ^create_activity, _}
assert_receive {:text, received_conversation1}
assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
@@ -947,8 +947,8 @@ test "it sends conversation update to the 'direct' stream when a message is dele
visibility: "direct"
})
- assert_receive {:render_with_user, _, _, ^create_activity}
- assert_receive {:render_with_user, _, _, ^create_activity2}
+ assert_receive {:render_with_user, _, _, ^create_activity, _}
+ assert_receive {:render_with_user, _, _, ^create_activity2, _}
assert_receive {:text, received_conversation1}
assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
assert_receive {:text, received_conversation1}
@@ -977,7 +977,7 @@ test "it sends conversation update to the 'direct' stream when a message is dele
receive do
{StreamerTest, :ready} ->
- assert_receive {:render_with_user, _, "update.json", _}
+ assert_receive {:render_with_user, _, "update.json", _, _}
receive do
{StreamerTest, :revoked} -> finalize.()
From a348c2e4dd0551a603509501d718d9e0b995be90 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 01:29:11 -0400
Subject: [PATCH 075/106] Use pleroma: instead of pleroma. for ws events
---
.../web/mastodon_api/websocket_handler.ex | 8 ++--
lib/pleroma/web/views/streamer_view.ex | 2 +-
.../integration/mastodon_websocket_test.exs | 46 ++++++++++---------
test/pleroma/web/streamer_test.exs | 14 +++++-
4 files changed, 42 insertions(+), 28 deletions(-)
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 2707673ba..07c2b62e3 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -215,7 +215,7 @@ defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params
end
defp handle_client_event(
- %{"type" => "pleroma.authenticate", "token" => access_token} = _params,
+ %{"type" => "pleroma:authenticate", "token" => access_token} = _params,
state
) do
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
@@ -223,7 +223,7 @@ defp handle_client_event(
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
- type: "pleroma.authenticate",
+ type: "pleroma:authenticate",
result: "success"
})}
], %{state | user: user, oauth_token: oauth_token}}
@@ -232,7 +232,7 @@ defp handle_client_event(
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
- type: "pleroma.authenticate",
+ type: "pleroma:authenticate",
result: "error",
error: :already_authenticated
})}
@@ -242,7 +242,7 @@ defp handle_client_event(
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
- type: "pleroma.authenticate",
+ type: "pleroma:authenticate",
result: "error",
error: :unauthorized
})}
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index f591da9a6..f97570b0a 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -148,7 +148,7 @@ def render("conversation.json", %Participation{} = participation, topic) do
def render("pleroma_respond.json", %{type: type, result: result} = params) do
%{
- event: "pleroma.respond",
+ event: "pleroma:respond",
payload:
%{
result: result,
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index d2e98df3b..21e7ca2b0 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -95,7 +95,7 @@ test "can subscribe" do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
@@ -124,16 +124,20 @@ test "can subscribe to multiple streams" do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
- WebsocketClient.send_text(pid, %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!())
+ WebsocketClient.send_text(
+ pid,
+ %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!()
+ )
+
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
@@ -157,7 +161,7 @@ test "won't double subscribe" do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
@@ -166,7 +170,7 @@ test "won't double subscribe" do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "ignored"}
}} = decode_json(raw_json)
@@ -184,7 +188,7 @@ test "can unsubscribe" do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
@@ -193,7 +197,7 @@ test "can unsubscribe" do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "unsubscribe", "result" => "success"}
}} = decode_json(raw_json)
@@ -262,15 +266,15 @@ test "accepts valid token on client-sent event", %{token: token} do
WebsocketClient.send_text(
pid,
- %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!()
+ %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
- "event" => "pleroma.respond",
- "payload" => %{"type" => "pleroma.authenticate", "result" => "success"}
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "pleroma:authenticate", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!())
@@ -278,7 +282,7 @@ test "accepts valid token on client-sent event", %{token: token} do
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
end
@@ -288,16 +292,16 @@ test "rejects invalid token on client-sent event" do
WebsocketClient.send_text(
pid,
- %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!()
+ %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{
- "type" => "pleroma.authenticate",
+ "type" => "pleroma:authenticate",
"result" => "error",
"error" => "unauthorized"
}
@@ -309,29 +313,29 @@ test "rejects new authenticate request if already logged-in", %{token: token} do
WebsocketClient.send_text(
pid,
- %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!()
+ %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
- "event" => "pleroma.respond",
- "payload" => %{"type" => "pleroma.authenticate", "result" => "success"}
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "pleroma:authenticate", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(
pid,
- %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!()
+ %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
- "event" => "pleroma.respond",
+ "event" => "pleroma:respond",
"payload" => %{
- "type" => "pleroma.authenticate",
+ "type" => "pleroma:authenticate",
"result" => "error",
"error" => "already_authenticated"
}
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index cc57a2989..d85358fd4 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -355,7 +355,12 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{
Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token)
Streamer.stream("user:pleroma_chat", {user, cm_ref})
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:pleroma_chat:#{user.id}")
+ text =
+ StreamerView.render(
+ "chat_update.json",
+ %{chat_message_reference: cm_ref},
+ "user:pleroma_chat:#{user.id}"
+ )
assert text =~ "hey cirno"
assert_receive {:text, ^text}
@@ -373,7 +378,12 @@ test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_t
Streamer.get_topic_and_add_socket("user", user, oauth_token)
Streamer.stream("user", {user, cm_ref})
- text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:#{user.id}")
+ text =
+ StreamerView.render(
+ "chat_update.json",
+ %{chat_message_reference: cm_ref},
+ "user:#{user.id}"
+ )
assert text =~ "hey cirno"
assert_receive {:text, ^text}
From 2d430679468a4c6a9b5c365a53f007cfa28679d9 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 02:24:30 -0400
Subject: [PATCH 076/106] Document the streaming endpoint
---
.../API/differences_in_mastoapi_responses.md | 116 ++++++++++++++++++
1 file changed, 116 insertions(+)
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index 4007c63c8..ef6b3b3be 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -357,6 +357,122 @@ The message payload consist of:
- `follower_count`: follower count
- `following_count`: following count
+### Authenticating via `sec-websocket-protocol` header
+
+Pleroma allows to authenticate via the `sec-websocket-protocol` header, for example, if your access token is `your-access-token`, you can authenticate using the following:
+
+```
+sec-websocket-protocol: your-access-token
+```
+
+### Authenticating after connection via `pleroma:authenticate` event
+
+Pleroma allows to authenticate after connection is established, via the `pleroma:authenticate` event. For example, if your access token is `your-access-token`, you can send the following after the connection is established:
+
+```
+{"type": "pleroma:authenticate", "token": "your-access-token"}
+```
+
+### Response to client-sent events
+
+Pleroma will respond to client-sent events that it recognizes. Supported event types are:
+
+- `subscribe`
+- `unsubscribe`
+- `pleroma:authenticate`
+
+The reply will be in the following format:
+
+```
+{
+ "event": "pleroma:respond",
+ "payload": "{\"type\": \"\", \"result\": \"\", \"error\": \"\"}"
+}
+```
+
+Result of the action can be either `success`, `ignored` or `error`. If it is `error`, the `error` property will contain the error code. Otherwise, the `error` property will not be present. Below are some examples:
+
+```
+{
+ "event": "pleroma:respond",
+ "payload": "{\"type\": \"pleroma:authenticate\", \"result\": \"success\"}"
+}
+
+{
+ "event": "pleroma:respond",
+ "payload": "{\"type\": \"subscribe\", \"result\": \"ignored\"}"
+}
+
+{
+ "event": "pleroma:respond",
+ "payload": "{\"type\": \"unsubscribe\", \"result\": \"error\", \"error\": \"bad_topic\"}"
+}
+```
+
+If the sent event is not of a type that Pleroma supports, it will not reply.
+
+### The `stream` attribute of a server-sent event
+
+Technically, this is in Mastodon, but its documentation does nothing to specify its format.
+
+This attribute appears on every event type except `pleroma:respond` and `delete`. It helps clients determine where they should display the new statuses.
+
+The value of the attribute is an array containing one or two elements. The first element is the type of the stream. The second is the identifier related to that specific stream, if applicable.
+
+For the following stream types, there is a second element in the array:
+
+- `list`: The second element is the id of the list.
+- `hashtag`: The second element is the name of the hashtag.
+- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
+
+For all other stream types, there is no second element.
+
+Some examples of valid `stream` values:
+
+- `list:1`: List of id 1.
+- `hashtag:mew`: The hashtag #mew.
+- `user:notifications`: Notifications for the current user.
+- `user`: Home timeline.
+- `public:remote:mew.moe`: Public posts from the instance mew.moe .
+
+### The unified streaming endpoint
+
+If you do not specify a stream to connect to when requesting `/api/v1/streaming`, you will enter a connection that subscribes to no streams. After the connection is established, you can authenticate and then subscribe to different streams.
+
+### List of supported streams
+
+Below is a list of supported streams by Pleroma. To make a single-stream WebSocket connection, append the string specified in "Query style" to the streaming endpoint url.
+To subscribe to a stream after the connection is established, merge the JSON object specified in "Subscribe style" with `{"type": "subscribe"}`. To unsubscribe, merge it with `{"type": "unsubscribe"}`.
+
+For example, to receive updates on the list 1, you can connect to `/api/v1/streaming/?stream=list&list=1`, or send
+
+```
+{"type": "subscribe", "stream": "list", "list": 1}
+```
+
+upon establishing the websocket connection.
+
+To unsubscribe to list 1, send
+
+```
+{"type": "unsubscribe", "stream": "list", "list": 1}
+```
+
+Note that if you specify a stream that requires a logged-in user in the query string (for example, `user` or `list`), you have to specify the access token when you are trying to establish the connection, i.e. in the query string or via the `sec-websocket-protocol` header.
+
+- `list`
+ - Query style: `?stream=list&list=`
+ - Subscribe style: `{"stream": "list", "list": }`
+- `public`, `public:local`, `public:media`, `public:local:media`, `user`, `user:pleroma_chat`, `user:notifications`, `direct`
+ - Query style: `?stream=`
+ - Subscribe style: `{"stream": ""}`
+- `hashtag`
+ - Query style: `?stream=hashtag&tag=`
+ - Subscribe style: `{"stream": "hashtag", "tag": ""}`
+- `public:remote`, `public:remote:media`
+ - Query style: `?stream=&instance=`
+ - Subscribe style: `{"stream": "", "instance": ""}`
+
## User muting and thread muting
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.
From 9572be1e5fe0201cd069271c7025ef233e8ddf00 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 02:35:12 -0400
Subject: [PATCH 077/106] Add tests for list streams
---
.../integration/mastodon_websocket_test.exs | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 21e7ca2b0..8129645ec 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -342,6 +342,43 @@ test "rejects new authenticate request if already logged-in", %{token: token} do
}} = decode_json(raw_json)
end
+ test "accepts the 'list' stream", %{token: token, user: user} do
+ posting_user = insert(:user)
+
+ {:ok, list} = Pleroma.List.create("test", user)
+ Pleroma.List.follow(list, posting_user)
+
+ assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}")
+
+ assert {:ok, pid} = start_socket("?access_token=#{token.token}")
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "subscribe", stream: "list", list: list.id} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "success"}
+ }} = decode_json(raw_json)
+
+ WebsocketClient.send_text(
+ pid,
+ %{type: "subscribe", stream: "list", list: to_string(list.id)} |> Jason.encode!()
+ )
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "ignored"}
+ }} = decode_json(raw_json)
+ end
+
test "disconnect when token is revoked", %{app: app, user: user, token: token} do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
From 7f12ad6dccfe4c81fa7e0d4e66c43bedadbf4c6a Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 02:37:19 -0400
Subject: [PATCH 078/106] Fix docs wording
---
.../API/differences_in_mastoapi_responses.md | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index ef6b3b3be..48a9c104c 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -421,7 +421,7 @@ The value of the attribute is an array containing one or two elements. The first
For the following stream types, there is a second element in the array:
-- `list`: The second element is the id of the list.
+- `list`: The second element is the id of the list, as a string.
- `hashtag`: The second element is the name of the hashtag.
- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
@@ -429,11 +429,11 @@ For all other stream types, there is no second element.
Some examples of valid `stream` values:
-- `list:1`: List of id 1.
-- `hashtag:mew`: The hashtag #mew.
-- `user:notifications`: Notifications for the current user.
-- `user`: Home timeline.
-- `public:remote:mew.moe`: Public posts from the instance mew.moe .
+- `["list", "1"]`: List of id 1.
+- `["hashtag", "mew"]`: The hashtag #mew.
+- `["user:notifications"]`: Notifications for the current user.
+- `["user"]`: Home timeline.
+- `["public:remote", "mew.moe"]`: Public posts from the instance mew.moe .
### The unified streaming endpoint
@@ -447,7 +447,7 @@ To subscribe to a stream after the connection is established, merge the JSON obj
For example, to receive updates on the list 1, you can connect to `/api/v1/streaming/?stream=list&list=1`, or send
```
-{"type": "subscribe", "stream": "list", "list": 1}
+{"type": "subscribe", "stream": "list", "list": "1"}
```
upon establishing the websocket connection.
@@ -455,14 +455,14 @@ upon establishing the websocket connection.
To unsubscribe to list 1, send
```
-{"type": "unsubscribe", "stream": "list", "list": 1}
+{"type": "unsubscribe", "stream": "list", "list": "1"}
```
Note that if you specify a stream that requires a logged-in user in the query string (for example, `user` or `list`), you have to specify the access token when you are trying to establish the connection, i.e. in the query string or via the `sec-websocket-protocol` header.
- `list`
- Query style: `?stream=list&list=`
- - Subscribe style: `{"stream": "list", "list": }`
+ - Subscribe style: `{"stream": "list", "list": ""}`
- `public`, `public:local`, `public:media`, `public:local:media`, `user`, `user:pleroma_chat`, `user:notifications`, `direct`
- Query style: `?stream=`
- Subscribe style: `{"stream": ""}`
From 949c4f01c6d5686ff1852e6439616c2069557ef0 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 02:38:16 -0400
Subject: [PATCH 079/106] Fix NotificationTest
---
test/pleroma/notification_test.exs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
index e55aa3a08..71af9acb8 100644
--- a/test/pleroma/notification_test.exs
+++ b/test/pleroma/notification_test.exs
@@ -252,7 +252,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no
task =
Task.async(fn ->
{:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token)
- assert_receive {:render_with_user, _, _, _}, 4_000
+ assert_receive {:render_with_user, _, _, _, _}, 4_000
end)
task_user_notification =
@@ -260,7 +260,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no
{:ok, _topic} =
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
- assert_receive {:render_with_user, _, _, _}, 4_000
+ assert_receive {:render_with_user, _, _, _, _}, 4_000
end)
activity = insert(:note_activity)
From eebc605bc25deead55c305d703c06ddb9d9b1107 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 08:27:43 -0400
Subject: [PATCH 080/106] Clear up debug statement
---
lib/pleroma/web/streamer.ex | 1 -
1 file changed, 1 deletion(-)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 70e46617a..48ca82421 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -357,7 +357,6 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
end
defp push_to_socket(topic, item) do
- Logger.debug("topic=#{topic}")
anon_render = StreamerView.render("update.json", item, topic)
Registry.dispatch(@registry, topic, fn list ->
From 050227f11898f402f5888d53e6460b704bcd0a8b Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 08:39:38 -0400
Subject: [PATCH 081/106] Add test to cover error: bad_topic
---
test/pleroma/integration/mastodon_websocket_test.exs | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 8129645ec..0e8413749 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -180,6 +180,18 @@ test "won't double subscribe" do
refute_receive {:text, _}, 1_000
end
+ test "rejects invalid streams" do
+ {:ok, pid} = start_socket()
+ WebsocketClient.send_text(pid, %{type: "subscribe", stream: "nonsense"} |> Jason.encode!())
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "pleroma:respond",
+ "payload" => %{"type" => "subscribe", "result" => "error", "error" => "bad_topic"}
+ }} = decode_json(raw_json)
+ end
+
test "can unsubscribe" do
user = insert(:user)
{:ok, pid} = start_socket()
From 4cf109d3c4093d788e6b2de229a9e4034146a5be Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 08:47:46 -0400
Subject: [PATCH 082/106] Add test to cover rendering update with user
---
.../integration/mastodon_websocket_test.exs | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 0e8413749..5ec681ce3 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -406,5 +406,32 @@ test "disconnect when token is revoked", %{app: app, user: user, token: token} d
assert_receive {:close, _}
refute_receive {:close, _}
end
+
+ test "receives private statuses", %{user: reading_user, token: token} do
+ user = insert(:user)
+ CommonAPI.follow(reading_user, user)
+
+ {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"})
+
+ assert_receive {:text, raw_json}, 1_000
+ assert {:ok, json} = Jason.decode(raw_json)
+
+ assert "update" == json["event"]
+ assert json["payload"]
+ assert {:ok, json} = Jason.decode(json["payload"])
+
+ view_json =
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json",
+ activity: activity,
+ for: reading_user
+ )
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert json == view_json
+ end
end
end
From 26f5caebae1581f1dcdc6d1352840d27009e061c Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 08:56:53 -0400
Subject: [PATCH 083/106] Add test to cover notifications streaming
---
.../integration/mastodon_websocket_test.exs | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 5ec681ce3..96648264e 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -433,5 +433,30 @@ test "receives private statuses", %{user: reading_user, token: token} do
assert json == view_json
end
+
+ test "receives notifications", %{user: reading_user, token: token} do
+ user = insert(:user)
+ CommonAPI.follow(reading_user, user)
+
+ {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
+
+ {:ok, %Pleroma.Activity{id: activity_id} = _activity} =
+ CommonAPI.post(user, %{
+ status: "nice echo chamber @#{reading_user.nickname}",
+ visibility: "private"
+ })
+
+ assert_receive {:text, raw_json}, 1_000
+
+ assert {:ok,
+ %{
+ "event" => "notification",
+ "payload" => %{
+ "status" => %{
+ "id" => ^activity_id
+ }
+ }
+ }} = decode_json(raw_json)
+ end
end
end
From 314360e5e3a4bdd13f152e94050cb8562e2ea0ed Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 09:02:43 -0400
Subject: [PATCH 084/106] Add test to cover edit streaming
---
.../integration/mastodon_websocket_test.exs | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 96648264e..7d5dfc255 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -434,6 +434,34 @@ test "receives private statuses", %{user: reading_user, token: token} do
assert json == view_json
end
+ test "receives edits", %{user: reading_user, token: token} do
+ user = insert(:user)
+ CommonAPI.follow(reading_user, user)
+
+ {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"})
+
+ assert_receive {:text, _raw_json}, 1_000
+
+ {:ok, _} = CommonAPI.update(user, activity, %{status: "mew mew", visibility: "private"})
+
+ assert_receive {:text, raw_json}, 1_000
+
+ activity = Pleroma.Activity.normalize(activity)
+
+ view_json =
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json",
+ activity: activity,
+ for: reading_user
+ )
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
+ end
+
test "receives notifications", %{user: reading_user, token: token} do
user = insert(:user)
CommonAPI.follow(reading_user, user)
From 844d1a14e0b4aabbb61a6693fa6dd3a0aa0dbc5b Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 15:31:56 -0400
Subject: [PATCH 085/106] Start writing api docs for streaming endpoint
---
lib/pleroma/web/api_spec.ex | 10 +-
.../operations/streaming_operation.ex | 186 ++++++++++++++++++
2 files changed, 195 insertions(+), 1 deletion(-)
create mode 100644 lib/pleroma/web/api_spec/operations/streaming_operation.ex
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 2d56dc643..163226ce5 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -10,6 +10,14 @@ defmodule Pleroma.Web.ApiSpec do
@behaviour OpenApi
+ defp streaming_paths do
+ %{
+ "/api/v1/streaming" => %OpenApiSpex.PathItem{
+ get: Pleroma.Web.ApiSpec.StreamingOperation.streaming_operation()
+ }
+ }
+ end
+
@impl OpenApi
def spec(opts \\ []) do
%OpenApi{
@@ -45,7 +53,7 @@ def spec(opts \\ []) do
}
},
# populate the paths from a phoenix router
- paths: OpenApiSpex.Paths.from_router(Router),
+ paths: Map.merge(streaming_paths(), OpenApiSpex.Paths.from_router(Router)),
components: %OpenApiSpex.Components{
parameters: %{
"accountIdOrNickname" =>
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
new file mode 100644
index 000000000..1ef7d72ef
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -0,0 +1,186 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.StreamingOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Response
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+
+ @spec open_api_operation(atom) :: Operation.t()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ @spec streaming_operation() :: Operation.t()
+ def streaming_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Establish streaming connection",
+ description: "Receive statuses in real-time via WebSocket.",
+ security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
+ parameters: [
+ Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
+ required: true
+ ),
+ Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
+ required: true
+ ),
+ Operation.parameter(
+ :"sec-websocket-key",
+ :header,
+ %Schema{type: :string},
+ "sec-websocket-key header",
+ required: true
+ ),
+ Operation.parameter(
+ :"sec-websocket-version",
+ :header,
+ %Schema{type: :string},
+ "sec-websocket-version header",
+ required: true
+ )
+ ],
+ responses: %{
+ 101 => switching_protocols_response(),
+ 200 =>
+ Operation.response(
+ "Server-sent events",
+ "application/json",
+ server_sent_events()
+ )
+ }
+ }
+ end
+
+ defp switching_protocols_response do
+ %Response{
+ description: "Switching protocols",
+ headers: %{
+ "connection" => %OpenApiSpex.Header{required: true},
+ "upgrade" => %OpenApiSpex.Header{required: true},
+ "sec-websocket-accept" => %OpenApiSpex.Header{required: true}
+ }
+ }
+ end
+
+ defp server_sent_events do
+ %Schema{
+ oneOf: [
+ update_event(),
+ status_update_event(),
+ pleroma_respond_event()
+ ]
+ }
+ end
+
+ defp stream do
+ %Schema{
+ type: :array,
+ title: "Stream",
+ description: """
+ The stream identifier.
+ The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier.
+ Currently, for the following stream types, there is a second element in the array:
+
+ - `list`: The second element is the id of the list, as a string.
+ - `hashtag`: The second element is the name of the hashtag.
+ - `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
+ """,
+ maxItems: 2,
+ minItems: 1,
+ items: %Schema{type: :string},
+ example: ["hashtag", "mew"]
+ }
+ end
+
+ defp get_schema(%Schema{} = schema), do: schema
+ defp get_schema(schema), do: schema.schema
+
+ defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
+ payload_type = opts[:payload_type] || :json
+ has_stream = opts[:has_stream] || true
+
+ stream_properties =
+ if has_stream do
+ %{stream: stream()}
+ else
+ %{}
+ end
+
+ stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{}
+
+ stream_required = if has_stream, do: [:stream], else: []
+
+ %Schema{
+ type: :object,
+ title: name,
+ description: description,
+ required: [:event, :payload] ++ stream_required,
+ properties:
+ %{
+ event: %Schema{
+ title: "Event type",
+ description: "Type of the event.",
+ type: :string,
+ required: true,
+ enum: [type]
+ },
+ payload:
+ if payload_type == :json do
+ %Schema{
+ title: "Event payload",
+ description: "JSON-encoded string of #{get_schema(payload).title}",
+ allOf: [payload]
+ }
+ else
+ payload
+ end
+ }
+ |> Map.merge(stream_properties),
+ example:
+ %{
+ "event" => type,
+ "payload" => get_schema(payload).example |> Jason.encode!()
+ }
+ |> Map.merge(stream_example)
+ }
+ end
+
+ defp update_event do
+ server_sent_event_helper("New status", "A newly-posted status.", "update", Status)
+ end
+
+ defp status_update_event do
+ server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
+ end
+
+ defp pleroma_respond_event do
+ server_sent_event_helper(
+ "Server response",
+ "A response to a client-sent event.",
+ "pleroma:respond",
+ %Schema{
+ type: :object,
+ title: "Results",
+ required: [:result],
+ properties: %{
+ result: %Schema{
+ type: :string,
+ title: "Result of the request",
+ enum: ["success", "error", "ignored"]
+ },
+ error: %Schema{
+ type: :string,
+ title: "Error code",
+ description: "An error identifier. Only appears if `result` is `error`."
+ }
+ },
+ example: %{"result" => "success"}
+ }
+ )
+ end
+end
From dcef33f5f0c93883a634d12dc662b83d7ef6abfa Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 16:07:27 -0400
Subject: [PATCH 086/106] Document server-sent events of streaming
---
.../operations/streaming_operation.ex | 106 ++++++++++++++++++
1 file changed, 106 insertions(+)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index 1ef7d72ef..cdef41603 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -7,6 +7,10 @@ defmodule Pleroma.Web.ApiSpec.StreamingOperation do
alias OpenApiSpex.Response
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.NotificationOperation
+ alias Pleroma.Web.ApiSpec.Schemas.Chat
+ alias Pleroma.Web.ApiSpec.Schemas.Conversation
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
@spec open_api_operation(atom) :: Operation.t()
@@ -22,6 +26,7 @@ def streaming_operation do
summary: "Establish streaming connection",
description: "Receive statuses in real-time via WebSocket.",
security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
+ operationId: "WebsocketHandler.streaming",
parameters: [
Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
required: true
@@ -72,6 +77,11 @@ defp server_sent_events do
oneOf: [
update_event(),
status_update_event(),
+ notification_event(),
+ chat_update_event(),
+ follow_relationships_update_event(),
+ conversation_event(),
+ delete_event(),
pleroma_respond_event()
]
}
@@ -158,6 +168,102 @@ defp status_update_event do
server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
end
+ defp notification_event do
+ server_sent_event_helper(
+ "Notification",
+ "A new notification.",
+ "notification",
+ NotificationOperation.notification()
+ )
+ end
+
+ defp follow_relationships_update_event do
+ server_sent_event_helper(
+ "Follow relationships update",
+ "An update to follow relationships.",
+ "pleroma:follow_relationships_update",
+ %Schema{
+ type: :object,
+ title: "Follow relationships update",
+ required: [:state, :follower, :following],
+ properties: %{
+ state: %Schema{
+ type: :string,
+ description: "Follow state of the relationship.",
+ enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"]
+ },
+ follower: %Schema{
+ type: :object,
+ description: "Information about the follower.",
+ required: [:id, :follower_count, :following_count],
+ properties: %{
+ id: FlakeID,
+ follower_count: %Schema{type: :integer},
+ following_count: %Schema{type: :integer}
+ }
+ },
+ following: %Schema{
+ type: :object,
+ description: "Information about the following person.",
+ required: [:id, :follower_count, :following_count],
+ properties: %{
+ id: FlakeID,
+ follower_count: %Schema{type: :integer},
+ following_count: %Schema{type: :integer}
+ }
+ }
+ },
+ example: %{
+ "state" => "follow_pending",
+ "follower" => %{
+ "id" => "someUser1",
+ "follower_count" => 1,
+ "following_count" => 1
+ },
+ "following" => %{
+ "id" => "someUser2",
+ "follower_count" => 1,
+ "following_count" => 1
+ }
+ }
+ }
+ )
+ end
+
+ defp chat_update_event do
+ server_sent_event_helper(
+ "Chat update",
+ "A new chat message.",
+ "pleroma:chat_update",
+ Chat
+ )
+ end
+
+ defp conversation_event do
+ server_sent_event_helper(
+ "Conversation",
+ "An update about a conversation",
+ "conversation",
+ Conversation
+ )
+ end
+
+ defp delete_event do
+ server_sent_event_helper(
+ "Delete",
+ "A status that was just deleted.",
+ "delete",
+ %Schema{
+ type: :string,
+ title: "Status id",
+ description: "Id of the deleted status",
+ allOf: [FlakeID],
+ example: "some-opaque-id"
+ },
+ payload_type: :string
+ )
+ end
+
defp pleroma_respond_event do
server_sent_event_helper(
"Server response",
From 8829dcaee42b3ad1ee50f95b0586b22118771785 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 16:33:22 -0400
Subject: [PATCH 087/106] Document client-sent events in streaming
---
.../operations/streaming_operation.ex | 126 +++++++++++++++++-
1 file changed, 125 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index cdef41603..18674c9a1 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -6,13 +6,14 @@ defmodule Pleroma.Web.ApiSpec.StreamingOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Response
alias OpenApiSpex.Schema
- alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.NotificationOperation
alias Pleroma.Web.ApiSpec.Schemas.Chat
alias Pleroma.Web.ApiSpec.Schemas.Conversation
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
+ require Pleroma.Constants
+
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@@ -49,6 +50,7 @@ def streaming_operation do
required: true
)
],
+ requestBody: request_body("Client-sent events", client_sent_events()),
responses: %{
101 => switching_protocols_response(),
200 =>
@@ -289,4 +291,126 @@ defp pleroma_respond_event do
}
)
end
+
+ defp client_sent_events do
+ %Schema{
+ oneOf: [
+ subscribe_event(),
+ unsubscribe_event(),
+ authenticate_event()
+ ]
+ }
+ end
+
+ defp request_body(description, schema, opts \\ []) do
+ %OpenApiSpex.RequestBody{
+ description: description,
+ content: %{
+ "application/json" => %OpenApiSpex.MediaType{
+ schema: schema,
+ example: opts[:example],
+ examples: opts[:examples]
+ }
+ }
+ }
+ end
+
+ defp client_sent_event_helper(name, description, type, properties, opts) do
+ required = opts[:required] || []
+
+ %Schema{
+ type: :object,
+ title: name,
+ required: [:type] ++ required,
+ description: description,
+ properties:
+ %{
+ type: %Schema{type: :string, enum: [type], description: "Type of the event."}
+ }
+ |> Map.merge(properties),
+ example: opts[:example]
+ }
+ end
+
+ defp subscribe_event do
+ client_sent_event_helper(
+ "Subscribe",
+ "Subscribe to a stream.",
+ "subscribe",
+ stream_specifier(),
+ required: [:stream],
+ example: %{"type" => "subscribe", "stream" => "list", "list" => "1"}
+ )
+ end
+
+ defp unsubscribe_event do
+ client_sent_event_helper(
+ "Unsubscribe",
+ "Unsubscribe from a stream.",
+ "subscribe",
+ stream_specifier(),
+ required: [:stream],
+ example: %{
+ "type" => "unsubscribe",
+ "stream" => "public:remote:media",
+ "instance" => "example.org"
+ }
+ )
+ end
+
+ defp authenticate_event do
+ client_sent_event_helper(
+ "Authenticate",
+ "Authenticate via an access token.",
+ "pleroma:authenticate",
+ %{
+ token: %Schema{
+ type: :string,
+ description: "An OAuth access token with corresponding permissions.",
+ example: "some token"
+ }
+ },
+ required: [:token]
+ )
+ end
+
+ defp stream_specifier do
+ %{
+ stream: %Schema{
+ type: :string,
+ description: "The name of the stream.",
+ enum:
+ Pleroma.Constants.public_streams() ++
+ [
+ "public:remote",
+ "public:remote:media",
+ "user",
+ "user:pleroma_chat",
+ "user:notification",
+ "direct",
+ "list",
+ "hashtag"
+ ]
+ },
+ list: %Schema{
+ type: :string,
+ title: "List id",
+ description: "The id of the list. Required when `stream` is `list`.",
+ example: "some-id"
+ },
+ tag: %Schema{
+ type: :string,
+ title: "Hashtag name",
+ description: "The name of the hashtag. Required when `stream` is `hashtag`.",
+ example: "mew"
+ },
+ instance: %Schema{
+ type: :string,
+ title: "Domain name",
+ description:
+ "Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.",
+ example: "example.org"
+ }
+ }
+ end
end
From f393a15dd1217a7f6aec9e9acc7b983e7b165a91 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 16:46:32 -0400
Subject: [PATCH 088/106] Fix some specs about server-sent events in streaming
---
.../operations/streaming_operation.ex | 49 ++++++++++++-------
1 file changed, 32 insertions(+), 17 deletions(-)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index 18674c9a1..fa48b9613 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -113,8 +113,8 @@ defp get_schema(%Schema{} = schema), do: schema
defp get_schema(schema), do: schema.schema
defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
- payload_type = opts[:payload_type] || :json
- has_stream = opts[:has_stream] || true
+ payload_type = Keyword.get(opts, :payload_type, :json)
+ has_stream = Keyword.get(opts, :has_stream, true)
stream_properties =
if has_stream do
@@ -127,6 +127,24 @@ defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
stream_required = if has_stream, do: [:stream], else: []
+ payload_schema =
+ if payload_type == :json do
+ %Schema{
+ title: "Event payload",
+ description: "JSON-encoded string of #{get_schema(payload).title}",
+ allOf: [payload]
+ }
+ else
+ payload
+ end
+
+ payload_example =
+ if payload_type == :json do
+ get_schema(payload).example |> Jason.encode!()
+ else
+ get_schema(payload).example
+ end
+
%Schema{
type: :object,
title: name,
@@ -141,22 +159,13 @@ defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
required: true,
enum: [type]
},
- payload:
- if payload_type == :json do
- %Schema{
- title: "Event payload",
- description: "JSON-encoded string of #{get_schema(payload).title}",
- allOf: [payload]
- }
- else
- payload
- end
+ payload: payload_schema
}
|> Map.merge(stream_properties),
example:
%{
"event" => type,
- "payload" => get_schema(payload).example |> Jason.encode!()
+ "payload" => payload_example
}
|> Map.merge(stream_example)
}
@@ -262,7 +271,8 @@ defp delete_event do
allOf: [FlakeID],
example: "some-opaque-id"
},
- payload_type: :string
+ payload_type: :string,
+ has_stream: false
)
end
@@ -274,7 +284,7 @@ defp pleroma_respond_event do
%Schema{
type: :object,
title: "Results",
- required: [:result],
+ required: [:result, :type],
properties: %{
result: %Schema{
type: :string,
@@ -285,10 +295,15 @@ defp pleroma_respond_event do
type: :string,
title: "Error code",
description: "An error identifier. Only appears if `result` is `error`."
+ },
+ type: %Schema{
+ type: :string,
+ description: "Type of the request."
}
},
- example: %{"result" => "success"}
- }
+ example: %{"result" => "success", "type" => "pleroma:authenticate"}
+ },
+ has_stream: false
)
end
From c13f0a8460853d8fe9aa04cb5d57ea06b692a3bf Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 17:00:35 -0400
Subject: [PATCH 089/106] Add meta-info and query strings to streaming doc
---
.../operations/streaming_operation.ex | 89 +++++++++++++------
1 file changed, 61 insertions(+), 28 deletions(-)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index fa48b9613..4c4888d8e 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -25,31 +25,46 @@ def streaming_operation do
%Operation{
tags: ["Timelines"],
summary: "Establish streaming connection",
- description: "Receive statuses in real-time via WebSocket.",
+ description: """
+ Receive statuses in real-time via WebSocket.
+
+ You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using
+ the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain
+ your client's compatibility with Mastodon).
+
+ You may specify a stream on the query string. If you do so and you are connecting to a stream that requires logged-in users,
+ you must specify the access token at the time of the connection (i.e. via query string or header).
+
+ Otherwise, you have the option to authenticate after you have established the connection through client-sent events.
+
+ The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section
+ describes what events server will send through WebSocket.
+ """,
security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
operationId: "WebsocketHandler.streaming",
- parameters: [
- Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
- required: true
- ),
- Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
- required: true
- ),
- Operation.parameter(
- :"sec-websocket-key",
- :header,
- %Schema{type: :string},
- "sec-websocket-key header",
- required: true
- ),
- Operation.parameter(
- :"sec-websocket-version",
- :header,
- %Schema{type: :string},
- "sec-websocket-version header",
- required: true
- )
- ],
+ parameters:
+ [
+ Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
+ required: true
+ ),
+ Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
+ required: true
+ ),
+ Operation.parameter(
+ :"sec-websocket-key",
+ :header,
+ %Schema{type: :string},
+ "sec-websocket-key header",
+ required: true
+ ),
+ Operation.parameter(
+ :"sec-websocket-version",
+ :header,
+ %Schema{type: :string},
+ "sec-websocket-version header",
+ required: true
+ )
+ ] ++ stream_params() ++ access_token_params(),
requestBody: request_body("Client-sent events", client_sent_events()),
responses: %{
101 => switching_protocols_response(),
@@ -63,6 +78,20 @@ def streaming_operation do
}
end
+ defp stream_params do
+ stream_specifier()
+ |> Enum.map(fn {name, schema} ->
+ Operation.parameter(name, :query, schema, get_schema(schema).description)
+ end)
+ end
+
+ defp access_token_params do
+ [
+ Operation.parameter(:access_token, :query, token(), token().description),
+ Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description)
+ ]
+ end
+
defp switching_protocols_response do
%Response{
description: "Switching protocols",
@@ -379,16 +408,20 @@ defp authenticate_event do
"Authenticate via an access token.",
"pleroma:authenticate",
%{
- token: %Schema{
- type: :string,
- description: "An OAuth access token with corresponding permissions.",
- example: "some token"
- }
+ token: token()
},
required: [:token]
)
end
+ defp token do
+ %Schema{
+ type: :string,
+ description: "An OAuth access token with corresponding permissions.",
+ example: "some token"
+ }
+ end
+
defp stream_specifier do
%{
stream: %Schema{
From 32de0683c4069f5877722dce0b4f5a9a6825b7c7 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 17:15:58 -0400
Subject: [PATCH 090/106] Fix unsubscribe event type in streaming doc
---
lib/pleroma/web/api_spec/operations/streaming_operation.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index 4c4888d8e..ae3aeb4ab 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -391,7 +391,7 @@ defp unsubscribe_event do
client_sent_event_helper(
"Unsubscribe",
"Unsubscribe from a stream.",
- "subscribe",
+ "unsubscribe",
stream_specifier(),
required: [:stream],
example: %{
From 840dd01035581a37613f695facdd99fbf6ac8319 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Apr 2023 18:33:43 -0400
Subject: [PATCH 091/106] Fix duplicated schema names
---
lib/pleroma/web/api_spec/operations/streaming_operation.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index ae3aeb4ab..b580bc2f0 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -281,7 +281,7 @@ defp chat_update_event do
defp conversation_event do
server_sent_event_helper(
- "Conversation",
+ "Conversation update",
"An update about a conversation",
"conversation",
Conversation
From 3e7d2e29b369535a9a942a4090cde9a21892f8c1 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 1 Jul 2023 23:07:07 -0400
Subject: [PATCH 092/106] Add changelog
---
changelog.d/unified-streaming.add | 1 +
1 file changed, 1 insertion(+)
create mode 100644 changelog.d/unified-streaming.add
diff --git a/changelog.d/unified-streaming.add b/changelog.d/unified-streaming.add
new file mode 100644
index 000000000..84821fcc8
--- /dev/null
+++ b/changelog.d/unified-streaming.add
@@ -0,0 +1 @@
+Add unified streaming endpoint
From eb33a03d0ad8571e3f0f3e0c5e9af39158c72a09 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Tue, 18 Jul 2023 18:13:49 -0400
Subject: [PATCH 093/106] Explain the encode-decode roundtrip
---
test/pleroma/integration/mastodon_websocket_test.exs | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 7d5dfc255..a2c20f0a6 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -38,6 +38,13 @@ defp decode_json(json) do
end
end
+ # Turns atom keys to strings
+ defp atom_key_to_string(json) do
+ json
+ |> Jason.encode!()
+ |> Jason.decode!()
+ end
+
test "refuses invalid requests" do
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk")
@@ -80,8 +87,7 @@ test "receives well formatted events" do
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
- |> Jason.encode!()
- |> Jason.decode!()
+ |> atom_key_to_string()
assert json == view_json
end
From b748efe66a099b66300f2beda42f5639911bab4a Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sat, 29 Jul 2023 12:55:43 -0400
Subject: [PATCH 094/106] Fix mentioning punycode domains when using Markdown
---
changelog.d/punycode-mention.fix | 1 +
lib/pleroma/formatter.ex | 2 +-
test/pleroma/web/common_api_test.exs | 12 ++++++++++++
3 files changed, 14 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/punycode-mention.fix
diff --git a/changelog.d/punycode-mention.fix b/changelog.d/punycode-mention.fix
new file mode 100644
index 000000000..f013c2dac
--- /dev/null
+++ b/changelog.d/punycode-mention.fix
@@ -0,0 +1 @@
+Fix mentioning punycode domains when using Markdown
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index a46c3e381..11d5af2fb 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -124,7 +124,7 @@ def mentions_escape(text, options \\ []) do
end
def markdown_to_html(text) do
- Earmark.as_html!(text, %Earmark.Options{compact_output: true})
+ Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
end
def html_escape({text, mentions, hashtags}, type) do
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index a98b16d4b..b21dd4e23 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -843,6 +843,18 @@ test "quote posting visibility" do
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id})
end
+
+ test "it properly mentions punycode domain" do
+ user = insert(:user)
+
+ _mentioned_user =
+ insert(:user, ap_id: "https://xn--i2raa.com/users/yyy", nickname: "yyy@xn--i2raa.com")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "hey @yyy@xn--i2raa.com", content_type: "text/markdown"})
+
+ assert "https://xn--i2raa.com/users/yyy" in Object.normalize(activity).data["to"]
+ end
end
describe "reactions" do
From df0b56576a6863db9f1ca2c7987f2a465bd1f1c3 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sun, 24 Sep 2023 22:46:30 -0400
Subject: [PATCH 095/106] Fix other quotation mark conversion tests
---
test/pleroma/web/common_api/utils_test.exs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs
index 4ce039d64..27b1da1e3 100644
--- a/test/pleroma/web/common_api/utils_test.exs
+++ b/test/pleroma/web/common_api/utils_test.exs
@@ -200,7 +200,7 @@ test "local mentions" do
{result, _, []} = Utils.format_input(code, "text/markdown")
assert result ==
- ~s[@mario @luigi yo what’s up?
]
+ ~s[@mario @luigi yo what's up?
]
end
test "remote mentions" do
@@ -211,7 +211,7 @@ test "remote mentions" do
{result, _, []} = Utils.format_input(code, "text/markdown")
assert result ==
- ~s[@mario @luigi yo what’s up?
]
+ ~s[@mario @luigi yo what's up?
]
end
test "raw HTML" do
@@ -229,7 +229,7 @@ test "rulers" do
test "blockquote" do
code = ~s[> whoms't are you quoting?]
{result, [], []} = Utils.format_input(code, "text/markdown")
- assert result == "whoms’t are you quoting?
"
+ assert result == "whoms't are you quoting?
"
end
test "code" do
From a2a69709b51692be307940c79d0befdd3c9678bb Mon Sep 17 00:00:00 2001
From: tusooa
Date: Tue, 24 Oct 2023 19:57:31 -0400
Subject: [PATCH 096/106] 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 097/106] 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 ab894d98f4be5aadc033b71dacc499b85b579bc1 Mon Sep 17 00:00:00 2001
From: tusooa
Date: Sun, 29 Oct 2023 12:59:03 -0400
Subject: [PATCH 098/106] 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 @@
-To use Pleroma, please enable JavaScript.
\ No newline at end of file
+
To use Pleroma, please enable JavaScript.
\ 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~W5Pk1gM5
IbwhmHN;}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
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
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":"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","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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