From fe8ec1b6f9a918c3b24b562c5dae752bcfc134ad Mon Sep 17 00:00:00 2001 From: Pleroma Date: Mon, 1 Jan 2018 16:36:40 +0000 Subject: [PATCH 001/317] shigusegubu customs --- config/config.exs | 8 ++++---- config/emoji.txt | 9 ++++++++- installation/pleroma.service | 2 ++ lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/ostatus/ostatus_controller.ex | 2 +- lib/pleroma/web/router.ex | 2 +- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/config/config.exs b/config/config.exs index cf6cbaa9d..fa616bcce 100644 --- a/config/config.exs +++ b/config/config.exs @@ -47,9 +47,9 @@ version = config :pleroma, :http, proxy_url: nil config :pleroma, :instance, - version: version, - name: "Pleroma", - email: "example@example.com", + version: "2.1", + name: "Shigusegubu", + email: "pleroma@hjkos.com", limit: 5000, upload_limit: 16_000_000, registrations_open: true, @@ -82,7 +82,7 @@ config :pleroma, :media_proxy, # base_url: "https://cache.pleroma.social" -config :pleroma, :chat, enabled: true +config :pleroma, :chat, enabled: false config :ecto, json_library: Jason diff --git a/config/emoji.txt b/config/emoji.txt index 7afacb09f..3ea245a5b 100644 --- a/config/emoji.txt +++ b/config/emoji.txt @@ -28,4 +28,11 @@ f_33b00b, /emoji/f_33b00b.png f_33b22b, /emoji/f_33b22b.png f_33h, /emoji/f_33h.png f_33t, /emoji/f_33t.png - +shinitai-shi, /emoji/shinitai-shi.png +shinitai-ni, /emoji/shinitai-ni.png +shinitai-ta, /emoji/shinitai-ta.png +shinitai-i, /emoji/shinitai-i.png +shinitai_shi, /emoji/shinitai_shi.png +shinitai_ni, /emoji/shinitai_ni.png +shinitai_ta, /emoji/shinitai_ta.png +shinitai_i, /emoji/shinitai_i.png diff --git a/installation/pleroma.service b/installation/pleroma.service index fd4180985..08c8eab32 100644 --- a/installation/pleroma.service +++ b/installation/pleroma.service @@ -6,10 +6,12 @@ After=network.target postgresql.service User=pleroma WorkingDirectory=/home/pleroma/pleroma Environment="HOME=/home/pleroma" +Environment="MIX_ENV=prod" ExecStart=/usr/local/bin/mix phx.server ExecReload=/bin/kill $MAINPID KillMode=process Restart=on-failure +StandardOutput=journal [Install] WantedBy=multi-user.target diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 1a012c1b4..f0748542b 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.Endpoint do plug( Plug.Static, at: "/", - from: :pleroma, + from: "priv_sid/static", only: ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png) ) diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 2f72fdb16..ce18c320a 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -134,7 +134,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do "html" -> conn |> put_resp_content_type("text/html") - |> send_file(200, "priv/static/index.html") + |> send_file(200, "priv_sid/static/index.html") _ -> represent_activity(conn, activity, user) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 34652cdde..a0e958e93 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -366,7 +366,7 @@ defmodule Fallback.RedirectController do if Mix.env() != :test do conn |> put_resp_content_type("text/html") - |> send_file(200, "priv/static/index.html") + |> send_file(200, "priv_sid/static/index.html") end end end From d5f36acd13d9b6b958aa71cc9abf729ec165b1a5 Mon Sep 17 00:00:00 2001 From: SGSGB Date: Fri, 6 Jul 2018 18:59:07 +0200 Subject: [PATCH 002/317] emoji movement --- config/custom_emoji.txt | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 config/custom_emoji.txt diff --git a/config/custom_emoji.txt b/config/custom_emoji.txt new file mode 100644 index 000000000..49369749d --- /dev/null +++ b/config/custom_emoji.txt @@ -0,0 +1,84 @@ +shinitai-shi, /emoji/sgsgb/shinitai-shi.png +shinitai-ni, /emoji/sgsgb/shinitai-ni.png +shinitai-ta, /emoji/sgsgb/shinitai-ta.png +shinitai-i, /emoji/sgsgb/shinitai-i.png +shinitai_shi, /emoji/sgsgb/shinitai_shi.png +shinitai_ni, /emoji/sgsgb/shinitai_ni.png +shinitai_ta, /emoji/sgsgb/shinitai_ta.png +shinitai_i, /emoji/sgsgb/shinitai_i.png +poptepipic23, /emoji/sgsgb/poptepipic23.png +poptepipic22, /emoji/sgsgb/poptepipic22.png +poptepipic21, /emoji/sgsgb/poptepipic21.png +poptepipic20, /emoji/sgsgb/poptepipic20.png +poptepipic19, /emoji/sgsgb/poptepipic19.png +poptepipic18, /emoji/sgsgb/poptepipic18.png +poptepipic17, /emoji/sgsgb/poptepipic17.png +poptepipic16, /emoji/sgsgb/poptepipic16.png +poptepipic15, /emoji/sgsgb/poptepipic15.png +poptepipic14, /emoji/sgsgb/poptepipic14.png +poptepipic13, /emoji/sgsgb/poptepipic13.png +poptepipic12, /emoji/sgsgb/poptepipic12.png +poptepipic11, /emoji/sgsgb/poptepipic11.png +poptepipic10, /emoji/sgsgb/poptepipic10.png +poptepipic9, /emoji/sgsgb/poptepipic9.png +poptepipic8, /emoji/sgsgb/poptepipic8.png +poptepipic7, /emoji/sgsgb/poptepipic7.png +poptepipic6, /emoji/sgsgb/poptepipic6.png +poptepipic5, /emoji/sgsgb/poptepipic5.png +poptepipic4, /emoji/sgsgb/poptepipic4.png +poptepipic3, /emoji/sgsgb/poptepipic3.png +poptepipic2, /emoji/sgsgb/poptepipic2.png +poptepipic1, /emoji/sgsgb/poptepipic1.png +payyap, /emoji/sgsgb/payyap.png +orangeman, /emoji/sgsgb/orangeman.png +ohyes, /emoji/sgsgb/ohyes.png +ohgno, /emoji/sgsgb/ohgno.png +nyoron, /emoji/sgsgb/nyoron.png +nice, /emoji/sgsgb/nice.png +necromancy3, /emoji/sgsgb/necromancy3.png +necromancy2, /emoji/sgsgb/necromancy2.png +necromancy1, /emoji/sgsgb/necromancy1.png +moraleup, /emoji/sgsgb/moraleup.png +moraledn, /emoji/sgsgb/moraledn.png +megaman, /emoji/sgsgb/megaman.png +luck, /emoji/sgsgb/luck.png +lucknorm, /emoji/sgsgb/lucknorm.png +jyushimatsu_think, /emoji/sgsgb/jyushimatsu_think.png +jake, /emoji/sgsgb/jake.png +itisamystery, /emoji/sgsgb/itisamystery.png +italian2, /emoji/sgsgb/italian2.png +italian1, /emoji/sgsgb/italian1.png +herrington, /emoji/sgsgb/herrington.png +heavy_winninggreat, /emoji/sgsgb/heavy_winninggreat.png +heavy_losingeh, /emoji/sgsgb/heavy_losingeh.png +granddad, /emoji/sgsgb/granddad.png +furrythink, /emoji/sgsgb/furrythink.png +FACE_QUAD, /emoji/sgsgb/FACE_QUAD.png +dogcited, /emoji/sgsgb/dogcited.png +denton, /emoji/sgsgb/denton.png +denton_laugh, /emoji/sgsgb/denton_laugh.png +debian, /emoji/sgsgb/debian.png +catboythink, /emoji/sgsgb/catboythink.png +cacolaugh, /emoji/sgsgb/cacolaugh.png +bandits, /emoji/sgsgb/bandits.png +bananya, /emoji/sgsgb/bananya.png +a_to, /emoji/sgsgb/a_to.png +angrypirds, /emoji/sgsgb/angrypirds.png +Advanced_Sorcery, /emoji/sgsgb/Advanced_Sorcery.png +Yukkuri_Reimu, /emoji/sgsgb/Yukkuri_Reimu.png +vanpeek, /emoji/sgsgb/vanpeek.png +tux, /emoji/sgsgb/tux.png +tuturu, /emoji/sgsgb/tuturu.png +tobdog, /emoji/sgsgb/tobdog.png +thnk, /emoji/sgsgb/thnk.png +stalkers, /emoji/sgsgb/stalkers.png +spurdo, /emoji/sgsgb/spurdo.png +smug_marisa, /emoji/sgsgb/smug_marisa.png +shotging, /emoji/sgsgb/shotging.png +shag, /emoji/sgsgb/shag.png +serious, /emoji/sgsgb/serious.png +scut, /emoji/sgsgb/scut.png +sandro, /emoji/sgsgb/sandro.png +puke, /emoji/sgsgb/puke.png +kokoko, /emoji/sgsgb/kokoko.png +the_e, /emoji/sgsgb/e.png From 21e6c1dfcb05ceedc510a762f3cb471801d381fb Mon Sep 17 00:00:00 2001 From: SGSGB Date: Sat, 4 Aug 2018 18:23:11 +0200 Subject: [PATCH 003/317] customs --- config/config.exs | 6 +++--- config/custom_emoji.txt | 3 +++ config/emoji.txt | 8 -------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/config/config.exs b/config/config.exs index fa616bcce..5c3c1b0c6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -47,16 +47,16 @@ version = config :pleroma, :http, proxy_url: nil config :pleroma, :instance, - version: "2.1", + version: version, name: "Shigusegubu", email: "pleroma@hjkos.com", limit: 5000, - upload_limit: 16_000_000, + upload_limit: 20_000_000, registrations_open: true, federating: true, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, - quarantined_instances: [] + quarantined_instances: ["pleroma.rareome.ga"] config :pleroma, :activitypub, accept_blocks: true, diff --git a/config/custom_emoji.txt b/config/custom_emoji.txt index 49369749d..ed9e6b467 100644 --- a/config/custom_emoji.txt +++ b/config/custom_emoji.txt @@ -82,3 +82,6 @@ sandro, /emoji/sgsgb/sandro.png puke, /emoji/sgsgb/puke.png kokoko, /emoji/sgsgb/kokoko.png the_e, /emoji/sgsgb/e.png +avard_bios, /emoji/sgsgb/avard_bios.png +energy_star, /emoji/sgsgb/energy_star.png +loss, /emoji/sgsgb/loss.png diff --git a/config/emoji.txt b/config/emoji.txt index 3ea245a5b..7e43eed9b 100644 --- a/config/emoji.txt +++ b/config/emoji.txt @@ -28,11 +28,3 @@ f_33b00b, /emoji/f_33b00b.png f_33b22b, /emoji/f_33b22b.png f_33h, /emoji/f_33h.png f_33t, /emoji/f_33t.png -shinitai-shi, /emoji/shinitai-shi.png -shinitai-ni, /emoji/shinitai-ni.png -shinitai-ta, /emoji/shinitai-ta.png -shinitai-i, /emoji/shinitai-i.png -shinitai_shi, /emoji/shinitai_shi.png -shinitai_ni, /emoji/shinitai_ni.png -shinitai_ta, /emoji/shinitai_ta.png -shinitai_i, /emoji/shinitai_i.png From 5720cf286c3946531cf540296214f83f7b312e23 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 25 Sep 2018 15:56:43 +0300 Subject: [PATCH 004/317] changes --- config/config.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.exs b/config/config.exs index ad2218980..b95d74a6c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -69,13 +69,13 @@ config :pleroma, :instance, quarantined_instances: ["pleroma.rareome.ga"] config :pleroma, :fe, - theme: "pleroma-dark", - logo: "/static/logo.png", - background: "/static/aurora_borealis.jpg", + theme: "sigsegv2", + logo: "/static/logo.svg", + background: "/static/sigsegv_s.png", redirect_root_no_login: "/main/all", redirect_root_login: "/main/friends", show_instance_panel: true, - scope_options_enabled: false, + scope_options_enabled: true, collapse_message_with_subject: false config :pleroma, :activitypub, From 08ab82cf0b9c465603b7381cf9f1d9e7daf2037d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 12 Nov 2018 16:26:43 +0300 Subject: [PATCH 005/317] fix --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 05b2f0372..ae95ac101 100644 --- a/config/config.exs +++ b/config/config.exs @@ -93,7 +93,7 @@ config :pleroma, :instance, allow_relay: true, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, - quarantined_instances: ["pleroma.rareome.ga"] + quarantined_instances: ["pleroma.rareome.ga"], managed_config: true, allowed_post_formats: [ "text/plain", From 37aa9a63b07fc53cb7d035c5be8445f08c47bd1a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 21 Jan 2019 15:52:44 +0300 Subject: [PATCH 006/317] allow only plaintext --- config/config.exs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/config.exs b/config/config.exs index 94f546efc..077220752 100644 --- a/config/config.exs +++ b/config/config.exs @@ -132,9 +132,7 @@ config :pleroma, :instance, managed_config: true, static_dir: "instance/static/", allowed_post_formats: [ - "text/plain", - "text/html", - "text/markdown" + "text/plain" ], finmoji_enabled: true, mrf_transparency: true, From af8a7d8fd833ab015f87479480d646a2e4d16dc7 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 2 Mar 2019 20:25:21 +0200 Subject: [PATCH 007/317] limit --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 2238d8040..38a71fcb8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -141,7 +141,7 @@ config :pleroma, :instance, description: "SigSegV, a pleroma instance", limit: 5_000, remote_limit: 100_000, - upload_limit: 20_000_000, + upload_limit: 50_000_000, avatar_upload_limit: 2_000_000, background_upload_limit: 4_000_000, banner_upload_limit: 4_000_000, From f95b354c3d8016c74c6b1cddb829a3f3046bf24c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 3 Mar 2019 21:33:48 +0200 Subject: [PATCH 008/317] configs update --- config/config.exs | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/config/config.exs b/config/config.exs index 38a71fcb8..de93d9921 100644 --- a/config/config.exs +++ b/config/config.exs @@ -149,7 +149,10 @@ config :pleroma, :instance, federating: true, federation_reachability_timeout_days: 7, allow_relay: true, - rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, + rewrite_policy: [ + Pleroma.Web.ActivityPub.MRF.HellthreadPolicy, + Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy + ], public: true, quarantined_instances: ["pleroma.rareome.ga"], managed_config: true, @@ -178,41 +181,24 @@ config :pleroma, :markup, Pleroma.HTML.Scrubber.Default ] -# Deprecated, will be gone in 1.0 -config :pleroma, :fe, - theme: "sigsegv2", - logo: "/static/logo.svg", - background: "/static/sigsegv_s.png", - redirect_root_no_login: "/main/all", - redirect_root_login: "/main/friends", - logo_mask: true, - logo_margin: "0.1em", - show_instance_panel: true, - scope_options_enabled: false, - formatting_options_enabled: false, - collapse_message_with_subject: false, - hide_post_stats: false, - hide_user_stats: false, - scope_copy: false, - subject_line_behavior: "noop", - always_show_subject_input: false - config :pleroma, :frontend_configurations, pleroma_fe: %{ - theme: "pleroma-dark", - logo: "/static/logo.png", - background: "/images/city.jpg", + theme: "sigsegv2", + logo: "/static/logo.svg", + background: "/static/sigsegv_s.png", redirectRootNoLogin: "/main/all", redirectRootLogin: "/main/friends", + logoMask: true, + logoMargin: "0.1em", showInstanceSpecificPanel: true, scopeOptionsEnabled: false, formattingOptionsEnabled: false, collapseMessageWithSubject: false, hidePostStats: false, hideUserStats: false, - scopeCopy: true, - subjectLineBehavior: "email", - alwaysShowSubjectInput: true + scopeCopy: false, + subjectLineBehavior: "noop", + alwaysShowSubjectInput: false } config :pleroma, :activitypub, From 580787d3608d57fca02d6f254c29818d4f857262 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 21 Mar 2019 20:35:52 +0200 Subject: [PATCH 009/317] piece of shit --- installation/pleroma.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installation/pleroma.service b/installation/pleroma.service index c7e1dd534..dcbd25ec1 100644 --- a/installation/pleroma.service +++ b/installation/pleroma.service @@ -15,9 +15,9 @@ Environment="MIX_ENV=prod" ; Make sure that all paths fit your installation. ; Path to the home directory of the user running the Pleroma service. -Environment="HOME=/var/lib/pleroma" +Environment="HOME=/home/pleroma" ; Path to the folder containing the Pleroma installation. -WorkingDirectory=/opt/pleroma +WorkingDirectory=/home/pleroma/pleroma ; Path to the Mix binary. ExecStart=/usr/bin/mix phx.server From c7ccd43500be645f6bad208e7728558631fc26c1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 21 Mar 2019 20:39:46 +0200 Subject: [PATCH 010/317] shitfuck --- installation/pleroma.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installation/pleroma.service b/installation/pleroma.service index dcbd25ec1..4ff4b7811 100644 --- a/installation/pleroma.service +++ b/installation/pleroma.service @@ -25,7 +25,7 @@ ExecStart=/usr/bin/mix phx.server ; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops. PrivateTmp=true ; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Pleroma user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false. -ProtectHome=true +ProtectHome=false ; Mount /usr, /boot, and /etc as read-only for processes invoked by this service. ProtectSystem=full ; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi. From 4069f6430dac018a731c00916ba0f6fccbe22c1c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 7 Apr 2019 12:13:49 +0300 Subject: [PATCH 011/317] update mrf --- config/config.exs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 0e3ab043f..62d85d7ce 100644 --- a/config/config.exs +++ b/config/config.exs @@ -166,6 +166,7 @@ config :pleroma, :instance, federation_reachability_timeout_days: 7, allow_relay: true, rewrite_policy: [ + Pleroma.Web.ActivityPub.MRF.SimplePolicy, Pleroma.Web.ActivityPub.MRF.HellthreadPolicy, Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy ], @@ -241,8 +242,16 @@ config :pleroma, :mrf_hellthread, config :pleroma, :mrf_simple, media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], + media_nsfw: [ + "preteengirls.biz", + "melalandia.tk", + "pl.smuglo.li", + "baraag.net" + ], + federated_timeline_removal: [ + "preteengirls.biz", + "melalandia.tk" + ], reject: [], accept: [] From 232dd793419e16ab27b1d314909bbfed240d387f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 28 Apr 2019 18:31:37 +0300 Subject: [PATCH 012/317] updates --- config/config.exs | 4 +- config/custom_emoji.txt | 192 +++++++++++++++++++++++----------------- 2 files changed, 112 insertions(+), 84 deletions(-) diff --git a/config/config.exs b/config/config.exs index 450db428a..de642a748 100644 --- a/config/config.exs +++ b/config/config.exs @@ -205,7 +205,7 @@ config :pleroma, :instance, description: "SigSegV, a pleroma instance", limit: 5_000, remote_limit: 100_000, - upload_limit: 50_000_000, + upload_limit: 200_000_000, avatar_upload_limit: 2_000_000, background_upload_limit: 4_000_000, banner_upload_limit: 4_000_000, @@ -223,7 +223,7 @@ config :pleroma, :instance, managed_config: true, static_dir: "instance/static/", allowed_post_formats: [ - "text/plain" + "text/plain", "text/bbcode" ], mrf_transparency: true, diff --git a/config/custom_emoji.txt b/config/custom_emoji.txt index ed9e6b467..c7a706630 100644 --- a/config/custom_emoji.txt +++ b/config/custom_emoji.txt @@ -1,87 +1,115 @@ -shinitai-shi, /emoji/sgsgb/shinitai-shi.png -shinitai-ni, /emoji/sgsgb/shinitai-ni.png -shinitai-ta, /emoji/sgsgb/shinitai-ta.png -shinitai-i, /emoji/sgsgb/shinitai-i.png -shinitai_shi, /emoji/sgsgb/shinitai_shi.png -shinitai_ni, /emoji/sgsgb/shinitai_ni.png -shinitai_ta, /emoji/sgsgb/shinitai_ta.png -shinitai_i, /emoji/sgsgb/shinitai_i.png -poptepipic23, /emoji/sgsgb/poptepipic23.png -poptepipic22, /emoji/sgsgb/poptepipic22.png -poptepipic21, /emoji/sgsgb/poptepipic21.png -poptepipic20, /emoji/sgsgb/poptepipic20.png -poptepipic19, /emoji/sgsgb/poptepipic19.png -poptepipic18, /emoji/sgsgb/poptepipic18.png -poptepipic17, /emoji/sgsgb/poptepipic17.png -poptepipic16, /emoji/sgsgb/poptepipic16.png -poptepipic15, /emoji/sgsgb/poptepipic15.png -poptepipic14, /emoji/sgsgb/poptepipic14.png -poptepipic13, /emoji/sgsgb/poptepipic13.png -poptepipic12, /emoji/sgsgb/poptepipic12.png -poptepipic11, /emoji/sgsgb/poptepipic11.png -poptepipic10, /emoji/sgsgb/poptepipic10.png -poptepipic9, /emoji/sgsgb/poptepipic9.png -poptepipic8, /emoji/sgsgb/poptepipic8.png -poptepipic7, /emoji/sgsgb/poptepipic7.png -poptepipic6, /emoji/sgsgb/poptepipic6.png -poptepipic5, /emoji/sgsgb/poptepipic5.png -poptepipic4, /emoji/sgsgb/poptepipic4.png -poptepipic3, /emoji/sgsgb/poptepipic3.png -poptepipic2, /emoji/sgsgb/poptepipic2.png -poptepipic1, /emoji/sgsgb/poptepipic1.png -payyap, /emoji/sgsgb/payyap.png -orangeman, /emoji/sgsgb/orangeman.png -ohyes, /emoji/sgsgb/ohyes.png -ohgno, /emoji/sgsgb/ohgno.png -nyoron, /emoji/sgsgb/nyoron.png -nice, /emoji/sgsgb/nice.png -necromancy3, /emoji/sgsgb/necromancy3.png -necromancy2, /emoji/sgsgb/necromancy2.png -necromancy1, /emoji/sgsgb/necromancy1.png -moraleup, /emoji/sgsgb/moraleup.png -moraledn, /emoji/sgsgb/moraledn.png -megaman, /emoji/sgsgb/megaman.png -luck, /emoji/sgsgb/luck.png -lucknorm, /emoji/sgsgb/lucknorm.png -jyushimatsu_think, /emoji/sgsgb/jyushimatsu_think.png -jake, /emoji/sgsgb/jake.png -itisamystery, /emoji/sgsgb/itisamystery.png -italian2, /emoji/sgsgb/italian2.png -italian1, /emoji/sgsgb/italian1.png -herrington, /emoji/sgsgb/herrington.png -heavy_winninggreat, /emoji/sgsgb/heavy_winninggreat.png -heavy_losingeh, /emoji/sgsgb/heavy_losingeh.png -granddad, /emoji/sgsgb/granddad.png -furrythink, /emoji/sgsgb/furrythink.png +Advanced_Sorcery, /emoji/sgsgb/Advanced_Sorcery.png FACE_QUAD, /emoji/sgsgb/FACE_QUAD.png -dogcited, /emoji/sgsgb/dogcited.png -denton, /emoji/sgsgb/denton.png -denton_laugh, /emoji/sgsgb/denton_laugh.png -debian, /emoji/sgsgb/debian.png -catboythink, /emoji/sgsgb/catboythink.png -cacolaugh, /emoji/sgsgb/cacolaugh.png -bandits, /emoji/sgsgb/bandits.png -bananya, /emoji/sgsgb/bananya.png +Yukkuri_Reimu, /emoji/sgsgb/Yukkuri_Reimu.png a_to, /emoji/sgsgb/a_to.png angrypirds, /emoji/sgsgb/angrypirds.png -Advanced_Sorcery, /emoji/sgsgb/Advanced_Sorcery.png -Yukkuri_Reimu, /emoji/sgsgb/Yukkuri_Reimu.png -vanpeek, /emoji/sgsgb/vanpeek.png -tux, /emoji/sgsgb/tux.png -tuturu, /emoji/sgsgb/tuturu.png -tobdog, /emoji/sgsgb/tobdog.png -thnk, /emoji/sgsgb/thnk.png -stalkers, /emoji/sgsgb/stalkers.png -spurdo, /emoji/sgsgb/spurdo.png -smug_marisa, /emoji/sgsgb/smug_marisa.png -shotging, /emoji/sgsgb/shotging.png -shag, /emoji/sgsgb/shag.png -serious, /emoji/sgsgb/serious.png -scut, /emoji/sgsgb/scut.png -sandro, /emoji/sgsgb/sandro.png -puke, /emoji/sgsgb/puke.png -kokoko, /emoji/sgsgb/kokoko.png -the_e, /emoji/sgsgb/e.png -avard_bios, /emoji/sgsgb/avard_bios.png +annoying_dog_hole, /emoji/sgsgb/annoying_dog_hole.png +award_bios, /emoji/sgsgb/award_bios.png +bananya, /emoji/sgsgb/bananya.png +bandits, /emoji/sgsgb/bandits.png +blobcatknife, /emoji/sgsgb/blobcatknife.png +cacolaugh, /emoji/sgsgb/cacolaugh.png +catboythink, /emoji/sgsgb/catboythink.png +cirno_run, /emoji/sgsgb/cirno_run.png +debian, /emoji/sgsgb/debian.png +denton, /emoji/sgsgb/denton.png +denton_laugh, /emoji/sgsgb/denton_laugh.png +dio, /emoji/sgsgb/dio.png +dogcited, /emoji/sgsgb/dogcited.png +dong, /emoji/sgsgb/dong.png +duane, /emoji/sgsgb/duane.png +e, /emoji/sgsgb/e.png energy_star, /emoji/sgsgb/energy_star.png +flynn_smirk, /emoji/sgsgb/flynn_smirk.png +furrythink, /emoji/sgsgb/furrythink.png +granddad, /emoji/sgsgb/granddad.png +heavy_losingeh, /emoji/sgsgb/heavy_losingeh.png +heavy_winninggreat, /emoji/sgsgb/heavy_winninggreat.png +herrington, /emoji/sgsgb/herrington.png +hogan, /emoji/sgsgb/hogan.png +hyperjoy, /emoji/sgsgb/hyperjoy.png +itisamystery, /emoji/sgsgb/itisamystery.png +jacobson, /emoji/sgsgb/jacobson.png +jake, /emoji/sgsgb/jake.png +jyushimatsu_think, /emoji/sgsgb/jyushimatsu_think.png +kinzo, /emoji/sgsgb/kinzo.png +kms, /emoji/sgsgb/kms.png +kokoko, /emoji/sgsgb/kokoko.png loss, /emoji/sgsgb/loss.png +luck, /emoji/sgsgb/luck.png +lucknorm, /emoji/sgsgb/lucknorm.png +marko, /emoji/sgsgb/marko.png +megaman, /emoji/sgsgb/megaman.png +miyanozoom, /emoji/sgsgb/miyanozoom.png +monster_energy_ultra, /emoji/sgsgb/monster_energy_ultra.png +moraledn, /emoji/sgsgb/moraledn.png +moraleup, /emoji/sgsgb/moraleup.png +multiply, /emoji/sgsgb/multiply.png +mycomputer, /emoji/sgsgb/mycomputer.png +necromancy1, /emoji/sgsgb/necromancy1.png +necromancy2, /emoji/sgsgb/necromancy2.png +necromancy3, /emoji/sgsgb/necromancy3.png +nice, /emoji/sgsgb/nice.png +nyoron, /emoji/sgsgb/nyoron.png +ohgno, /emoji/sgsgb/ohgno.png +ohyes, /emoji/sgsgb/ohyes.png +orangeman, /emoji/sgsgb/orangeman.png +payyap, /emoji/sgsgb/payyap.png +pooh, /emoji/sgsgb/pooh.png +poohington, /emoji/sgsgb/poohington.png +poptepipic1, /emoji/sgsgb/poptepipic1.png +poptepipic10, /emoji/sgsgb/poptepipic10.png +poptepipic11, /emoji/sgsgb/poptepipic11.png +poptepipic12, /emoji/sgsgb/poptepipic12.png +poptepipic13, /emoji/sgsgb/poptepipic13.png +poptepipic14, /emoji/sgsgb/poptepipic14.png +poptepipic15, /emoji/sgsgb/poptepipic15.png +poptepipic16, /emoji/sgsgb/poptepipic16.png +poptepipic17, /emoji/sgsgb/poptepipic17.png +poptepipic18, /emoji/sgsgb/poptepipic18.png +poptepipic19, /emoji/sgsgb/poptepipic19.png +poptepipic2, /emoji/sgsgb/poptepipic2.png +poptepipic20, /emoji/sgsgb/poptepipic20.png +poptepipic21, /emoji/sgsgb/poptepipic21.png +poptepipic22, /emoji/sgsgb/poptepipic22.png +poptepipic23, /emoji/sgsgb/poptepipic23.png +poptepipic3, /emoji/sgsgb/poptepipic3.png +poptepipic4, /emoji/sgsgb/poptepipic4.png +poptepipic5, /emoji/sgsgb/poptepipic5.png +poptepipic6, /emoji/sgsgb/poptepipic6.png +poptepipic7, /emoji/sgsgb/poptepipic7.png +poptepipic8, /emoji/sgsgb/poptepipic8.png +poptepipic9, /emoji/sgsgb/poptepipic9.png +puke, /emoji/sgsgb/puke.png +quake_net, /emoji/sgsgb/quake_net.png +quake_ram, /emoji/sgsgb/quake_ram.png +ranger, /emoji/sgsgb/ranger.png +ranger_none, /emoji/sgsgb/ranger_none.png +risitas1, /emoji/sgsgb/risitas1.png +risitas2, /emoji/sgsgb/risitas2.png +rokalife, /emoji/sgsgb/rokalife.png +sandro, /emoji/sgsgb/sandro.png +scut, /emoji/sgsgb/scut.png +serious, /emoji/sgsgb/serious.png +shag, /emoji/sgsgb/shag.png +shinitai_i, /emoji/sgsgb/shinitai_i.png +shinitai_ni, /emoji/sgsgb/shinitai_ni.png +shinitai_shi, /emoji/sgsgb/shinitai_shi.png +shinitai_ta, /emoji/sgsgb/shinitai_ta.png +shotging, /emoji/sgsgb/shotging.png +skelethor, /emoji/sgsgb/skelethor.png +smug_marisa, /emoji/sgsgb/smug_marisa.png +spurdo, /emoji/sgsgb/spurdo.png +stalkers, /emoji/sgsgb/stalkers.png +tenshi_eating_corndog, /emoji/sgsgb/tenshi_eating_corndog.png +thinkingwat, /emoji/sgsgb/thinkingwat.png +thnk, /emoji/sgsgb/thnk.png +tobdog, /emoji/sgsgb/tobdog.png +todd, /emoji/sgsgb/todd.png +trash, /emoji/sgsgb/trash.png +tuturu, /emoji/sgsgb/tuturu.png +tux, /emoji/sgsgb/tux.png +uwot, /emoji/sgsgb/uwot.png +uwot2, /emoji/sgsgb/uwot2.png +uwot3, /emoji/sgsgb/uwot3.png +vanpeek, /emoji/sgsgb/vanpeek.png From a506b7e1d7731e0e4011aa10278c6d0b7f8faeef Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 4 May 2019 15:28:27 +0300 Subject: [PATCH 013/317] undo priv_sid hacks --- lib/pleroma/plugs/instance_static.ex | 2 +- lib/pleroma/web/endpoint.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex index c63a9c95e..a64f1ea80 100644 --- a/lib/pleroma/plugs/instance_static.ex +++ b/lib/pleroma/plugs/instance_static.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Plugs.InstanceStatic do if File.exists?(instance_path) do instance_path else - Path.join(Application.app_dir(:pleroma, "priv_sid/static/"), path) + Path.join(Application.app_dir(:pleroma, "priv/static/"), path) end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 7d1453a49..7f939991d 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.Endpoint do plug( Plug.Static, at: "/", - from: "priv_sid/static", + from: :pleroma, only: ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc) # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength From 733b53748c4813a9ceb60c321c68a7fb0d8b8e98 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 5 May 2019 23:57:11 +0300 Subject: [PATCH 014/317] more mrf --- config/config.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index de642a748..92de2dbd1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -295,7 +295,8 @@ config :pleroma, :mrf_simple, "preteengirls.biz", "melalandia.tk", "pl.smuglo.li", - "baraag.net" + "baraag.net", + "humblr.social" ], federated_timeline_removal: [ "preteengirls.biz", From 239f4d56bd308ebf9dd3150252144ae6489086bf Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 9 Feb 2020 21:30:43 +0200 Subject: [PATCH 015/317] update MRF --- config/config.exs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 87f48c17f..6e102333d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -369,11 +369,18 @@ config :pleroma, :mrf_simple, "melalandia.tk", "pl.smuglo.li", "baraag.net", - "humblr.social" + "humblr.social", + "stereophonic.space", + "pawoo.net", + "sinblr.com" ], federated_timeline_removal: [ "preteengirls.biz", - "melalandia.tk" + "melalandia.tk", + "pawoo.net", + "baraag.net", + "humblr.social", + "sinblr.com" ], report_removal: [], reject: [], From dfad261302dcb4b73af8e93e8d85eb5084635c67 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 29 Feb 2020 15:22:32 +0200 Subject: [PATCH 016/317] fix bullshit new default --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index d9262d039..b3ee7ce09 100644 --- a/config/config.exs +++ b/config/config.exs @@ -245,7 +245,7 @@ config :pleroma, :instance, mrf_transparency_exclusions: [], autofollowed_nicknames: [], max_pinned_statuses: 1, - attachment_links: false, + attachment_links: true, welcome_user_nickname: nil, welcome_message: nil, max_report_comment_size: 1000, From 2a06b66daf6d00b741711471ff41fc6b08d67216 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 22 May 2020 00:33:59 +0300 Subject: [PATCH 017/317] fix --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 2cf1f7f43..ab8ac740f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -274,7 +274,7 @@ config :pleroma, :markup, config :pleroma, :frontend_configurations, pleroma_fe: %{ - alwaysShowSubjectInput: false + alwaysShowSubjectInput: false, background: "/static/sigsegv_s.png", collapseMessageWithSubject: false, disableChat: false, From f20479ded8c60d58ee8b11deb5e6194dbf92b836 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 22 May 2020 00:37:24 +0300 Subject: [PATCH 018/317] fix --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index ab8ac740f..cb49583e9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -288,7 +288,7 @@ config :pleroma, :frontend_configurations, logo: "/static/logo.svg", logoMargin: ".1em", logoMask: true, - minimalScopesMode: false, + minimalScopesMode: true, noAttachmentLinks: false, nsfwCensorImage: "", postContentType: "text/plain", From 080416f175b4a1562b8a1709938f64e89c82b206 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 27 Jul 2020 22:09:52 +0300 Subject: [PATCH 019/317] demilitarize --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index ad943de69..07182c7f1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -172,7 +172,7 @@ config :mime, :types, %{ "application/ld+json" => ["activity+json"] } -config :tesla, adapter: Tesla.Adapter.Gun +config :tesla, adapter: Tesla.Adapter.Hackney # Configures http settings, upstream proxy etc. config :pleroma, :http, From 34272773b04f022839f9e557f36e41d5030d77e6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 28 Jul 2020 21:51:59 +0300 Subject: [PATCH 020/317] important shit --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 07182c7f1..d738ca34c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -222,7 +222,7 @@ config :pleroma, :instance, ], autofollowed_nicknames: [], max_pinned_statuses: 1, - attachment_links: false, + attachment_links: true, max_report_comment_size: 1000, safe_dm_mentions: false, healthcheck: false, From 6d93391deecb595540af326a0a057b7da7a82cb2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 31 Jul 2020 11:29:52 +0300 Subject: [PATCH 021/317] power time --- config/config.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index d738ca34c..949983fb3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -723,7 +723,8 @@ config :pleroma, :mrf, policies: [ Pleroma.Web.ActivityPub.MRF.SimplePolicy, Pleroma.Web.ActivityPub.MRF.HellthreadPolicy, - Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy + Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy, + Pleroma.Web.ActivityPub.MRF.TagPolicy ], transparency: true, transparency_exclusions: [] From 461c465fe78d6a6e121a184cafa0730f81a42df8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 18 Sep 2020 13:39:01 +0300 Subject: [PATCH 022/317] fix filenameposting --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 3533ff6ab..19055e465 100644 --- a/config/config.exs +++ b/config/config.exs @@ -73,7 +73,7 @@ config :pleroma, Pleroma.Upload, ] ], filename_display_max_length: 30, - default_description: nil + default_description: :filename config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" From a756ec71a78a1540a1c3cb3491a2e5c5b00584e0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 12 Apr 2021 00:38:25 +0300 Subject: [PATCH 023/317] try to fix ruffle on chrome --- lib/pleroma/web/plugs/http_security_plug.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index 0025b042a..1dcf02e1a 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -116,7 +116,9 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do if Config.get(:env) == :dev do "script-src 'self' 'unsafe-eval'" else - "script-src 'self'" + # TODO right now unsafe-eval is needed for WASM to load in chrome + # see: https://github.com/WebAssembly/content-security-policy/issues/7 + "script-src 'self' 'unsafe-eval'" end report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] From d860aa32f70e26668bc0f07611db27c1875b32d8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 6 Apr 2022 11:16:53 +0300 Subject: [PATCH 024/317] revert to default CSP for testing --- 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 27ba83ec2..ab7c55f6e 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -119,7 +119,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do else # TODO right now unsafe-eval is needed for WASM to load in chrome # see: https://github.com/WebAssembly/content-security-policy/issues/7 - "script-src 'self' 'unsafe-eval'" + "script-src 'self'" end report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] From 0613478bdb56b4a8592deb004a4a1b6000edca87 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 Apr 2022 17:56:26 +0300 Subject: [PATCH 025/317] engage db congfiguration --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index eb9aa35df..0ca995ebb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -783,7 +783,7 @@ config :pleroma, :web_cache_ttl, config :pleroma, :modules, runtime_dir: "instance/modules" -config :pleroma, configurable_from_database: false +config :pleroma, configurable_from_database: true config :pleroma, Pleroma.Repo, parameters: [gin_fuzzy_search_limit: "500"], From aa38f1da418a1bb423e06963aad0fed53ab9524b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 Apr 2022 18:16:30 +0300 Subject: [PATCH 026/317] resolve deprecation warnings in conf --- config/config.exs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/config/config.exs b/config/config.exs index 0ca995ebb..500a9834b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -214,7 +214,7 @@ config :pleroma, :instance, ], allow_relay: true, public: true, - quarantined_instances: ["pleroma.rareome.ga"], + quarantined_instances: [{ "pleroma.rareome.ga", "leaks private posts or sumshit i dont rember" }], static_dir: "instance/static/", allowed_post_formats: [ "text/plain", @@ -379,22 +379,21 @@ config :pleroma, :mrf_hellthread, config :pleroma, :mrf_simple, media_removal: [], media_nsfw: [ - "preteengirls.biz", - "melalandia.tk", - "pl.smuglo.li", - "baraag.net", - "humblr.social", - "stereophonic.space", - "pawoo.net", - "sinblr.com" + { "preteengirls.biz", "pron or even cp" }, + { "melalandia.tk", "i don't rember" }, + { "pl.smuglo.li", "instance is dead but i still remember, rip smuglo" }, + { "baraag.net", "pron" }, + { "humblr.social", "3dpd pron" } + { "pawoo.net", "tasteful pron, mostly." }, + { "sinblr.com", "3dpd pron" } ], federated_timeline_removal: [ - "preteengirls.biz", - "melalandia.tk", - "pawoo.net", - "baraag.net", - "humblr.social", - "sinblr.com" + { "preteengirls.biz", "pron or even cp" }, + { "melalandia.tk", "i don't rember" }, + { "baraag.net", "pron" }, + { "humblr.social", "3dpd pron" } + { "pawoo.net", "tasteful pron, mostly." }, + { "sinblr.com", "3dpd pron" } ], report_removal: [], reject: [], From d08362cb30725e506651995344ae327771cc3cb5 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 Apr 2022 18:17:25 +0300 Subject: [PATCH 027/317] hups --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 500a9834b..94efa40a8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -383,7 +383,7 @@ config :pleroma, :mrf_simple, { "melalandia.tk", "i don't rember" }, { "pl.smuglo.li", "instance is dead but i still remember, rip smuglo" }, { "baraag.net", "pron" }, - { "humblr.social", "3dpd pron" } + { "humblr.social", "3dpd pron" }, { "pawoo.net", "tasteful pron, mostly." }, { "sinblr.com", "3dpd pron" } ], From 46c4d3b80f1e95c85f1ff631d56d2aa577a780d2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 Apr 2022 18:17:41 +0300 Subject: [PATCH 028/317] huuups --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 94efa40a8..a81b70382 100644 --- a/config/config.exs +++ b/config/config.exs @@ -391,7 +391,7 @@ config :pleroma, :mrf_simple, { "preteengirls.biz", "pron or even cp" }, { "melalandia.tk", "i don't rember" }, { "baraag.net", "pron" }, - { "humblr.social", "3dpd pron" } + { "humblr.social", "3dpd pron" }, { "pawoo.net", "tasteful pron, mostly." }, { "sinblr.com", "3dpd pron" } ], From f72beb6c2625331877872515d69d13f63a944b8e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 10 Mar 2023 00:08:08 +0200 Subject: [PATCH 029/317] testing newer pot --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 8aa18d5ee..d225c9f7c 100644 --- a/mix.exs +++ b/mix.exs @@ -179,7 +179,7 @@ defmodule Pleroma.Mixfile do {:recon, "~> 2.5"}, {:joken, "~> 2.0"}, {:benchee, "~> 1.0"}, - {:pot, "~> 1.0"}, + {:pot, "~> 1.0.2"}, {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, {:ex_const, "~> 0.2"}, {:plug_static_index_html, "~> 1.0.0"}, From ece1f7bca65e8c97ff57b6594a88a3e16cd0611d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 20 Mar 2023 21:29:24 +0200 Subject: [PATCH 030/317] Revert "testing newer pot" This reverts commit f72beb6c2625331877872515d69d13f63a944b8e. --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index d225c9f7c..8aa18d5ee 100644 --- a/mix.exs +++ b/mix.exs @@ -179,7 +179,7 @@ defmodule Pleroma.Mixfile do {:recon, "~> 2.5"}, {:joken, "~> 2.0"}, {:benchee, "~> 1.0"}, - {:pot, "~> 1.0.2"}, + {:pot, "~> 1.0"}, {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, {:ex_const, "~> 0.2"}, {:plug_static_index_html, "~> 1.0.0"}, From f39a86344cfc5504b210da70cba46c194f03e535 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 16:41:30 -0600 Subject: [PATCH 031/317] 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields 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 4c90f39e144841d8020fc063cee07d8f7acf158d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 17:30:49 -0600 Subject: [PATCH 032/317] 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 2c17d29c4b4070fe3db81c61ae33bbdea89a25d2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 18:03:22 -0600 Subject: [PATCH 033/317] 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{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 f01e2d090208e1db9c537fad0be8f452b29b54ce Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 18:46:58 -0600 Subject: [PATCH 034/317] 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 @@ defmodule HttpRequestMock do }} 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 @@ defmodule HttpRequestMock do }} 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 5fdf4d1b1cef53033b672d971782283196891ca7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 19:14:39 -0600 Subject: [PATCH 035/317] 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> 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 d10408558e4cf487a177aad9c8694770a0600f49 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 19:47:08 -0600 Subject: [PATCH 036/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 f95cad4603365e49fa002ccc32a54b1b91d1e81c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 20:05:58 -0600 Subject: [PATCH 037/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 fe5b8cbe05df3cb6b112295f3d32228bc39eb0a4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 21:27:05 -0600 Subject: [PATCH 038/317] 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 9af635da8..2fb63d13a 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do 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 a938a96ae80e25b386638ab1adbea0d2b9520180 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:15:54 -0600 Subject: [PATCH 039/317] 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 @@ defmodule Pleroma.Web.ActivityPub.Builder 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 @@ defmodule Pleroma.Web.ActivityPub.Builder 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 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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 968e11a14..d26dfd6b8 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -778,6 +778,18 @@ defmodule Pleroma.Web.CommonAPITest 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 bc2ffd0c1697f27e29331bf2b7b5e7b8b0dffc9e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:29:13 -0600 Subject: [PATCH 040/317] 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 @@ defmodule Pleroma.Web.ActivityPub.BuilderTest 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 b9c10c61b715daa6eb37328e84aed3bdd4c56328 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:35:08 -0600 Subject: [PATCH 041/317] 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 4f434cb69..f38557c15 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 efd6d40a40611a049b95afe94e8276f54f0b852a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:41:57 -0600 Subject: [PATCH 042/317] 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 57e4e4304246def26098ad5fe473fbe8a7b42fcf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:57:42 -0600 Subject: [PATCH 043/317] 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 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do nullable: true, allOf: [FlakeID], description: "ID of the status being quoted, if any" - }, + } }, example: %{ "status" => "What time is it?", From 72a0236c9acd80a54536ecac509d0b92fdb9aa70 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:02:44 -0600 Subject: [PATCH 044/317] 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest 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 d26dfd6b8..99c1a317f 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -779,7 +779,7 @@ defmodule Pleroma.Web.CommonAPITest 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 e47c6a24453ee23a5764247849a8ace292913b68 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:09:33 -0600 Subject: [PATCH 045/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do "multifetch", "pleroma:api/v1/notifications:include_types_filter", "editing", + "quote_posting", if Config.get([:activitypub, :blockers_visible]) do "blockers_visible" end, From 2f9a098dd597edb16f4fab7809d936a04ae6cabb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:29:55 -0600 Subject: [PATCH 046/317] @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 4bcdf0cf4fe6c9084ffb107e5fdf9ca8d02b39c7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 13:55:25 -0600 Subject: [PATCH 047/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 f38557c15..7c9ac3147 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 cca63d707db58ce5eaea96ac2962418bf3df5740 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 15:46:44 -0600 Subject: [PATCH 048/317] 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 2fb63d13a..750750b7a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -137,11 +137,11 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft 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 99c1a317f..ea5aa8c7f 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -789,6 +789,18 @@ defmodule Pleroma.Web.CommonAPITest 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 a3b7c1da4f800ef0d46bb8a48a382c3d344ef9dc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 16:03:46 -0600 Subject: [PATCH 049/317] 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 750750b7a..b7ea75747 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do 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 ea5aa8c7f..2770ac51b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -798,7 +798,9 @@ defmodule Pleroma.Web.CommonAPITest 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 06cc4ad7fd555356bf364ade5d0c211f84fb30ce Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 15:34:23 -0600 Subject: [PATCH 050/317] 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 1a6ab18ebcb7c2b8372661b83e9d630aa016955d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 16:44:35 -0600 Subject: [PATCH 051/317] 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_object_age, 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 6d26ce54c4c5c9e8b4832fa1e7521990cce86b98 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:21:49 -0600 Subject: [PATCH 052/317] 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 b7ea75747..82f561e92 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft 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 2770ac51b..afd9e980a 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -804,6 +804,20 @@ defmodule Pleroma.Web.CommonAPITest 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 14a26e4433738eebef7c576d1537c46344d231e0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:49:31 -0600 Subject: [PATCH 053/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 029a1045d4fd2cde9bd12d42b003a9e5401c1461 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:52:50 -0600 Subject: [PATCH 054/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 63f8c3335358271fe62ed0d8f0facd6b37171115 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Jan 2022 14:28:06 -0600 Subject: [PATCH 055/317] 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest 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 1cb39bfb90b952eea605b713a1adddec8119eb4b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Jan 2022 15:01:20 -0600 Subject: [PATCH 056/317] 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, :restrict_unauthenticated, 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 f8b420932ec7231cad03f3519bc46a465b8c02d1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 12:33:07 -0600 Subject: [PATCH 057/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 f89874596e4bb4108c0574540f574d5aad26288b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 14:06:32 -0600 Subject: [PATCH 058/317] 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 32e284ed2c5379b0dcedd4fa4d512a5ad63435b7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 15:55:52 -0600 Subject: [PATCH 059/317] 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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/107699333438289729

", + "contentMap": { + "ja": "

美味しそう
QT: https://fedibird.com/@yamako/107699333438289729

" + }, + "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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 5df951f670b5fa33d3cf44049d4443501fe9c0a8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 16:07:17 -0600 Subject: [PATCH 060/317] 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest 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 34cf4222c1d8acf291e114f10c508bbd31f495e0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 17:53:19 -0600 Subject: [PATCH 061/317] 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert modified["object"]["quoteUrl"] == quote_id assert modified["object"]["quoteUri"] == quote_id - assert modified["object"]["_misskey_quote"] == quote_id end end From 52c81cdf6829bd6b5de73fa5856cf059ef57f73c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 30 Jan 2022 10:57:29 -0600 Subject: [PATCH 062/317] 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy 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/107712176849067434

", + "contentMap": { + "ja": "

揺れていたようだ
QT: https://unnerv.jp/@UN_NERV/107712176849067434

" + }, + "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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest 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 44cea92fbc5f61718f47102da818c7d0394926c9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 17:57:09 -0400 Subject: [PATCH 063/317] 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> 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 3bb384d37826b09f5bde8e07ec45f22ba9544ad6 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 18:27:23 -0400 Subject: [PATCH 064/317] 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 82f561e92..c80c4940e 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -167,9 +167,21 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft 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 ebcac09f5c55d5a4379b46db871384818eabea7f Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 18:28:13 -0400 Subject: [PATCH 065/317] 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 26b499eca00e5a505e57d68abb1ddcc56e9ee8f8 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 19:43:18 -0400 Subject: [PATCH 066/317] 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 afd9e980a..04ac01c79 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -807,16 +807,23 @@ defmodule Pleroma.Web.CommonAPITest 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 2c70857f9fc3246b5d754237bca31446dfd5e8cc Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 09:30:43 -0400 Subject: [PATCH 067/317] 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_object_age, 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 @@ config :pleroma, :config_description, [ } ] }, + %{ + 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest 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 "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 post

https://gleasonator.com/objects/1234's quoting
" end test "doesn't add line breaks to markdown posts" do @@ -41,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest 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 8f252fd99b2d974162c636fd4936dd9b186339e5 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 11:09:10 -0400 Subject: [PATCH 068/317] 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 7b4fd03b6..2a65bd50e 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -81,4 +81,11 @@ defmodule Pleroma.Constants do const(mime_regex, do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/ ) + + 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest 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 f8b01788ebe88a4e10634af74ca761edac7886a4 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:08:24 -0400 Subject: [PATCH 069/317] 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest 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 05beada21b6b4c33e413dc6936303a8f761eb7ff Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:27:29 -0400 Subject: [PATCH 070/317] 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 2a65bd50e..3193cf8f0 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -82,6 +82,10 @@ defmodule Pleroma.Constants do do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/ ) + 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes 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 01eafc037243d1ceabdf4b707a24bdf28cbc8615 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:37:12 -0400 Subject: [PATCH 071/317] 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @impl true def describe, do: {:ok, %{}} + @impl Pleroma.Web.ActivityPub.MRF.Policy + def history_awareness, do: :auto + @impl true def config_description do %{ From fca6a7933e963655661f1bdd9c542a4748df6de2 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:58:20 -0400 Subject: [PATCH 072/317] 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 44eb64817926021b45c77455a22db7691d570639 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 22:07:16 -0400 Subject: [PATCH 073/317] 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 @@ config :pleroma, :config_description, [ } ] }, - %{ - 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy 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 5ebabcd582d711fe395c3dfe1ba54861f8f46371 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:29:23 -0400 Subject: [PATCH 074/317] 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 c80c4940e..0ae3dfe76 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -139,9 +139,6 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do 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 04ac01c79..e49ef1c10 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -790,8 +790,8 @@ defmodule Pleroma.Web.CommonAPITest 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 2436c9d61de88927d7402e26865ed0fceec70732 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:47:31 -0400 Subject: [PATCH 075/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 bffa258a23f1f963e0706a4615511351b22ad771 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:56:54 -0400 Subject: [PATCH 076/317] 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 a5048c270ed36c5d9d31c83b8f00a135c0a2483f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 4 Sep 2023 19:39:51 +0300 Subject: [PATCH 077/317] oops --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 4d4c0b13e..55cd4a008 100644 --- a/config/config.exs +++ b/config/config.exs @@ -877,7 +877,7 @@ config :pleroma, :mrf, Pleroma.Web.ActivityPub.MRF.SimplePolicy, Pleroma.Web.ActivityPub.MRF.HellthreadPolicy, Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy, - Pleroma.Web.ActivityPub.MRF.TagPolicy + Pleroma.Web.ActivityPub.MRF.TagPolicy, Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy ], From 337d047c4dedaa753e381b80524beb43924dc5cd Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 29 Oct 2023 18:58:57 +0200 Subject: [PATCH 078/317] embed favicon on backend and make it configurable --- config/config.exs | 1 + config/description.exs | 7 +++++++ lib/pleroma/web/fallback/redirect_controller.ex | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index e8ae31542..2b2583671 100644 --- a/config/config.exs +++ b/config/config.exs @@ -185,6 +185,7 @@ config :pleroma, :instance, short_description: "", background_image: "/images/city.jpg", instance_thumbnail: "/instance/thumbnail.jpeg", + favicon: "/favicon.png", limit: 5_000, description_limit: 5_000, remote_limit: 100_000, diff --git a/config/description.exs b/config/description.exs index d18649ae8..86756586f 100644 --- a/config/description.exs +++ b/config/description.exs @@ -987,6 +987,13 @@ config :pleroma, :config_description, [ "The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance.", suggestions: ["/instance/thumbnail.jpeg"] }, + %{ + key: :favicon, + type: {:string, :image}, + description: + "Favicon of the instance", + suggestions: ["/favicon.png"] + }, %{ key: :show_reactions, type: :boolean, diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 1a86f7a53..9e1bbfb49 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -38,10 +38,11 @@ defmodule Pleroma.Web.Fallback.RedirectController do tags = build_tags(conn, params) preloads = preload_data(conn, params) title = "#{Pleroma.Config.get([:instance, :name])}" + favicon = "" response = index_content - |> String.replace("", tags <> preloads <> title) + |> String.replace("", tags <> preloads <> title <> favicon) conn |> put_resp_content_type("text/html") @@ -56,10 +57,11 @@ defmodule Pleroma.Web.Fallback.RedirectController do {:ok, index_content} = File.read(index_file_path()) preloads = preload_data(conn, params) title = "#{Pleroma.Config.get([:instance, :name])}" + favicon = "" response = index_content - |> String.replace("", preloads <> title) + |> String.replace("", preloads <> title <> favicon) conn |> put_resp_content_type("text/html") From 82e4c5d0775a22ddade3d8b6b71564a3f11604b4 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 29 Oct 2023 19:13:02 +0200 Subject: [PATCH 079/317] include the PWA manifest --- lib/pleroma/web/fallback/redirect_controller.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 9e1bbfb49..2999ded26 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -39,10 +39,11 @@ defmodule Pleroma.Web.Fallback.RedirectController do preloads = preload_data(conn, params) title = "#{Pleroma.Config.get([:instance, :name])}" favicon = "" + manifest = "" response = index_content - |> String.replace("", tags <> preloads <> title <> favicon) + |> String.replace("", tags <> preloads <> title <> favicon <> manifest) conn |> put_resp_content_type("text/html") @@ -58,10 +59,11 @@ defmodule Pleroma.Web.Fallback.RedirectController do preloads = preload_data(conn, params) title = "#{Pleroma.Config.get([:instance, :name])}" favicon = "" + manifest = "" response = index_content - |> String.replace("", preloads <> title <> favicon) + |> String.replace("", preloads <> title <> favicon <> manifest) conn |> put_resp_content_type("text/html") From 5848ef137f40cf64e1b54f7eeaf4a73a35f7a34e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 29 Oct 2023 19:25:14 +0200 Subject: [PATCH 080/317] add fields needed for chrome to make website installable --- config/config.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.exs b/config/config.exs index 2b2583671..b29f6de3a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -361,6 +361,8 @@ config :pleroma, :manifest, icons: [ %{ src: "/static/logo.svg", + size: "144x144", + purpose: "any", type: "image/svg+xml" } ], From 3d2067d4331feb32c3c0a82131e2b6af6a154b58 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 29 Oct 2023 19:28:57 +0200 Subject: [PATCH 081/317] typo --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index b29f6de3a..a5328fc9b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -361,7 +361,7 @@ config :pleroma, :manifest, icons: [ %{ src: "/static/logo.svg", - size: "144x144", + sizes: "144x144", purpose: "any", type: "image/svg+xml" } From d8eb741b6d85d966c608628cf2ecfb669eafeb11 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 29 Oct 2023 19:32:07 +0200 Subject: [PATCH 082/317] changelog --- changelog.d/favicon.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/favicon.add diff --git a/changelog.d/favicon.add b/changelog.d/favicon.add new file mode 100644 index 000000000..cf12395e7 --- /dev/null +++ b/changelog.d/favicon.add @@ -0,0 +1 @@ +Add support for configuring favicon, embed favicon and PWA manifest in server-generated meta From 6f38915198c3a820f6c265044684a082b756cb91 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 30 Oct 2023 11:29:57 +0200 Subject: [PATCH 083/317] lint? --- config/description.exs | 3 +-- lib/pleroma/web/fallback/redirect_controller.ex | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/description.exs b/config/description.exs index 86756586f..20c8cc05d 100644 --- a/config/description.exs +++ b/config/description.exs @@ -990,8 +990,7 @@ config :pleroma, :config_description, [ %{ key: :favicon, type: {:string, :image}, - description: - "Favicon of the instance", + description: "Favicon of the instance", suggestions: ["/favicon.png"] }, %{ diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 2999ded26..44726c6df 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -43,7 +43,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do response = index_content - |> String.replace("", tags <> preloads <> title <> favicon <> manifest) + |> String.replace( + "", + tags <> preloads <> title <> favicon <> manifest + ) conn |> put_resp_content_type("text/html") From 5a807299864d9aab28431c113f9aa4585e9ca464 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 1 Nov 2023 18:57:20 +0200 Subject: [PATCH 084/317] always include title, manifest and favicon --- lib/pleroma/web/fallback/redirect_controller.ex | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 44726c6df..593e4c282 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -18,9 +18,22 @@ defmodule Pleroma.Web.Fallback.RedirectController do end def redirector(conn, _params, code \\ 200) do + {:ok, index_content} = File.read(index_file_path()) + + title = "#{Pleroma.Config.get([:instance, :name])}" + favicon = "" + manifest = "" + + response = + index_content + |> String.replace( + "", + title <> favicon <> manifest + ) + conn |> put_resp_content_type("text/html") - |> send_file(code, index_file_path()) + |> send_resp(200, response) end def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do From 2561febffdf341552c3553ee8300d0305e2b45a5 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 26 Mar 2024 12:48:52 +0200 Subject: [PATCH 085/317] thanks emacs/arch --- .../web/fallback/redirect_controller.ex | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index d556ae88e..4a0885fab 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -32,22 +32,6 @@ defmodule Pleroma.Web.Fallback.RedirectController do def redirector(conn, _params, code \\ 200) do {:ok, index_content} = File.read(index_file_path()) -<<<<<<< HEAD - title = "#{Pleroma.Config.get([:instance, :name])}" - favicon = "" - manifest = "" - - response = - index_content - |> String.replace( - "", - title <> favicon <> manifest - ) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) -======= response = index_content |> add_generated_metadata() @@ -55,7 +39,6 @@ defmodule Pleroma.Web.Fallback.RedirectController do conn |> put_resp_content_type("text/html") |> send_resp(code, response) ->>>>>>> origin/develop end def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do @@ -71,23 +54,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do {:ok, index_content} = File.read(index_file_path()) tags = build_tags(conn, params) preloads = preload_data(conn, params) -<<<<<<< HEAD - title = "#{Pleroma.Config.get([:instance, :name])}" - favicon = "" - manifest = "" - - response = - index_content - |> String.replace( - "", - tags <> preloads <> title <> favicon <> manifest - ) -======= response = index_content |> add_generated_metadata(tags <> preloads) ->>>>>>> origin/develop conn |> put_resp_content_type("text/html") @@ -101,20 +71,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do def redirector_with_preload(conn, params) do {:ok, index_content} = File.read(index_file_path()) preloads = preload_data(conn, params) -<<<<<<< HEAD - title = "#{Pleroma.Config.get([:instance, :name])}" - favicon = "" - manifest = "" - - response = - index_content - |> String.replace("", preloads <> title <> favicon <> manifest) -======= response = index_content |> add_generated_metadata(preloads) ->>>>>>> origin/develop conn |> put_resp_content_type("text/html") From 627c944fecebe3d48a6bc35ed62273fc058814ff Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 09:20:46 -0400 Subject: [PATCH 086/317] Search Indexing: filter indexable activities before inserting Oban jobs --- lib/pleroma/search.ex | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index fd0218cb8..b60ca3d05 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,12 +1,26 @@ defmodule Pleroma.Search do + alias Pleroma.Activity + alias Pleroma.Object alias Pleroma.Workers.SearchIndexingWorker - def add_to_index(%Pleroma.Activity{id: activity_id}) do - SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) + @spec add_to_index(Activity.t()) :: :ok | :error + def add_to_index(%Pleroma.Activity{id: activity_id} = activity) do + with true <- indexable?(activity), + {:ok, %Oban.Job{}} <- + SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) do + :ok + else + false -> :ok + _ -> :error + end end + @spec remove_from_index(Object.t()) :: :ok | :error def remove_from_index(%Pleroma.Object{id: object_id}) do - SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) + case SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) do + {:ok, %Oban.Job{}} -> :ok + _ -> :error + end end def search(query, options) do @@ -18,4 +32,7 @@ defmodule Pleroma.Search do search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module.healthcheck_endpoints end + + defp indexable?(%Activity{object: %Object{}, data: %{"type" => "Create"}}), do: true + defp indexable?(_), do: false end From a5c88eb39b8d01b7d831dd1ba95fc1c00f0eba52 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 09:24:48 -0400 Subject: [PATCH 087/317] Remove redundant checks from backends' add_to_index/1 --- lib/pleroma/search/meilisearch.ex | 34 +++++++++++++---------------- lib/pleroma/search/qdrant_search.ex | 26 ++++++++++------------ 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 9bba5b30f..fb8fdea1b 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Search.Meilisearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config + alias Pleroma.Object import Pleroma.Search.DatabaseSearch import Ecto.Query @@ -155,28 +156,23 @@ defmodule Pleroma.Search.Meilisearch do end @impl true - def add_to_index(activity) do - maybe_search_data = object_to_search_data(activity.object) + def add_to_index(%Activity{object: %Object{} = object} = activity) do + search_data = object_to_search_data(object) - if activity.data["type"] == "Create" and maybe_search_data do - result = - meili_put( - "/indexes/objects/documents", - [maybe_search_data] - ) + result = + meili_put( + "/indexes/objects/documents", + [search_data] + ) - with {:ok, %{"status" => "enqueued"}} <- result do - # Added successfully - :ok - else - _ -> - # There was an error, report it - Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") - {:error, result} - end - else - # The post isn't something we can search, that's ok + with {:ok, %{"status" => "enqueued"}} <- result do + # Added successfully :ok + else + _ -> + # There was an error, report it + Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") + {:error, result} end end diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex index b659bb682..f00475799 100644 --- a/lib/pleroma/search/qdrant_search.ex +++ b/lib/pleroma/search/qdrant_search.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Search.QdrantSearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config + alias Pleroma.Object alias __MODULE__.OpenAIClient alias __MODULE__.QdrantClient @@ -82,23 +83,18 @@ defmodule Pleroma.Search.QdrantSearch do end @impl true - def add_to_index(activity) do - # This will only index public or unlisted notes - maybe_search_data = object_to_search_data(activity.object) + def add_to_index(%Activity{object: %Object{} = object} = activity) do + search_data = object_to_search_data(object) - if activity.data["type"] == "Create" and maybe_search_data do - with {:ok, embedding} <- get_embedding(maybe_search_data.content), - {:ok, %{status: 200}} <- - QdrantClient.put( - "/collections/posts/points", - build_index_payload(activity, embedding) - ) do - :ok - else - e -> {:error, e} - end - else + with {:ok, embedding} <- get_embedding(search_data.content), + {:ok, %{status: 200}} <- + QdrantClient.put( + "/collections/posts/points", + build_index_payload(activity, embedding) + ) do :ok + else + e -> {:error, e} end end From 77436451adf736285f3aa3435cd79d0429eb797d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 09:26:02 -0400 Subject: [PATCH 088/317] Indexable changelog --- changelog.d/search-indexing.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/search-indexing.change diff --git a/changelog.d/search-indexing.change b/changelog.d/search-indexing.change new file mode 100644 index 000000000..766934f3f --- /dev/null +++ b/changelog.d/search-indexing.change @@ -0,0 +1 @@ +Filter indexable activities before inserting indexing jobs into the queue. From 7c09150cdbdf8c270021bb7b1ccfcc1632354e23 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 10:02:48 -0400 Subject: [PATCH 089/317] Validate the activity is public before indexing Additional tests added --- lib/pleroma/search.ex | 9 ++-- test/pleroma/search/meilisearch_test.exs | 23 -------- test/pleroma/search_test.exs | 69 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 test/pleroma/search_test.exs diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index b60ca3d05..2c7a60c48 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,16 +1,19 @@ defmodule Pleroma.Search do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Workers.SearchIndexingWorker @spec add_to_index(Activity.t()) :: :ok | :error - def add_to_index(%Pleroma.Activity{id: activity_id} = activity) do - with true <- indexable?(activity), + def add_to_index(%Pleroma.Activity{id: activity_id, object: %Object{} = object} = activity) do + with {_, true} <- {:indexable, indexable?(activity)}, + {_, "public"} <- {:visibility, Visibility.get_visibility(object)}, {:ok, %Oban.Job{}} <- SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) do :ok else - false -> :ok + {:indexable, false} -> :ok + {:visibility, _} -> :ok _ -> :error end end diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs index eea454323..ff32491c5 100644 --- a/test/pleroma/search/meilisearch_test.exs +++ b/test/pleroma/search/meilisearch_test.exs @@ -74,29 +74,6 @@ defmodule Pleroma.Search.MeilisearchTest do assert_received("posted_to_meilisearch") end - test "doesn't index posts that are not public" do - user = insert(:user) - - Enum.each(["private", "direct"], fn visibility -> - {:ok, activity} = - CommonAPI.post(user, %{ - status: "guys i just don't wanna leave the swamp", - visibility: visibility - }) - - args = %{"op" => "add_to_index", "activity" => activity.id} - - Config - |> expect(:get, fn - [Pleroma.Search, :module], nil -> - Meilisearch - end) - - assert_enqueued(worker: SearchIndexingWorker, args: args) - assert :ok = perform_job(SearchIndexingWorker, args) - end) - end - test "deletes posts from index when deleted locally" do user = insert(:user) diff --git a/test/pleroma/search_test.exs b/test/pleroma/search_test.exs new file mode 100644 index 000000000..cdd2a72bd --- /dev/null +++ b/test/pleroma/search_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SearchTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.SearchIndexingWorker + + test "indexes posts that are public" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Well this is a story all about how my life got flipped turned upside down", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued(worker: SearchIndexingWorker, args: args) + end + + test "doesn't index posts that are not public" do + user = insert(:user) + + Enum.each(["private", "direct"], fn visibility -> + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: visibility + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + refute_enqueued(worker: SearchIndexingWorker, args: args) + end) + end + + test "Indexes appropriate activity types" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "I'm my own hype man", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued(worker: SearchIndexingWorker, args: args) + + {:ok, fav_activity} = CommonAPI.favorite(user, activity.id) + + args = %{"op" => "add_to_index", "activity" => fav_activity.id} + + refute_enqueued(worker: SearchIndexingWorker, args: args) + + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, user) + + args = %{"op" => "add_to_index", "activity" => repeat_activity.id} + + refute_enqueued(worker: SearchIndexingWorker, args: args) + end +end From 592955a895fff4003e62155b08209490625d156d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 11:04:52 -0400 Subject: [PATCH 090/317] Improve add_to_index/1 when there is no Object --- lib/pleroma/search.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index 2c7a60c48..7eb88672b 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Search do alias Pleroma.Workers.SearchIndexingWorker @spec add_to_index(Activity.t()) :: :ok | :error - def add_to_index(%Pleroma.Activity{id: activity_id, object: %Object{} = object} = activity) do + def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do with {_, true} <- {:indexable, indexable?(activity)}, {_, "public"} <- {:visibility, Visibility.get_visibility(object)}, {:ok, %Oban.Job{}} <- @@ -18,6 +18,13 @@ defmodule Pleroma.Search do end end + def add_to_index(%Activity{id: activity_id}) do + case Activity.get_by_id_with_object(activity_id) do + %Activity{} = preloaded -> add_to_index(preloaded) + _ -> :ok + end + end + @spec remove_from_index(Object.t()) :: :ok | :error def remove_from_index(%Pleroma.Object{id: object_id}) do case SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) do @@ -36,6 +43,6 @@ defmodule Pleroma.Search do search_module.healthcheck_endpoints end - defp indexable?(%Activity{object: %Object{}, data: %{"type" => "Create"}}), do: true + defp indexable?(%Activity{data: %{"type" => "Create"}}), do: true defp indexable?(_), do: false end From fcfe16340a6ee8583257e80cd69eb71e7ae99c2b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 8 Oct 2024 18:38:43 +0300 Subject: [PATCH 091/317] alsp --- config/config.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.exs b/config/config.exs index 1d354a682..44b3be572 100644 --- a/config/config.exs +++ b/config/config.exs @@ -903,6 +903,7 @@ config :pleroma, :mrf, Pleroma.Web.ActivityPub.MRF.TagPolicy, Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy ], transparency: true, transparency_exclusions: [] From 77119d936829d7d87370b1d9f2bba457af1dc707 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 8 Oct 2024 18:39:17 +0300 Subject: [PATCH 092/317] c --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 44b3be572..de9547b10 100644 --- a/config/config.exs +++ b/config/config.exs @@ -902,7 +902,7 @@ config :pleroma, :mrf, Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy, Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, - Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy, Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy ], transparency: true, From 7f121afabaac162ead0307f32005e4bc3473944f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 11 Mar 2025 22:05:16 +0200 Subject: [PATCH 093/317] fix --- config/config.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 3e50b4207..d90d56d9b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -65,8 +65,8 @@ config :pleroma, Pleroma.Upload, proxy_remote: false, filename_display_max_length: 30, default_description: :filename, - base_url: nil - allowed_mime_types: ["image", "audio", "video"] + base_url: nil, + allowed_mime_types: ["image", "audio", "video", "application"] config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" From 82a65328c232d50b693fb9f5ef7ddcddca8e27cb Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 10 Jul 2025 21:42:06 +0300 Subject: [PATCH 094/317] delete custom emoji file --- config/custom_emoji.txt | 115 ---------------------------------------- 1 file changed, 115 deletions(-) delete mode 100644 config/custom_emoji.txt diff --git a/config/custom_emoji.txt b/config/custom_emoji.txt deleted file mode 100644 index c7a706630..000000000 --- a/config/custom_emoji.txt +++ /dev/null @@ -1,115 +0,0 @@ -Advanced_Sorcery, /emoji/sgsgb/Advanced_Sorcery.png -FACE_QUAD, /emoji/sgsgb/FACE_QUAD.png -Yukkuri_Reimu, /emoji/sgsgb/Yukkuri_Reimu.png -a_to, /emoji/sgsgb/a_to.png -angrypirds, /emoji/sgsgb/angrypirds.png -annoying_dog_hole, /emoji/sgsgb/annoying_dog_hole.png -award_bios, /emoji/sgsgb/award_bios.png -bananya, /emoji/sgsgb/bananya.png -bandits, /emoji/sgsgb/bandits.png -blobcatknife, /emoji/sgsgb/blobcatknife.png -cacolaugh, /emoji/sgsgb/cacolaugh.png -catboythink, /emoji/sgsgb/catboythink.png -cirno_run, /emoji/sgsgb/cirno_run.png -debian, /emoji/sgsgb/debian.png -denton, /emoji/sgsgb/denton.png -denton_laugh, /emoji/sgsgb/denton_laugh.png -dio, /emoji/sgsgb/dio.png -dogcited, /emoji/sgsgb/dogcited.png -dong, /emoji/sgsgb/dong.png -duane, /emoji/sgsgb/duane.png -e, /emoji/sgsgb/e.png -energy_star, /emoji/sgsgb/energy_star.png -flynn_smirk, /emoji/sgsgb/flynn_smirk.png -furrythink, /emoji/sgsgb/furrythink.png -granddad, /emoji/sgsgb/granddad.png -heavy_losingeh, /emoji/sgsgb/heavy_losingeh.png -heavy_winninggreat, /emoji/sgsgb/heavy_winninggreat.png -herrington, /emoji/sgsgb/herrington.png -hogan, /emoji/sgsgb/hogan.png -hyperjoy, /emoji/sgsgb/hyperjoy.png -itisamystery, /emoji/sgsgb/itisamystery.png -jacobson, /emoji/sgsgb/jacobson.png -jake, /emoji/sgsgb/jake.png -jyushimatsu_think, /emoji/sgsgb/jyushimatsu_think.png -kinzo, /emoji/sgsgb/kinzo.png -kms, /emoji/sgsgb/kms.png -kokoko, /emoji/sgsgb/kokoko.png -loss, /emoji/sgsgb/loss.png -luck, /emoji/sgsgb/luck.png -lucknorm, /emoji/sgsgb/lucknorm.png -marko, /emoji/sgsgb/marko.png -megaman, /emoji/sgsgb/megaman.png -miyanozoom, /emoji/sgsgb/miyanozoom.png -monster_energy_ultra, /emoji/sgsgb/monster_energy_ultra.png -moraledn, /emoji/sgsgb/moraledn.png -moraleup, /emoji/sgsgb/moraleup.png -multiply, /emoji/sgsgb/multiply.png -mycomputer, /emoji/sgsgb/mycomputer.png -necromancy1, /emoji/sgsgb/necromancy1.png -necromancy2, /emoji/sgsgb/necromancy2.png -necromancy3, /emoji/sgsgb/necromancy3.png -nice, /emoji/sgsgb/nice.png -nyoron, /emoji/sgsgb/nyoron.png -ohgno, /emoji/sgsgb/ohgno.png -ohyes, /emoji/sgsgb/ohyes.png -orangeman, /emoji/sgsgb/orangeman.png -payyap, /emoji/sgsgb/payyap.png -pooh, /emoji/sgsgb/pooh.png -poohington, /emoji/sgsgb/poohington.png -poptepipic1, /emoji/sgsgb/poptepipic1.png -poptepipic10, /emoji/sgsgb/poptepipic10.png -poptepipic11, /emoji/sgsgb/poptepipic11.png -poptepipic12, /emoji/sgsgb/poptepipic12.png -poptepipic13, /emoji/sgsgb/poptepipic13.png -poptepipic14, /emoji/sgsgb/poptepipic14.png -poptepipic15, /emoji/sgsgb/poptepipic15.png -poptepipic16, /emoji/sgsgb/poptepipic16.png -poptepipic17, /emoji/sgsgb/poptepipic17.png -poptepipic18, /emoji/sgsgb/poptepipic18.png -poptepipic19, /emoji/sgsgb/poptepipic19.png -poptepipic2, /emoji/sgsgb/poptepipic2.png -poptepipic20, /emoji/sgsgb/poptepipic20.png -poptepipic21, /emoji/sgsgb/poptepipic21.png -poptepipic22, /emoji/sgsgb/poptepipic22.png -poptepipic23, /emoji/sgsgb/poptepipic23.png -poptepipic3, /emoji/sgsgb/poptepipic3.png -poptepipic4, /emoji/sgsgb/poptepipic4.png -poptepipic5, /emoji/sgsgb/poptepipic5.png -poptepipic6, /emoji/sgsgb/poptepipic6.png -poptepipic7, /emoji/sgsgb/poptepipic7.png -poptepipic8, /emoji/sgsgb/poptepipic8.png -poptepipic9, /emoji/sgsgb/poptepipic9.png -puke, /emoji/sgsgb/puke.png -quake_net, /emoji/sgsgb/quake_net.png -quake_ram, /emoji/sgsgb/quake_ram.png -ranger, /emoji/sgsgb/ranger.png -ranger_none, /emoji/sgsgb/ranger_none.png -risitas1, /emoji/sgsgb/risitas1.png -risitas2, /emoji/sgsgb/risitas2.png -rokalife, /emoji/sgsgb/rokalife.png -sandro, /emoji/sgsgb/sandro.png -scut, /emoji/sgsgb/scut.png -serious, /emoji/sgsgb/serious.png -shag, /emoji/sgsgb/shag.png -shinitai_i, /emoji/sgsgb/shinitai_i.png -shinitai_ni, /emoji/sgsgb/shinitai_ni.png -shinitai_shi, /emoji/sgsgb/shinitai_shi.png -shinitai_ta, /emoji/sgsgb/shinitai_ta.png -shotging, /emoji/sgsgb/shotging.png -skelethor, /emoji/sgsgb/skelethor.png -smug_marisa, /emoji/sgsgb/smug_marisa.png -spurdo, /emoji/sgsgb/spurdo.png -stalkers, /emoji/sgsgb/stalkers.png -tenshi_eating_corndog, /emoji/sgsgb/tenshi_eating_corndog.png -thinkingwat, /emoji/sgsgb/thinkingwat.png -thnk, /emoji/sgsgb/thnk.png -tobdog, /emoji/sgsgb/tobdog.png -todd, /emoji/sgsgb/todd.png -trash, /emoji/sgsgb/trash.png -tuturu, /emoji/sgsgb/tuturu.png -tux, /emoji/sgsgb/tux.png -uwot, /emoji/sgsgb/uwot.png -uwot2, /emoji/sgsgb/uwot2.png -uwot3, /emoji/sgsgb/uwot3.png -vanpeek, /emoji/sgsgb/vanpeek.png From baa8004a2a46f080d9fbeb4afd324639f277c19f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 12 Aug 2025 02:31:47 +0300 Subject: [PATCH 095/317] new styles for static pages --- lib/pleroma/web/templates/layout/app.html.eex | 6 +- .../templates/o_auth/o_auth/_scopes.html.eex | 15 +- .../web/templates/o_auth/o_auth/show.html.eex | 25 +- priv/static/instance/static.css | 485 +++++++++--------- 4 files changed, 287 insertions(+), 244 deletions(-) diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index e33bada85..99ab0ddad 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -13,8 +13,10 @@

<%= Pleroma.Config.get([:instance, :name]) %>

-
- <%= @inner_content %> +
+
+ <%= @inner_content %> +
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex index 310bf2358..fb34b551d 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -5,10 +5,19 @@ <% # Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> <%= if scope in @scopes do %>
- <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> - <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> + + <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> + <%= label @form, :"scope_#{scope}", "" %> + <%= if scope in @scopes && scope do %> - <%= scope %> <%= :"Elixir.Gettext".dgettext(Gettext, "oauth_scopes", scope) %> +
+
+ <%= :"Elixir.Gettext".dgettext(Gettext, "oauth_scopes", scope) %> +
+
+ <%= scope %> +
+
<% end %>
<% else %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 6bc8eb602..aadddf1f4 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -8,17 +8,20 @@ <%= form_for @conn, Routes.o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> <%= if @user do %> - diff --git a/priv/static/instance/static.css b/priv/static/instance/static.css index 48c74c125..32f56d510 100644 --- a/priv/static/instance/static.css +++ b/priv/static/instance/static.css @@ -3,206 +3,84 @@ } :root { - --brand-color: #d8a070; - --background-color: #121a24; - --foreground-color: #182230; - --primary-text-color: #b9b9ba; - --muted-text-color: #89898a; + font-size: 16px; + + --bg: rgba(18, 26, 36, 1); + --fg: rgba(24, 34, 48, 1); + --text: rgba(185, 185, 186, 1); + --link: rgba(216, 160, 112, 1); + --accent: rgba(216, 160, 112, 1); + --cRed: rgba(211, 16, 20, 1); + --cBlue: rgba(0, 149, 255, 1); + --cGreen: rgba(15, 160, 15, 1); + --cOrange: rgba(255, 165, 0, 1); + --font: sans-serif; + --monoFont: monospace; + --wallpaper: rgba(14, 21, 29, 1); + --selectionBackground: rgba(216, 160, 112, 1); + --selectionText: rgba(0, 0, 0, 1); + --badgeNotification: rgba(211, 16, 20, 1); } body { - background-color: var(--background-color); + background-color: var(--wallpaper); font-family: sans-serif; - color: var(--primary-text-color); + color: var(--text); padding: 0; margin: 0; -} - -.instance-header { - height: 60px; - padding: 10px; - background: var(--foreground-color); - box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5); -} - -.instance-header__content { display: flex; - align-items: center; - max-width: 400px; - margin: 0 auto; -} - -.instance-header__thumbnail { - max-width: 40px; - border-radius: 4px; - margin-right: 12px; -} - -.instance-header__title { - font-size: 16px; - font-weight: bold; - color: var(--primary-text-color); -} - -.container { - max-width: 400px; - background-color: var(--foreground-color); - border-radius: 4px; - overflow: hidden; - margin: 35px auto; - box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5); - padding: 0em 1em 0em 1em; -} - -.container__content { - padding: 0 20px; -} - -h1 { - margin: 0; - font-size: 24px; - text-align: center; -} - -h2 { - color: var(--primary-text-color); - font-weight: normal; - font-size: 18px; - margin-bottom: 20px; + flex-direction: column; + align-items: stretch; } a { - color: var(--brand-color); + color: var(--link); text-decoration: none; } -form { - width: 100%; -} - -.input { - color: var(--muted-text-color); - display: flex; - flex-direction: column; -} - -input { - padding: 10px; - margin-top: 5px; - margin-bottom: 10px; - background-color: var(--background-color); - color: var(--primary-text-color); - border: 0; - transition-property: border-bottom; - transition-duration: 0.35s; - border-bottom: 2px solid #2a384a; - font-size: 14px; - width: inherit; - box-sizing: border-box; -} - -.scopes-input { - display: flex; - flex-direction: column; - margin: 1em 0; - color: var(--muted-text-color); -} - -.scopes-input label:first-child { - height: 2em; -} - -.scopes { - display: flex; - flex-wrap: wrap; - color: var(--primary-text-color); -} - -.scope { - display: flex; - flex-basis: 100%; - height: 2em; - align-items: center; -} - -.scope:before { - color: var(--primary-text-color); - content: "✔\fe0e"; - margin-left: 1em; - margin-right: 1em; -} - -[type="checkbox"] + label { - display: none; - cursor: pointer; - margin: 0.5em; -} - -[type="checkbox"] { - display: none; -} - -[type="checkbox"] + label:before { - cursor: pointer; - display: inline-block; - color: white; - background-color: var(--background-color); - border: 4px solid var(--background-color); - box-shadow: 0px 0px 1px 0 var(--brand-color); - width: 1.2em; - height: 1.2em; - margin-right: 1.0em; - content: ""; - transition-property: background-color; - transition-duration: 0.35s; - color: var(--background-color); - margin-bottom: -0.2em; - border-radius: 2px; -} - -[type="checkbox"]:checked + label:before { - background-color: var(--brand-color); -} - -input:focus { - outline: none; - border-bottom: 2px solid var(--brand-color); -} - -.actions { - display: flex; - justify-content: flex-end; -} - -.actions button, -.actions a.button { - width: auto; - margin-left: 10px; -} - -a.button, -button { +.button { width: 100%; background-color: #1c2a3a; color: var(--primary-text-color); border-radius: 4px; border: none; - padding: 10px 16px; - margin-top: 20px; - margin-bottom: 20px; - text-transform: uppercase; - font-size: 16px; - box-shadow: 0px 0px 2px 0px black, - 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, - 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + padding: 0 1em; + line-height: 2; + margin-top: 2em; + font-size: 1em; + cursor: pointer; + box-shadow: + 0 0 2px 0 black, + 0 1px 0 0 rgba(255, 255, 255, 0.2) inset, + 0 -1px 0 0 rgba(0, 0, 0, 0.2) inset; } -a.button:hover, -button:hover { - cursor: pointer; - box-shadow: 0px 0px 0px 1px var(--brand-color), - 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, - 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; +.button:hover { + box-shadow: + 0 0 1px 2px rgba(185, 185, 186, 0.4), + 0 1px 0 0 rgba(255, 255, 255, 0.2) inset, + 0 -1px 0 0 rgba(0, 0, 0, 0.2) inset; +} + +.button:active { + transform: translate(1px, 1px); + box-shadow: + 0 0 2px 0 black, + 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, + 0 1px 0 0 rgba(0, 0, 0, 0.2) inset; +} + +.button:hover:active { + box-shadow: + 0 0 1px 2px rgba(185, 185, 186, 0.4), + 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, + 0 1px 0 0 rgba(0, 0, 0, 0.2) inset; +} + +.input { + color: var(--text); + display: flex; + flex-direction: column; } .alert-danger { @@ -226,73 +104,222 @@ button:hover { font-size: 16px; } -.account-header__banner { +.instance-header { + display: flex; + height: 3.5rem; + padding: 0 1em; + background: var(--fg); + box-shadow: + 0 1px 4px 0 rgba(0, 0, 0, 0.4), + 0 2px 7px 0 rgba(0, 0, 0, 0.3); + justify-content: center; + align-self: stretch; + margin-bottom: 1rem; +} + +.instance-header__content { + line-height: 3.5rem; + display: flex; + flex: 1 1 45em; + max-width: 45em; + align-items: center; +} + +.instance-header__thumbnail { + max-width: 3.5em; + border-radius: 0.25em; + margin-right: 0.75em; +} + +.instance-header__title { + font-weight: bold; + color: var(--text); + font-size: 1rem; +} + +.contents { + display: flex; + align-items: stretch; + justify-content: center; +} + +.panel { + color: var(--text); + background-color: var(--bg); + position: relative; + max-width: 45em; + border-radius: 0.5em; + overflow: hidden; + margin: 0; + box-shadow: + 0 0 3px 0 rgba(0, 0, 0, 0.5), + 0 4px 6px 3px rgba(0, 0, 0, 0.3); + padding: 0; + flex: 1 1 45em; +} + +.button { + width: auto; +} + +.panel-body { + padding: 0 1em 1em; + background-color: var(--bg); +} + +.actions { + display: flex; + justify-content: flex-end; +} + +.actions .button { + margin-left: 0.5em; +} + +.account-header { + position: relative; + display: flex; + align-items: end; + width: 100%; + aspect-ratio: 3; + gap: 1em; + padding: 0 1em; +} + +.account-header_container { + position: absolute; + inset: 0; + z-index: 0; + mask: linear-gradient(to top,transparent 0,white 5em) bottom no-repeat; +} + +.account-header__overlay, +.account-header__banner { + position: absolute; + width: 100%; + inset: 0; +} + +.account-header__overlay { + background-color: rgba(18, 26, 36, 0.5); + z-index: -1; +} + +.account-header__banner { + z-index: -2; + object-fit: cover; width: 100%; - height: 112px; - background-size: cover; - background-position: center; } .account-header__avatar { - width: 94px; - height: 94px; + width: 6em; + height: 6em; + flex: 0 0 6em; + z-index: 1; background-size: cover; background-position: center; - margin: -47px 10px 0; - border: 6px solid var(--foreground-color); - border-radius: 999px; } .account-header__meta { - padding: 6px 20px 17px; + z-index: 1; + flex: 1 1 0; + display: flex; + flex-direction: column; + min-width: 10em; + font-size: 125%; } +.account-header__nickname, .account-header__display-name { - font-size: 20px; + font-size: 1.25em; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; +} + +.acocunt-header__display-name { font-weight: bold; } .account-header__nickname { - font-size: 14px; - color: var(--muted-text-color); + font-size: 0.75em; + color: var(--link); } -@media all and (max-width: 420px) { - .container { - margin: 0 auto; - border-radius: 0; - } - - .scope { - flex-basis: 0%; - } - - .scope:before { - content: ""; - margin-left: 0em; - margin-right: 1em; - } - - .scope:first-child:before { - margin-left: 1em; - content: "✔\fe0e"; - } - - .scope:after { - content: ","; - } - - .scope:last-child:after { - content: ""; - } +input { + display: inline-block; + background-color: var(--background); + outline: none; + color: var(--text); + line-height: 2; + border-radius: 0.5em; + box-shadow: var(--shadow); + border: none; + padding: 0 0.5em; } -.form-row { + +input[type="checkbox"] { + display: none; +} + +input, label, label::before { + --background: rgba(15, 21, 30, 1); + --shadow: + 0 0 2px 0 rgba(0, 0, 0, 1), + 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, + 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, + 0 0 2px 0 rgba(0, 0, 0, 0.15) inset, + 1px 0 1px 1px rgba(185, 185, 186, 0.15), + -1px 0 1px 1px rgba(185, 185, 186, 0.15); + --shadowHover: + 0 0 4px 0 rgba(185, 185, 186, 0.5), + 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, + 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, + 0 0 2px 0 rgba(0, 0, 0, 0.15) inset, + 1px 0 1px 1px rgba(185, 185, 186, 0.15), + -1px 0 1px 1px rgba(185, 185, 186, 0.15); +} + + +label[type="checkbox"]::before { + content: "" +} + +input[type="checkbox"]:checked + label::before { + content: "✓" +} + +label { + padding: 1em; + display: inline-block; +} + +input[type="checkbox"] + label::before { + content: ""; + + box-shadow: + min-width: 1em; + min-height: 1em; + text-align: center; + box-shadow: var(--shadow); +} + +label:hover::before { + box-shadow: var(--shadowHover); +} + +.scope { display: flex; -} -.form-row > label { - line-height: 47px; - flex: 1; -} -.form-row > input { - flex: 2; + align-items: center; + + dl { + margin: 0.75em; + + display: flex; + flex-direction: column; + + dt, dd { + margin: 0; + } + } } From 9ede9b92d3604486fdf7189ef6d55c356ffb190a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:32:01 +0400 Subject: [PATCH 096/317] RateLimiterTest: Add failing test for invalid values. --- test/pleroma/web/plugs/rate_limiter_test.exs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs index 19cee8aee..10c93fa73 100644 --- a/test/pleroma/web/plugs/rate_limiter_test.exs +++ b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -268,6 +268,23 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) end + test "doesn't crash if rate limit scale is invalid (e.g. broken DB config)" do + limiter_name = :test_invalid_rate_limit_config + + clear_config([:rate_limit, limiter_name], [{"", 0}, {"", ""}]) + clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + opts = RateLimiter.init(name: limiter_name) + + conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 1}} + + conn_limited = RateLimiter.call(conn, opts) + + refute conn_limited.status == Conn.Status.code(:too_many_requests) + refute conn_limited.resp_body + refute conn_limited.halted + end + def expire_ttl(%{remote_ip: remote_ip} = _conn, bucket_name_root) do bucket_name = "anon:#{bucket_name_root}" |> String.to_atom() key_name = "ip::#{remote_ip |> Tuple.to_list() |> Enum.join(".")}" From 958a4581d647110f00a108e9b77ccfd17fe2a78a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:54:06 +0400 Subject: [PATCH 097/317] RateLimiter: Ensure that the rate limiter doesn't crash on bad values --- lib/pleroma/web/plugs/rate_limiter.ex | 96 ++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index aa79dbf6b..22c145514 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -67,6 +67,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do import Plug.Conn alias Pleroma.Config + alias Pleroma.Config.Holder alias Pleroma.User alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor @@ -143,7 +144,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do def action_settings(plug_opts) do with limiter_name when is_atom(limiter_name) <- plug_opts[:name], - limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do + {:ok, limits} <- fetch_and_normalize_limits(limiter_name) do bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name) %{ @@ -151,9 +152,102 @@ defmodule Pleroma.Web.Plugs.RateLimiter do limits: limits, opts: plug_opts } + else + :disabled -> nil end end + defp fetch_and_normalize_limits(limiter_name) do + limits = Config.get([:rate_limit, limiter_name]) + + case normalize_limits(limits) do + {:ok, limits} -> + {:ok, limits} + + :disabled -> + :disabled + + :error -> + default_limits = + Holder.default_config(:pleroma, :rate_limit) + |> get_default_limits(limiter_name) + + case normalize_limits(default_limits) do + {:ok, normalized_limits} -> + warn_invalid_limits_once(limiter_name, limits) + {:ok, normalized_limits} + + _ -> + warn_invalid_limits_once(limiter_name, limits) + :disabled + end + end + end + + defp get_default_limits(%{} = rate_limit, limiter_name), do: Map.get(rate_limit, limiter_name) + + defp get_default_limits(rate_limit, limiter_name) when is_list(rate_limit) do + if Keyword.keyword?(rate_limit) do + Keyword.get(rate_limit, limiter_name) + else + nil + end + end + + defp get_default_limits(_, _), do: nil + + @invalid_limits_warned_key {__MODULE__, :invalid_limits_warned} + + defp warn_invalid_limits_once(limiter_name, limits) do + warned = :persistent_term.get(@invalid_limits_warned_key, MapSet.new()) + + if MapSet.member?(warned, limiter_name) do + :ok + else + :persistent_term.put(@invalid_limits_warned_key, MapSet.put(warned, limiter_name)) + + Logger.warning( + "Invalid rate limiter config for #{inspect(limiter_name)}: #{inspect(limits)}. Falling back to defaults or disabling this limiter." + ) + end + end + + defp normalize_limits(nil), do: :disabled + + defp normalize_limits({scale, limit}) do + with {:ok, scale} <- normalize_integer(scale), + {:ok, limit} <- normalize_integer(limit), + true <- scale >= 1 and limit >= 1 do + {:ok, {scale, limit}} + else + _ -> :error + end + end + + defp normalize_limits([{_, _} = first, {_, _} = second]) do + with {:ok, first} <- normalize_limits(first), + {:ok, second} <- normalize_limits(second) do + {:ok, [first, second]} + else + _ -> :error + end + end + + defp normalize_limits(_), do: :error + + defp normalize_integer(value) when is_integer(value), do: {:ok, value} + + defp normalize_integer(value) when is_binary(value) do + value = String.trim(value) + + case Integer.parse(value) do + {number, ""} -> {:ok, number} + _ -> :error + end + end + + defp normalize_integer(_), do: :error + defp check_rate(action_settings) do bucket_name = make_bucket_name(action_settings) key_name = make_key_name(action_settings) From 47f4bde0ea0475cd530dd3127ccf863131a0c335 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 16:06:51 +0400 Subject: [PATCH 098/317] ConfigDBTest: Add failing test for invalid rate limiter values. --- test/pleroma/config_db_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index d68e4e6fa..bb20ac4cd 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -174,6 +174,22 @@ defmodule Pleroma.ConfigDBTest do assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]] assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]] end + + test "rejects invalid :rate_limit values (e.g. empty-string scale from AdminFE)" do + assert {:error, _changeset} = + ConfigDB.update_or_create(%{ + group: ":pleroma", + key: ":rate_limit", + value: [ + %{ + "tuple" => [ + ":statuses_actions", + [%{"tuple" => ["", 0]}, %{"tuple" => ["", ""]}] + ] + } + ] + }) + end end describe "delete/1" do From bd619162708487adfc034cf2859f1190eb3717ef Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 16:34:17 +0400 Subject: [PATCH 099/317] ConfigDB, RateLimiter, RateLimit: Use new type to parse and cast rate limits. --- lib/pleroma/config_db.ex | 52 ++++++++++++++++ lib/pleroma/ecto_type/config/rate_limit.ex | 71 ++++++++++++++++++++++ lib/pleroma/web/plugs/rate_limiter.ex | 36 ++--------- 3 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 lib/pleroma/ecto_type/config/rate_limit.ex diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index e9990fa35..2c3df773b 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do import Pleroma.Web.Gettext alias __MODULE__ + alias Pleroma.EctoType.Config.RateLimit alias Pleroma.Repo @type t :: %__MODULE__{} @@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do |> cast(params, [:key, :group, :value]) |> validate_required([:key, :group, :value]) |> unique_constraint(:key, name: :config_group_key_index) + |> validate_rate_limit() end + defp validate_rate_limit(changeset) do + group = get_field(changeset, :group) + key = get_field(changeset, :key) + + if group == :pleroma and key == :rate_limit do + value = get_field(changeset, :value) + + case normalize_rate_limit(value) do + {:ok, normalized_value} -> + put_change(changeset, :value, normalized_value) + + {:error, {limiter_name, reason}} -> + add_error( + changeset, + :value, + "invalid :rate_limit value for #{inspect(limiter_name)}: #{reason}" + ) + end + else + changeset + end + end + + defp normalize_rate_limit(nil), do: {:ok, nil} + + defp normalize_rate_limit(%{} = value), do: normalize_rate_limit(Map.to_list(value)) + + defp normalize_rate_limit(value) when is_list(value) do + if Keyword.keyword?(value) do + value + |> Enum.reduce_while({:ok, []}, fn {limiter_name, limiter_value}, {:ok, acc} -> + case RateLimit.cast_with_error(limiter_value) do + {:ok, normalized_limiter_value} -> + {:cont, {:ok, [{limiter_name, normalized_limiter_value} | acc]}} + + {:error, reason} -> + {:halt, {:error, {limiter_name, reason}}} + end + end) + |> case do + {:ok, acc} -> {:ok, Enum.reverse(acc)} + {:error, _} = error -> error + end + else + {:error, {:rate_limit, "must be a keyword list"}} + end + end + + defp normalize_rate_limit(_), do: {:error, {:rate_limit, "must be a keyword list"}} + defp create(params) do %ConfigDB{} |> changeset(params) diff --git a/lib/pleroma/ecto_type/config/rate_limit.ex b/lib/pleroma/ecto_type/config/rate_limit.ex new file mode 100644 index 000000000..0518ffd7e --- /dev/null +++ b/lib/pleroma/ecto_type/config/rate_limit.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.Config.RateLimit do + @moduledoc false + + use Ecto.Type + + @type t :: + nil + | {non_neg_integer(), non_neg_integer()} + | [{non_neg_integer(), non_neg_integer()}] + + @impl true + def type, do: :term + + @impl true + def cast(value) do + case cast_with_error(value) do + {:ok, normalized} -> {:ok, normalized} + {:error, _reason} -> :error + end + end + + @impl true + def load(value), do: cast(value) + + @impl true + def dump(value), do: cast(value) + + @spec cast_with_error(term()) :: {:ok, t()} | {:error, String.t()} + def cast_with_error(nil), do: {:ok, nil} + + def cast_with_error({scale, limit}) do + with {:ok, scale} <- parse_integer(scale, "scale"), + {:ok, limit} <- parse_integer(limit, "limit"), + true <- scale >= 1 and limit >= 1 do + {:ok, {scale, limit}} + else + false -> {:error, "scale and limit must be >= 1"} + {:error, reason} -> {:error, reason} + end + end + + def cast_with_error([{_, _} = unauth, {_, _} = auth]) do + with {:ok, unauth} <- cast_with_error(unauth), + {:ok, auth} <- cast_with_error(auth) do + {:ok, [unauth, auth]} + else + {:error, reason} -> {:error, reason} + end + end + + def cast_with_error(_), + do: + {:error, "must be a {scale, limit} tuple, a [{scale, limit}, {scale, limit}] list, or nil"} + + defp parse_integer(value, _label) when is_integer(value), do: {:ok, value} + + defp parse_integer(value, label) when is_binary(value) do + value = String.trim(value) + + case Integer.parse(value) do + {number, ""} -> {:ok, number} + _ -> {:error, "#{label} must be an integer"} + end + end + + defp parse_integer(_value, label), do: {:error, "#{label} must be an integer"} +end diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index 22c145514..0ebb73bbc 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -68,6 +68,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do alias Pleroma.Config alias Pleroma.Config.Holder + alias Pleroma.EctoType.Config.RateLimit alias Pleroma.User alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor @@ -214,40 +215,13 @@ defmodule Pleroma.Web.Plugs.RateLimiter do defp normalize_limits(nil), do: :disabled - defp normalize_limits({scale, limit}) do - with {:ok, scale} <- normalize_integer(scale), - {:ok, limit} <- normalize_integer(limit), - true <- scale >= 1 and limit >= 1 do - {:ok, {scale, limit}} - else - _ -> :error + defp normalize_limits(limits) do + case RateLimit.cast(limits) do + {:ok, normalized_limits} -> {:ok, normalized_limits} + :error -> :error end end - defp normalize_limits([{_, _} = first, {_, _} = second]) do - with {:ok, first} <- normalize_limits(first), - {:ok, second} <- normalize_limits(second) do - {:ok, [first, second]} - else - _ -> :error - end - end - - defp normalize_limits(_), do: :error - - defp normalize_integer(value) when is_integer(value), do: {:ok, value} - - defp normalize_integer(value) when is_binary(value) do - value = String.trim(value) - - case Integer.parse(value) do - {number, ""} -> {:ok, number} - _ -> :error - end - end - - defp normalize_integer(_), do: :error - defp check_rate(action_settings) do bucket_name = make_bucket_name(action_settings) key_name = make_key_name(action_settings) From b9c281a0c3087d67de2292a0b27ce33c159b2246 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 16:42:29 +0400 Subject: [PATCH 100/317] Add changelog --- changelog.d/rate-limiter-hardening.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/rate-limiter-hardening.fix diff --git a/changelog.d/rate-limiter-hardening.fix b/changelog.d/rate-limiter-hardening.fix new file mode 100644 index 000000000..a3af8fcc4 --- /dev/null +++ b/changelog.d/rate-limiter-hardening.fix @@ -0,0 +1 @@ +Stop the rate limiter from crashing when run with wrong settings. From 1af8997462c52e52c72be436aee990621b692dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:32:35 +0100 Subject: [PATCH 101/317] do not ever allow setting database_config_whitelist to database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/admin_api/controllers/config_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index 2c9c27294..f3adeb35a 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -174,6 +174,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do end end + defp whitelisted_config?(:pleroma, :database_config_whitelist), do: false + defp whitelisted_config?(group, key) do if whitelisted_configs = Config.get(:database_config_whitelist) do Enum.any?(whitelisted_configs, fn From 57a3b1f6d0069934cc564c192dd46e834be8497e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:33:14 +0100 Subject: [PATCH 102/317] Add sane defaults for :database_config_whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- config/config.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/config.exs b/config/config.exs index 683805fe3..5bf2c5c2e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -960,6 +960,15 @@ config :pleroma, Pleroma.Search.QdrantSearch, vectors: %{size: 384, distance: "Cosine"} } +config :pleroma, :database_config_whitelist, [ + {:pleroma}, + {:cors_plug}, + {:ex_aws, :s3}, + {:mime}, + {:prometheus, Pleroma.Web.Endpoint.MetricsExporter}, + {:web_push_encryption, :vapid_details} +] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" From f0669997d30d79fc2d93e7bbadbb8f6fd418cc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:34:06 +0100 Subject: [PATCH 103/317] Add test for default whitelist config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../web/admin_api/controllers/config_controller_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index e12115ea1..ab216e49f 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -1472,5 +1472,13 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) assert web_endpoint["children"] end + + test "all keys from description are whitelisted", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/config/descriptions") + + assert response = json_response_and_validate_schema(conn, 200) + + assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions()) + end end end From b66b93a94ada205c710f1e93efe7a984485ae43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:34:45 +0100 Subject: [PATCH 104/317] Add task for filtering non-whitelisted configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/database-config-whitelist.add | 1 + docs/administration/CLI_tasks/config.md | 16 ++++++- lib/mix/tasks/pleroma/config.ex | 56 +++++++++++++++++++++++ test/mix/tasks/pleroma/config_test.exs | 18 ++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 changelog.d/database-config-whitelist.add diff --git a/changelog.d/database-config-whitelist.add b/changelog.d/database-config-whitelist.add new file mode 100644 index 000000000..a78960c98 --- /dev/null +++ b/changelog.d/database-config-whitelist.add @@ -0,0 +1 @@ +Add reasonable defaults for :database_config_whitelist \ No newline at end of file diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md index 13d671a7e..35e02145e 100644 --- a/docs/administration/CLI_tasks/config.md +++ b/docs/administration/CLI_tasks/config.md @@ -169,4 +169,18 @@ This forcibly removes any enabled MRF that does not exist and will fix the abili === "From Source" ```sh mix pleroma.config fix_mrf_policies - ``` \ No newline at end of file + ``` + +## Remove non-whitelisted configs from the database + +This removes any configuration value that is not explicitly whitelisted by `:pleroma, :database_config_whitelist`. Might be useful after updating the whitelist. + +=== "OTP" + ```sh + ./bin/pleroma_ctl config filter_whitelisted + ``` + +=== "From Source" + ```sh + mix pleroma.config filter_whitelisted + ``` diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 834b4fe14..fc6982ea8 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -234,6 +234,57 @@ defmodule Mix.Tasks.Pleroma.Config do end) end + # Removes non-whitelisted configuration sections + def run(["filter_whitelisted" | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [force: :boolean], + aliases: [f: :force] + ) + + force = Keyword.get(options, :force, false) + + start_pleroma() + + whitelisted_configs = Pleroma.Config.get(:database_config_whitelist) + + whitelisted_groups = + whitelisted_configs + |> Enum.filter(fn + {_group} -> true + _ -> false + end) + |> Enum.map(fn {group} -> group end) + + whitelisted_keys = + whitelisted_configs + |> Enum.filter(fn + {_group, _key} -> true + _ -> false + end) + + filtered = + from(c in ConfigDB) + |> Repo.all() + |> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys)) + + if not Enum.empty?(filtered) do + shell_info("The following settings will be removed from ConfigDB:\n") + Enum.each(filtered, &dump(&1)) + + if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do + filtered_ids = Enum.map(filtered, fn %{id: id} -> id end) + + Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids)) + else + shell_error("No changes made.") + end + else + shell_error("No unwanted settings in ConfigDB. No changes made.") + end + end + @spec migrate_to_db(Path.t() | nil) :: any() def migrate_to_db(file_path \\ nil) do with :ok <- Pleroma.Config.DeprecationWarnings.warn() do @@ -434,4 +485,9 @@ defmodule Mix.Tasks.Pleroma.Config do Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") end + + defp not_whitelisted?(%{group: group, key: key}, whitelisted_groups, whitelisted_keys) do + not Enum.member?(whitelisted_groups, group) and + not Enum.member?(whitelisted_keys, {group, key}) + end end diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index 942cfa83d..3b1037f0a 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -329,5 +329,23 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do assert config_records() == [] end + + test "filters non-whitelisted settings" do + clear_config(:database_config_whitelist, [ + {:pleroma}, + {:web_push_encryption, :vapid_details} + ]) + + insert_config_record(:web_push_encryption, :non_whitelisted_key, a: 1) + insert_config_record(:web_push_encryption, :vapid_details, b: 1) + + MixTask.run(["filter_whitelisted", "--force"]) + + assert [ + %ConfigDB{group: :pleroma, key: :instance}, + %ConfigDB{group: :pleroma, key: Pleroma.Captcha}, + %ConfigDB{group: :web_push_encryption, key: :vapid_details} + ] = config_records() + end end end From 92fd157cd82f487899e451635d1b4a94560062b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:35:53 +0100 Subject: [PATCH 105/317] Update cheatsheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- docs/configuration/cheatsheet.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 54dd4a5f0..b156c74cf 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1132,8 +1132,9 @@ Boolean, enables/disables in-database configuration. Read [Transferring the conf List of valid configuration sections which are allowed to be configured from the database. Settings stored in the database before the whitelist is configured are -still applied, so it is suggested to only use the whitelist on instances that -have not migrated the config to the database. +still applied. Consider running the `mix pleroma.config filter_whitelisted` task +after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration//CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database) +for more information. Example: ```elixir From 49985b1614467120f1585f9590f5fc7c5c363eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:37:02 +0100 Subject: [PATCH 106/317] Update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../controllers/config_controller_test.exs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index ab216e49f..7cb4ec938 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -194,6 +194,16 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do setup do: clear_config(:configurable_from_database, true) + setup do: + clear_config(:database_config_whitelist, [ + {:pleroma}, + {:http}, + {:idna}, + {:oban}, + {:tesla}, + {:ueberauth} + ]) + test "create new config setting in db", %{conn: conn} do ueberauth = Application.get_env(:ueberauth, Ueberauth) on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) @@ -807,7 +817,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do %{ "tuple" => [ "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", + ":sth", %{ "tuple" => [ "Phoenix.Transports.WebSocket", @@ -871,7 +881,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do %{ "tuple" => [ "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", + ":sth", %{ "tuple" => [ "Phoenix.Transports.WebSocket", From 77a1d79f92398476a081c3adcca8d227035e6f21 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 12:31:35 +0400 Subject: [PATCH 107/317] ConfigTest: Don't crash when whitelist is unset / disabled --- test/mix/tasks/pleroma/config_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index 3b1037f0a..ef1adc235 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -347,5 +347,21 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do %ConfigDB{group: :web_push_encryption, key: :vapid_details} ] = config_records() end + + test "filter_whitelisted doesn't crash when whitelist is unset" do + clear_config(:database_config_whitelist, nil) + + existing = config_records() + MixTask.run(["filter_whitelisted", "--force"]) + assert config_records() == existing + end + + test "filter_whitelisted doesn't crash when whitelist is disabled" do + clear_config(:database_config_whitelist, false) + + existing = config_records() + MixTask.run(["filter_whitelisted", "--force"]) + assert config_records() == existing + end end end From 0b871ff1f298d84c7b3c12444ee923bcfb1ac02a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 12:32:10 +0400 Subject: [PATCH 108/317] ConfigController: Don't allow updating the whitelist --- .../controllers/config_controller_test.exs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index 7cb4ec938..e62d95fad 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -1220,6 +1220,31 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do assert Application.get_env(:not_real, :anything) == "value6" end + test "doesn't allow updating the database_config_whitelist itself", %{conn: conn} do + original_whitelist = Pleroma.Config.get(:database_config_whitelist) + + refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":database_config_whitelist", + value: [%{"tuple" => [":pleroma", ":key1"]}] + } + ] + }) + + %{"configs" => configs} = json_response_and_validate_schema(conn, 200) + + assert configs == [] + assert Pleroma.Config.get(:database_config_whitelist) == original_whitelist + refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist) + end + test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do assert conn |> put_req_header("content-type", "application/json") From 49f9ab3034b9809c53369cf0ade33dac8409faa4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 13:02:18 +0400 Subject: [PATCH 109/317] Cheatsheet: Fix double slash --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index b156c74cf..9efa1c8b3 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1133,7 +1133,7 @@ Boolean, enables/disables in-database configuration. Read [Transferring the conf List of valid configuration sections which are allowed to be configured from the database. Settings stored in the database before the whitelist is configured are still applied. Consider running the `mix pleroma.config filter_whitelisted` task -after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration//CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database) +after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration/CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database) for more information. Example: From 117b0bd79ed21338a46a558061cc54e86313f5b5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 13:03:02 +0400 Subject: [PATCH 110/317] Config: Don't crash on falsy whitelist config --- lib/mix/tasks/pleroma/config.ex | 68 +++++++++++++++++---------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index fc6982ea8..c5f2e9b0a 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -249,39 +249,43 @@ defmodule Mix.Tasks.Pleroma.Config do whitelisted_configs = Pleroma.Config.get(:database_config_whitelist) - whitelisted_groups = - whitelisted_configs - |> Enum.filter(fn - {_group} -> true - _ -> false - end) - |> Enum.map(fn {group} -> group end) - - whitelisted_keys = - whitelisted_configs - |> Enum.filter(fn - {_group, _key} -> true - _ -> false - end) - - filtered = - from(c in ConfigDB) - |> Repo.all() - |> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys)) - - if not Enum.empty?(filtered) do - shell_info("The following settings will be removed from ConfigDB:\n") - Enum.each(filtered, &dump(&1)) - - if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do - filtered_ids = Enum.map(filtered, fn %{id: id} -> id end) - - Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids)) - else - shell_error("No changes made.") - end - else + if whitelisted_configs in [nil, false] do shell_error("No unwanted settings in ConfigDB. No changes made.") + else + whitelisted_groups = + whitelisted_configs + |> Enum.filter(fn + {_group} -> true + _ -> false + end) + |> Enum.map(fn {group} -> group end) + + whitelisted_keys = + whitelisted_configs + |> Enum.filter(fn + {_group, _key} -> true + _ -> false + end) + + filtered = + from(c in ConfigDB) + |> Repo.all() + |> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys)) + + if not Enum.empty?(filtered) do + shell_info("The following settings will be removed from ConfigDB:\n") + Enum.each(filtered, &dump(&1)) + + if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do + filtered_ids = Enum.map(filtered, fn %{id: id} -> id end) + + Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids)) + else + shell_error("No changes made.") + end + else + shell_error("No unwanted settings in ConfigDB. No changes made.") + end end end From a4fb651fac4416db7d5102672f10c9a9c3ce14fc Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 13:30:07 +0400 Subject: [PATCH 111/317] ConfigController: Don't allow whitelist modification. --- lib/pleroma/web/admin_api/controllers/config_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index f3adeb35a..3eba03525 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -174,7 +174,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do end end - defp whitelisted_config?(:pleroma, :database_config_whitelist), do: false + defp whitelisted_config?(":pleroma", ":database_config_whitelist"), do: false defp whitelisted_config?(group, key) do if whitelisted_configs = Config.get(:database_config_whitelist) do From ddc1a86f400e866d65aeaeb88403161d184dfaa5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 22 Jan 2026 23:01:11 +0100 Subject: [PATCH 112/317] MastoAPI AccountView: Add mute/block expiry to the relationship key --- .../web/mastodon_api/views/account_view.ex | 40 ++++++---- .../mastodon_api/views/account_view_test.exs | 78 ++++++++++++++++++- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a7d994593..909212711 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -124,6 +124,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do target, &User.blocks_user?(&1, &2) ), + block_expires_at: maybe_put_block_expires_at(user_relationships, target, reading_user), blocked_by: UserRelationship.exists?( user_relationships, @@ -140,6 +141,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do target, &User.mutes?(&1, &2) ), + mute_expires_at: maybe_put_mute_expires_at(user_relationships, target, reading_user), muting_notifications: UserRelationship.exists?( user_relationships, @@ -343,8 +345,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) - |> maybe_put_mute_expires_at(user, opts[:for], opts) - |> maybe_put_block_expires_at(user, opts[:for], opts) |> maybe_show_birthday(user, opts[:for]) end @@ -472,26 +472,32 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_email_address(data, _, _), do: data - defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do - Map.put( - data, - :mute_expires_at, - UserRelationship.get_mute_expire_date(target, user) - ) + defp maybe_put_mute_expires_at(user_relationships, %User{} = target, %User{} = user) do + cond do + UserRelationship.exists?(user_relationships, :mute, user, target, &User.mutes_user?(&1, &2)) -> + UserRelationship.get_mute_expire_date(user, target) + + true -> + nil + end end - defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(user_relationships, %User{} = target, %User{} = user) do + cond do + UserRelationship.exists?( + user_relationships, + :block, + user, + target, + &User.blocks_user?(&1, &2) + ) -> + UserRelationship.get_block_expire_date(user, target) - defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do - Map.put( - data, - :block_expires_at, - UserRelationship.get_block_expire_date(target, user) - ) + true -> + nil + end end - defp maybe_put_block_expires_at(data, _, _, _), do: data - defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data |> Kernel.put_in([:pleroma, :birthday], user.birthday) diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index bd151cc61..c230cf653 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -439,8 +439,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do following: false, followed_by: false, blocking: false, + block_expires_at: nil, blocked_by: false, muting: false, + mute_expires_at: nil, muting_notifications: false, subscribing: false, notifying: false, @@ -536,6 +538,53 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do test_relationship_rendering(user, other_user, expected) end + test "represent a relationship for the blocking and blocked user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, user, other_user} = User.follow(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60}) + {:ok, _user_relationship} = User.block(other_user, user) + + expected = + Map.merge( + @blank_response, + %{ + following: false, + blocking: true, + block_expires_at: date, + blocked_by: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the muting user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, _user_relationship} = + User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) + + expected = + Map.merge( + @blank_response, + %{ + muting: true, + mute_expires_at: date, + muting_notifications: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + test "represent a relationship for the user blocking a domain" do user = insert(:user) other_user = insert(:user, ap_id: "https://bad.site/users/other_user") @@ -856,12 +905,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) %{ - mute_expires_at: mute_expires_at - } = AccountView.render("show.json", %{user: other_user, for: user, mutes: true}) + pleroma: %{ + relationship: %{ + mute_expires_at: mute_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( mute_expires_at, DateTime.utc_now() |> DateTime.add(24 * 60 * 60) ) in -3..3 end + + test "renders block expiration date" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = + User.block(user, other_user, %{duration: 24 * 60 * 60}) + + %{ + pleroma: %{ + relationship: %{ + block_expires_at: block_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) + + assert DateTime.diff( + block_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end end From 56b3db71ff081c069263297c8bbe8b316129cc78 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 24 Jan 2026 21:33:49 +0100 Subject: [PATCH 113/317] MastoAPI AccountView: Readd block/mute expiry outside the relationship --- .../web/mastodon_api/views/account_view.ex | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 909212711..e215b073e 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -345,6 +345,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) + |> maybe_put_mute_expires_at(user, opts[:for], relationship) + |> maybe_put_block_expires_at(user, opts[:for], relationship) |> maybe_show_birthday(user, opts[:for]) end @@ -482,6 +484,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end end + defp maybe_put_mute_expires_at(data, %User{} = target, %User{} = user, relationship) do + cond do + Map.has_key?(relationship, :mute_expires_at) -> + Map.put(data, :mute_expires_at, relationship.mute_expires_at) + + User.mutes_user?(user, target) -> + Map.put(data, :mute_expires_at, UserRelationship.get_mute_expire_date(user, target)) + + true -> + Map.put(data, :mute_expires_at, nil) + end + end + + defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(user_relationships, %User{} = target, %User{} = user) do cond do UserRelationship.exists?( @@ -498,6 +515,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end end + defp maybe_put_block_expires_at(data, %User{} = target, %User{} = user, relationship) do + cond do + Map.has_key?(relationship, :block_expires_at) -> + Map.put(data, :block_expires_at, relationship.block_expires_at) + + User.blocks_user?(user, target) -> + Map.put(data, :block_expires_at, UserRelationship.get_block_expire_date(user, target)) + + true -> + Map.put(data, :block_expires_at, nil) + end + end + + defp maybe_put_block_expires_at(data, _, _, _), do: data + defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data |> Kernel.put_in([:pleroma, :birthday], user.birthday) From 9b5afe9cd4064a4ca7f9a9568e9cde0e48ae8db2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 24 Jan 2026 21:35:27 +0100 Subject: [PATCH 114/317] MastoAPI AccountView AccountController: Add more block/mute expiry tests --- .../controllers/account_controller_test.exs | 103 ++++++++++++++++-- .../mastodon_api/views/account_view_test.exs | 6 +- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 02da781dd..ea98b53a8 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1901,7 +1901,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationships} = User.mute(user, other_user1) {:ok, _user_relationships} = User.mute(user, other_user2) - {:ok, _user_relationships} = User.mute(user, other_user3) + {:ok, _user_relationships} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -1937,6 +1943,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id3}] = result + + result = + conn + |> get("/api/v1/mutes") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "mute_expires_at" => ^date}, + %{"id" => ^id2, "mute_expires_at" => nil}, + %{"id" => ^id1, "mute_expires_at" => nil} + ] = result end test "list of mutes with with_relationships parameter" do @@ -1951,20 +1968,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.mute(user, other_user1) {:ok, _} = User.mute(user, other_user2) - {:ok, _} = User.mute(user, other_user3) + {:ok, _} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => ^date, + "followed_by" => true + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } } ] = conn @@ -1980,7 +2021,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationship} = User.block(user, other_user1) {:ok, _user_relationship} = User.block(user, other_user3) - {:ok, _user_relationship} = User.block(user, other_user2) + {:ok, _user_relationship} = User.block(user, other_user2, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -2045,6 +2092,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id1}] = result + + result = + conn + |> assign(:user, user) + |> get("api/v1/blocks") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "block_expires_at" => nil}, + %{"id" => ^id2, "block_expires_at" => ^date}, + %{"id" => ^id1, "block_expires_at" => nil} + ] = result end test "list of blocks with with_relationships parameter" do @@ -2059,20 +2118,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.block(user, other_user1) {:ok, _} = User.block(user, other_user2) - {:ok, _} = User.block(user, other_user3) + {:ok, _} = User.block(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => ^date, + "followed_by" => false + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } } ] = conn diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c230cf653..6984442cc 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -909,7 +909,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ mute_expires_at: mute_expires_at } - } + }, + mute_expires_at: mute_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( @@ -930,7 +931,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ block_expires_at: block_expires_at } - } + }, + block_expires_at: block_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( From 01d94a01358684223c5ed9e4348aee448ae2bf44 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 26 Dec 2025 05:08:52 +0000 Subject: [PATCH 115/317] Revert "Merge branch 'revert-cdd6df06' into 'develop'" This reverts merge request !4411 --- changelog.d/hackney.change | 1 + mix.exs | 2 +- mix.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/hackney.change diff --git a/changelog.d/hackney.change b/changelog.d/hackney.change new file mode 100644 index 000000000..3158cfc77 --- /dev/null +++ b/changelog.d/hackney.change @@ -0,0 +1 @@ +Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation diff --git a/mix.exs b/mix.exs index 9e7ce7d4e..ac998e9ee 100644 --- a/mix.exs +++ b/mix.exs @@ -210,7 +210,7 @@ defmodule Pleroma.Mixfile do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.5", only: :test}, {:covertool, "~> 2.0", only: :test}, - {:hackney, "~> 1.18.0", override: true}, + {:hackney, "~> 1.25.0", override: true}, {:mox, "~> 1.0", only: :test}, {:websockex, "~> 0.4.3", only: :test}, {:benchee, "~> 1.0", only: :benchmark}, diff --git a/mix.lock b/mix.lock index 3599f62b4..b8c9e240d 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"}, @@ -59,7 +59,7 @@ "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, - "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"}, From b85507c764e03076844d67d4480609faecc6fa88 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 21:16:04 +0400 Subject: [PATCH 116/317] http(hackney): disable adapter redirects by default Hackney 1.25.x has redirect handling issues behind CONNECT proxies and with pools. Disable hackney-level redirects and rely on Tesla.Middleware.FollowRedirects instead. Also default to with_body: true so redirects can be followed reliably. --- lib/pleroma/http/adapter_helper/hackney.ex | 5 +++-- test/pleroma/http/adapter_helper/hackney_test.exs | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index f3451cf9c..2b1502001 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -6,8 +6,9 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do @behaviour Pleroma.HTTP.AdapterHelper @defaults [ - follow_redirect: true, - force_redirect: true + follow_redirect: false, + force_redirect: false, + with_body: true ] @spec options(keyword(), URI.t()) :: keyword() diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs index 57ce4728c..343bdb800 100644 --- a/test/pleroma/http/adapter_helper/hackney_test.exs +++ b/test/pleroma/http/adapter_helper/hackney_test.exs @@ -16,6 +16,14 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do describe "options/2" do setup do: clear_config([:http, :adapter], a: 1, b: 2) + test "uses redirect-safe defaults", %{uri: uri} do + opts = Hackney.options([], uri) + + assert opts[:follow_redirect] == false + assert opts[:force_redirect] == false + assert opts[:with_body] == true + end + test "add proxy and opts from config", %{uri: uri} do opts = Hackney.options([proxy: "localhost:8123"], uri) From 500340fc8214ff91a4f4e165b84670f998392664 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 21:16:26 +0400 Subject: [PATCH 117/317] test(http): cover pooled redirect with hackney Reproduces the Hackney 1.25 pooled redirect cleanup issue which can surface as :req_not_found when the adapter returns a Ref and the body is later fetched. --- .../http/hackney_redirect_regression_test.exs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 test/pleroma/http/hackney_redirect_regression_test.exs diff --git a/test/pleroma/http/hackney_redirect_regression_test.exs b/test/pleroma/http/hackney_redirect_regression_test.exs new file mode 100644 index 000000000..61389aa7e --- /dev/null +++ b/test/pleroma/http/hackney_redirect_regression_test.exs @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.HackneyRedirectRegressionTest do + use ExUnit.Case, async: false + + alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper + + setup do + {:ok, _} = Application.ensure_all_started(:hackney) + + {:ok, server} = start_server() + on_exit(fn -> stop_server(server) end) + + {:ok, server: server} + end + + test "pooled redirects work with follow_redirect disabled", %{server: server} do + url = "#{server.base_url}/redirect" + uri = URI.parse(url) + + adapter_opts = + HackneyAdapterHelper.options( + [pool: :media, follow_redirect: false, no_proxy_env: true], + uri + ) + + client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney) + + assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = + Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts]) + end + + defp start_server do + {:ok, listener} = + :gen_tcp.listen(0, [ + :binary, + active: false, + packet: :raw, + reuseaddr: true, + ip: {127, 0, 0, 1} + ]) + + {:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener) + + {:ok, acceptor} = + Task.start_link(fn -> + accept_loop(listener) + end) + + {:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}} + end + + defp stop_server(%{listener: listener, acceptor: acceptor}) do + :ok = :gen_tcp.close(listener) + + if Process.alive?(acceptor) do + Process.exit(acceptor, :normal) + end + end + + defp accept_loop(listener) do + case :gen_tcp.accept(listener) do + {:ok, socket} -> + serve(socket) + accept_loop(listener) + + {:error, :closed} -> + :ok + + {:error, _reason} -> + :ok + end + end + + defp serve(socket) do + with {:ok, data} <- recv_headers(socket), + {:ok, path} <- parse_path(data) do + case path do + "/redirect" -> + send_response(socket, 302, "Found", [{"Location", "/final"}], "") + + "/final" -> + send_response(socket, 200, "OK", [], "ok") + + _ -> + send_response(socket, 404, "Not Found", [], "not found") + end + else + _ -> :ok + end + + :gen_tcp.close(socket) + end + + defp recv_headers(socket, acc \\ <<>>) do + case :gen_tcp.recv(socket, 0, 1_000) do + {:ok, data} -> + acc = acc <> data + + if :binary.match(acc, "\r\n\r\n") != :nomatch do + {:ok, acc} + else + if byte_size(acc) > 8_192 do + {:error, :too_large} + else + recv_headers(socket, acc) + end + end + + {:error, _} = error -> + error + end + end + + defp parse_path(data) do + case String.split(data, "\r\n", parts: 2) do + [request_line | _] -> + case String.split(request_line, " ") do + [_method, path, _protocol] -> {:ok, path} + _ -> {:error, :invalid_request} + end + + _ -> + {:error, :invalid_request} + end + end + + defp send_response(socket, status, reason, headers, body) do + base_headers = + [ + {"Content-Length", Integer.to_string(byte_size(body))}, + {"Connection", "close"} + ] ++ headers + + iodata = + [ + "HTTP/1.1 ", + Integer.to_string(status), + " ", + reason, + "\r\n", + Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end), + "\r\n", + body + ] + + :gen_tcp.send(socket, iodata) + end +end From f2ad6100f20b07a4f620cfa4edbccc054a85af7d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 21:16:39 +0400 Subject: [PATCH 118/317] test(http): reproduce hackney follow_redirect crash via CONNECT proxy Hackney 1.25 crashes when follow_redirect is enabled behind an HTTPS CONNECT proxy and the Location header is relative (hackney_http_connect transport). This test demonstrates the failure and verifies Tesla-level redirects work when hackney redirects are disabled. --- test/fixtures/server.pem | 18 + ...ackney_follow_redirect_regression_test.exs | 325 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 test/fixtures/server.pem create mode 100644 test/pleroma/http/hackney_follow_redirect_regression_test.exs diff --git a/test/fixtures/server.pem b/test/fixtures/server.pem new file mode 100644 index 000000000..8bd3d6d05 --- /dev/null +++ b/test/fixtures/server.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICpDCCAYwCCQC0vCQAnSoGdzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwHhcNMjYwMTE2MTY1ODE5WhcNMzYwMTE0MTY1ODE5WjAUMRIwEAYD +VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq +dZ4O2upZqwIo1eK5KrW1IIsjkfsFK8hE7Llh+4axcesiUKot0ib1CUhRSYiL1DLO +CIYQOw8IKQDVSC4JWAX9SsnX4W8dwexMQuSQG7/IKX2auC1bNNySFvoqM6Gq3GL9 +MqBFonZGXDPZu8fmxsI/2p9+2GK13F+HXgoLlXSCoO3XELJaBmjv29tgxxWRxCiH +m4u0briSxgUEx+CctpKPvGDmLaoIOIhjtuoG6OjkeWUOp6jDcteazO23VxPyF5cS +NbRJgm8AckrTQ6wbWSnhyqF8rPEsIc0ZAlUdDEs5fL3sjugc566FvE+GOkZIEyDD +tgWbc4Ne+Kp/nnt6oVxpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADv+J1DTok8V +MKVKo0hsRnHTeJQ2+EIgOspuYlEzez3PysOZH6diAQxO2lzuo9LKxP3hnmw17XO/ +P2oCzYyb9/P58VY/gr4UDIfuhgcE0cVfdsRhVId/I2FW6VP2f5q1TGbDUxSsVIlG +6hufn1aLBu90LtEbDkHqbnD05yYPwdqzWg4TrOXbX+jBhQrXJJdB3W7KTgozjRQw +F7+/2IyXoxXuxcwQBQlYhUbvGlsFqFpP/6cz2al5i5pNUkiNaSYwlRmuwa7zoTft +tHf57dhfXIpXET2BaJM6DSjDOOG/QleRXkvkTI5J21q+Bo+XnOzo19p4cZKJpTFC +SNgrftyNh3k= +-----END CERTIFICATE----- + diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs new file mode 100644 index 000000000..ea631024c --- /dev/null +++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs @@ -0,0 +1,325 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do + use ExUnit.Case, async: false + + setup do + {:ok, _} = Application.ensure_all_started(:hackney) + + {:ok, tls_server} = start_tls_redirect_server() + {:ok, proxy} = start_connect_proxy() + + on_exit(fn -> + stop_connect_proxy(proxy) + stop_tls_redirect_server(tls_server) + end) + + {:ok, tls_server: tls_server, proxy: proxy} + end + + test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/redirect" + + opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: true, + force_redirect: true + ] + + {pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end) + + assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000 + + assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or + match?({:function_clause, _}, reason) + end + + test "redirects work via proxy when hackney follow_redirect is disabled", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/redirect" + + adapter_opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: false, + force_redirect: false, + with_body: true + ] + + client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney) + + assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = + Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts]) + end + + defp start_tls_redirect_server do + certfile = Path.expand("../../fixtures/server.pem", __DIR__) + keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__) + + {:ok, listener} = + :ssl.listen(0, [ + :binary, + certfile: certfile, + keyfile: keyfile, + reuseaddr: true, + active: false, + packet: :raw, + ip: {127, 0, 0, 1} + ]) + + {:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener) + + {:ok, acceptor} = + Task.start_link(fn -> + accept_tls_loop(listener) + end) + + {:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}} + end + + defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do + :ok = :ssl.close(listener) + + if Process.alive?(acceptor) do + Process.exit(acceptor, :normal) + end + end + + defp accept_tls_loop(listener) do + case :ssl.transport_accept(listener) do + {:ok, socket} -> + _ = Task.start(fn -> serve_tls(socket) end) + accept_tls_loop(listener) + + {:error, :closed} -> + :ok + + {:error, _reason} -> + :ok + end + end + + defp serve_tls(tcp_socket) do + with {:ok, ssl_socket} <- :ssl.handshake(tcp_socket, 2_000), + {:ok, data} <- recv_ssl_headers(ssl_socket), + {:ok, path} <- parse_path(data) do + case path do + "/redirect" -> + send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "") + + "/final" -> + send_ssl_response(ssl_socket, 200, "OK", [], "ok") + + _ -> + send_ssl_response(ssl_socket, 404, "Not Found", [], "not found") + end + + :ssl.close(ssl_socket) + else + _ -> + _ = :gen_tcp.close(tcp_socket) + :ok + end + end + + defp recv_ssl_headers(socket, acc \\ <<>>) do + case :ssl.recv(socket, 0, 1_000) do + {:ok, data} -> + acc = acc <> data + + if :binary.match(acc, "\r\n\r\n") != :nomatch do + {:ok, acc} + else + if byte_size(acc) > 8_192 do + {:error, :too_large} + else + recv_ssl_headers(socket, acc) + end + end + + {:error, _} = error -> + error + end + end + + defp send_ssl_response(socket, status, reason, headers, body) do + base_headers = + [ + {"Content-Length", Integer.to_string(byte_size(body))}, + {"Connection", "close"} + ] ++ headers + + iodata = + [ + "HTTP/1.1 ", + Integer.to_string(status), + " ", + reason, + "\r\n", + Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end), + "\r\n", + body + ] + + :ssl.send(socket, iodata) + end + + defp start_connect_proxy do + {:ok, listener} = + :gen_tcp.listen(0, [ + :binary, + active: false, + packet: :raw, + reuseaddr: true, + ip: {127, 0, 0, 1} + ]) + + {:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener) + + {:ok, acceptor} = + Task.start_link(fn -> + accept_proxy_loop(listener) + end) + + {:ok, %{listener: listener, acceptor: acceptor, proxy_url: "127.0.0.1:#{port}"}} + end + + defp stop_connect_proxy(%{listener: listener, acceptor: acceptor}) do + :ok = :gen_tcp.close(listener) + + if Process.alive?(acceptor) do + Process.exit(acceptor, :normal) + end + end + + defp accept_proxy_loop(listener) do + case :gen_tcp.accept(listener) do + {:ok, socket} -> + _ = Task.start(fn -> serve_proxy(socket) end) + accept_proxy_loop(listener) + + {:error, :closed} -> + :ok + + {:error, _reason} -> + :ok + end + end + + defp serve_proxy(client_socket) do + with {:ok, {headers, rest}} <- recv_tcp_headers(client_socket), + {:ok, {host, port}} <- parse_connect(headers), + {:ok, upstream_socket} <- connect_upstream(host, port) do + :gen_tcp.send(client_socket, "HTTP/1.1 200 Connection established\r\n\r\n") + + if rest != <<>> do + :gen_tcp.send(upstream_socket, rest) + end + + tunnel(client_socket, upstream_socket) + else + _ -> + :gen_tcp.close(client_socket) + :ok + end + end + + defp tunnel(client_socket, upstream_socket) do + parent = self() + _ = spawn_link(fn -> forward(client_socket, upstream_socket, parent) end) + _ = spawn_link(fn -> forward(upstream_socket, client_socket, parent) end) + + receive do + :tunnel_closed -> :ok + after + 10_000 -> :ok + end + + :gen_tcp.close(client_socket) + :gen_tcp.close(upstream_socket) + end + + defp forward(from_socket, to_socket, parent) do + case :gen_tcp.recv(from_socket, 0, 10_000) do + {:ok, data} -> + _ = :gen_tcp.send(to_socket, data) + forward(from_socket, to_socket, parent) + + {:error, _reason} -> + send(parent, :tunnel_closed) + :ok + end + end + + defp recv_tcp_headers(socket, acc \\ <<>>) do + case :gen_tcp.recv(socket, 0, 1_000) do + {:ok, data} -> + acc = acc <> data + + case :binary.match(acc, "\r\n\r\n") do + :nomatch -> + if byte_size(acc) > 8_192 do + {:error, :too_large} + else + recv_tcp_headers(socket, acc) + end + + {idx, _len} -> + split_at = idx + 4 + <> = acc + {:ok, {headers, rest}} + end + + {:error, _} = error -> + error + end + end + + defp parse_connect(data) do + with [request_line | _] <- String.split(data, "\r\n", trim: true), + ["CONNECT", hostport | _] <- String.split(request_line, " ", parts: 3), + [host, port_str] <- String.split(hostport, ":", parts: 2), + {port, ""} <- Integer.parse(port_str) do + {:ok, {host, port}} + else + _ -> {:error, :invalid_connect} + end + end + + defp connect_upstream(host, port) do + address = + case :inet.parse_address(String.to_charlist(host)) do + {:ok, ip} -> ip + {:error, _} -> String.to_charlist(host) + end + + :gen_tcp.connect(address, port, [:binary, active: false, packet: :raw], 1_000) + end + + defp parse_path(data) do + case String.split(data, "\r\n", parts: 2) do + [request_line | _] -> + case String.split(request_line, " ") do + [_method, path, _protocol] -> {:ok, path} + _ -> {:error, :invalid_request} + end + + _ -> + {:error, :invalid_request} + end + end +end From 71a4b8d0f2cdeba56dfcfdc034f797f534bb9d76 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 24 Dec 2025 14:35:54 -0800 Subject: [PATCH 119/317] In-house redirect handler for mediaproxy with Hackney adapter Also ensure we always pass an absolute URL to Hackney when parsing a redirect response (cherry picked from commit 00ac6bce8d244eec7e2460358296619e5cacba6b) --- changelog.d/hackney-mediaproxy.change | 1 + lib/pleroma/reverse_proxy/client/hackney.ex | 44 ++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 changelog.d/hackney-mediaproxy.change diff --git a/changelog.d/hackney-mediaproxy.change b/changelog.d/hackney-mediaproxy.change new file mode 100644 index 000000000..10dfb0775 --- /dev/null +++ b/changelog.d/hackney-mediaproxy.change @@ -0,0 +1 @@ +Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index 0aa5f5715..7ccd28bb1 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -5,6 +5,20 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @behaviour Pleroma.ReverseProxy.Client + # redirect handler from Pleb, slightly modified to work with Hackney + # https://declin.eu/objects/d4f38e62-5429-4614-86d1-e8fc16e6bf33 + # https://github.com/benoitc/hackney/issues/273 + @redirect_statuses [301, 302, 303, 307, 308] + defp absolute_redirect_url(original_url, resp_headers) do + location = + Enum.find(resp_headers, fn {header, _location} -> + String.downcase(header) == "location" + end) + + URI.merge(original_url, elem(location, 1)) + |> URI.to_string() + end + @impl true def request(method, url, headers, body, opts \\ []) do opts = @@ -12,7 +26,35 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do path end) - :hackney.request(method, url, headers, body, opts) + if opts[:follow_redirect] != false do + {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) + res = :hackney.request(method, url, headers, body, req_opts) + + case res do + {:ok, code, resp_headers, _client} when code in @redirect_statuses -> + :hackney.request( + method, + absolute_redirect_url(url, resp_headers), + headers, + body, + req_opts + ) + + {:ok, code, resp_headers} when code in @redirect_statuses -> + :hackney.request( + method, + absolute_redirect_url(url, resp_headers), + headers, + body, + req_opts + ) + + _ -> + res + end + else + :hackney.request(method, url, headers, body, opts) + end end @impl true From 89360664272bb94641802df26a81b90fae80420b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 22:00:36 +0400 Subject: [PATCH 120/317] test(http): cover reverse proxy redirects via CONNECT proxy Exercises Pleroma.ReverseProxy.Client.Hackney with follow_redirect enabled behind an HTTPS CONNECT proxy, ensuring the client follows a relative redirect and can stream the final body. --- ...ackney_follow_redirect_regression_test.exs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs index ea631024c..71fda4479 100644 --- a/test/pleroma/http/hackney_follow_redirect_regression_test.exs +++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs @@ -66,6 +66,36 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts]) end + test "reverse proxy hackney client follows redirects via proxy without crashing", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/redirect" + + opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: true + ] + + assert {:ok, 200, _headers, ref} = + Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts) + + assert collect_body(ref) == "ok" + Pleroma.ReverseProxy.Client.Hackney.close(ref) + end + + defp collect_body(ref, acc \\ "") do + case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do + :done -> acc + {:ok, data, _ref} -> collect_body(ref, acc <> data) + {:error, error} -> flunk("stream_body failed: #{inspect(error)}") + end + end + defp start_tls_redirect_server do certfile = Path.expand("../../fixtures/server.pem", __DIR__) keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__) From e40bedc601eeef73e0b3e3964110534a7710e16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 7 Sep 2025 23:28:58 +0200 Subject: [PATCH 121/317] Add v1/instance/domain_blocks endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/instance-domain-blocks.add | 1 + .../api_spec/operations/instance_operation.ex | 31 ++++++++++++ .../controllers/instance_controller.ex | 7 ++- .../web/mastodon_api/views/instance_view.ex | 49 +++++++++++++++++++ lib/pleroma/web/router.ex | 1 + .../controllers/instance_controller_test.exs | 27 ++++++++++ 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 changelog.d/instance-domain-blocks.add diff --git a/changelog.d/instance-domain-blocks.add b/changelog.d/instance-domain-blocks.add new file mode 100644 index 000000000..85f01c5c2 --- /dev/null +++ b/changelog.d/instance-domain-blocks.add @@ -0,0 +1 @@ +Add v1/instance/domain_blocks endpoint diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 911ffb994..d8b2901d3 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -57,6 +57,22 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do } end + def domain_blocks_operation do + %Operation{ + tags: ["Instance misc"], + summary: "Retrieve instance domain blocks", + operationId: "InstanceController.domain_blocks", + responses: %{ + 200 => + Operation.response( + "Array of domain blocks", + "application/json", + array_of_domain_blocks() + ) + } + } + end + def translation_languages_operation do %Operation{ tags: ["Instance misc"], @@ -420,4 +436,19 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do } } end + + defp array_of_domain_blocks do + %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + domain: %Schema{type: :string}, + digest: %Schema{type: :string}, + severity: %Schema{type: :string}, + comment: %Schema{type: :string} + } + } + } + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 0f74c1dff..cf5bbf16e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:skip_auth when action in [:show, :show2, :peers]) + plug(:skip_auth) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation @@ -31,6 +31,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do render(conn, "rules.json") end + @doc "GET /api/v1/instance/domain_blocks" + def domain_blocks(conn, _params) do + render(conn, "domain_blocks.json") + end + @doc "GET /api/v1/instance/translation_languages" def translation_languages(conn, _params) do render(conn, "translation_languages.json") diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 57372248f..18c6c0f80 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -5,11 +5,18 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do use Pleroma.Web, :view + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + alias Pleroma.Config alias Pleroma.Web.ActivityPub.MRF @mastodon_api_level "2.7.2" + @block_severities %{ + federated_timeline_removal: "silence", + reject: "suspend" + } + def render("show.json", _) do instance = Config.get(:instance) @@ -90,6 +97,48 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do } end + def render("domain_blocks.json", _) do + if Config.get([:mrf, :transparency]) do + exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples() + + domain_blocks = + Config.get(:mrf_simple) + |> Enum.map(fn {rule, instances} -> + instances + |> Enum.map(fn + {host, reason} when not_empty_string(host) and not_empty_string(reason) -> + {host, reason} + + {host, _reason} when not_empty_string(host) -> + {host, ""} + + host when not_empty_string(host) -> + {host, ""} + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + |> Enum.reject(fn {host, _} -> + host in exclusions or not Map.has_key?(@block_severities, rule) + end) + |> Enum.map(fn {host, reason} -> + %{ + domain: host, + digest: :crypto.hash(:sha256, host) |> Base.encode16(case: :lower), + severity: Map.get(@block_severities, rule), + comment: reason + } + end) + end) + |> List.flatten() + + domain_blocks + else + [] + end + end + def render("translation_languages.json", _) do with true <- Pleroma.Language.Translation.configured?(), {:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index da9626147..a0fa7b3e3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -813,6 +813,7 @@ defmodule Pleroma.Web.Router do get("/instance", InstanceController, :show) get("/instance/peers", InstanceController, :peers) get("/instance/rules", InstanceController, :rules) + get("/instance/domain_blocks", InstanceController, :domain_blocks) get("/instance/translation_languages", InstanceController, :translation_languages) get("/statuses", StatusController, :index) diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index 10c0b6ea7..e0c9091e0 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -153,6 +153,33 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do ] = result["rules"] end + describe "instance domain blocks" do + setup do + clear_config([:mrf_simple, :reject], [{"fediverse.pl", "uses pl-fe"}]) + end + + test "get instance domain blocks", %{conn: conn} do + conn = get(conn, "/api/v1/instance/domain_blocks") + + assert [ + %{ + "comment" => "uses pl-fe", + "digest" => "55e3f44aefe7eb022d3b1daaf7396cabf7f181bf6093c8ea841e30c9fc7d8226", + "domain" => "fediverse.pl", + "severity" => "suspend" + } + ] == json_response_and_validate_schema(conn, 200) + end + + test "returns empty array if mrf transparency is disabled", %{conn: conn} do + clear_config([:mrf, :transparency], false) + + conn = get(conn, "/api/v1/instance/domain_blocks") + + assert [] == json_response_and_validate_schema(conn, 200) + end + end + test "translation languages matrix", %{conn: conn} do clear_config([Pleroma.Language.Translation, :provider], TranslationMock) From 56006345746b97f3b8d267eec4c2cbbff137d4d5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 31 Dec 2025 18:52:14 +0400 Subject: [PATCH 122/317] Installation: Add Release-Via-Docker option --- docs/installation/otp_en.md | 3 + docs/installation/release_to_docker_en.md | 61 +++++++++++++++++ installation/release-to-docker/Dockerfile | 24 +++++++ installation/release-to-docker/README.md | 66 +++++++++++++++++++ .../release-to-docker/docker-compose.yml | 22 +++++++ .../pleroma-host-release-entrypoint.sh | 19 ++++++ .../release-to-docker/pleroma.service | 16 +++++ 7 files changed, 211 insertions(+) create mode 100644 docs/installation/release_to_docker_en.md create mode 100644 installation/release-to-docker/Dockerfile create mode 100644 installation/release-to-docker/README.md create mode 100644 installation/release-to-docker/docker-compose.yml create mode 100644 installation/release-to-docker/pleroma-host-release-entrypoint.sh create mode 100644 installation/release-to-docker/pleroma.service diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 86efa27f8..123dccf3a 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -13,6 +13,9 @@ You will be running commands as root. If you aren't root already, please elevate Similarly to other binaries, OTP releases tend to be only compatible with the distro they are built on, as such this guide focuses only on Debian/Ubuntu and Alpine. +!!! note + If you get `GLIBC_... not found` errors on Debian/Ubuntu, you can run the OTP release from `/opt/pleroma` inside a newer distro container without upgrading the host. See [`release_to_docker_en.md`](release_to_docker_en.md). + ### Detecting flavour Paste the following into the shell: diff --git a/docs/installation/release_to_docker_en.md b/docs/installation/release_to_docker_en.md new file mode 100644 index 000000000..a1fd165ac --- /dev/null +++ b/docs/installation/release_to_docker_en.md @@ -0,0 +1,61 @@ +# Running OTP releases via Docker (glibc shim) + +Pleroma OTP releases are built on specific distros. If your host OS is older than +the build environment, you may hit runtime linker errors such as: + +``` +/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found +``` + +If you don't want to upgrade your host OS, you can run the existing OTP release +from `/opt/pleroma` inside an Ubuntu 24.04 container while keeping your existing +host config and data directories. + +This approach uses a small "shim" container image to provide a newer `glibc`. +It is **not** the official Pleroma Docker image. + +## Requirements + +- Docker Engine + the Docker Compose plugin on the host +- Root access (or equivalent access to the Docker socket) +- Existing OTP release in `/opt/pleroma` +- Existing config in `/etc/pleroma` and data in `/var/lib/pleroma` + +## Setup + +1. Copy the provided templates: + + ```sh + mkdir -p /etc/pleroma/container + cp -a /opt/pleroma/installation/release-to-docker/* /etc/pleroma/container/ + ``` + +2. Build the shim image: + + ```sh + cd /etc/pleroma/container + docker compose build + ``` + +3. Replace your systemd unit: + + ```sh + cp /etc/pleroma/container/pleroma.service /etc/systemd/system/pleroma.service + systemctl daemon-reload + systemctl enable --now pleroma + journalctl -u pleroma -f + ``` + +## Running migrations / `pleroma_ctl` + +Migrations are run automatically by default when the container starts. You can +disable this by setting `PLEROMA_RUN_MIGRATIONS=0` in +`/etc/pleroma/container/docker-compose.yml`. + +To run admin commands inside the container: + +```sh +cd /etc/pleroma/container +docker compose exec pleroma /opt/pleroma/bin/pleroma_ctl status +docker compose run --rm --no-deps pleroma /opt/pleroma/bin/pleroma_ctl migrate +``` diff --git a/installation/release-to-docker/Dockerfile b/installation/release-to-docker/Dockerfile new file mode 100644 index 000000000..ddff4570d --- /dev/null +++ b/installation/release-to-docker/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + gosu \ + libstdc++6 \ + libncurses6 libncursesw6 \ + openssl libssl3 \ + libmagic1t64 file \ + postgresql-client \ + ffmpeg imagemagick libimage-exiftool-perl \ + libvips42t64 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/pleroma + +COPY pleroma-host-release-entrypoint.sh /usr/local/bin/pleroma-host-release-entrypoint.sh +RUN chmod +x /usr/local/bin/pleroma-host-release-entrypoint.sh +ENTRYPOINT ["/usr/local/bin/pleroma-host-release-entrypoint.sh"] +CMD ["/opt/pleroma/bin/pleroma", "start"] diff --git a/installation/release-to-docker/README.md b/installation/release-to-docker/README.md new file mode 100644 index 000000000..4129bd94b --- /dev/null +++ b/installation/release-to-docker/README.md @@ -0,0 +1,66 @@ +# Run OTP releases on older glibc using Docker + +Pleroma OTP releases are built on specific distros and may require a newer `glibc` +than your host has. A typical failure looks like: + +``` +... /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found ... +``` + +If you don't want to upgrade your host OS, you can run the existing OTP release +from `/opt/pleroma` inside an Ubuntu 24.04 container while keeping your existing +host paths (`/etc/pleroma`, `/var/lib/pleroma`, etc.). + +This folder provides a "shim" container + systemd unit. It is **not** the Pleroma +Docker image. + +## What this does + +- Builds a small Ubuntu 24.04 image with runtime libs (including newer `glibc`). +- Mounts your existing host release at `/opt/pleroma` into the container. +- Runs as the same UID/GID that owns `/opt/pleroma` on the host (via `gosu`). +- Optionally runs migrations automatically on container start. +- Uses `network_mode: host` so your existing config that talks to `localhost` + keeps working. + +## Setup (Debian/Ubuntu host) + +1. Install Docker Engine + the Docker Compose plugin. +2. Copy these files to a stable location (example: `/etc/pleroma/container`): + + ``` + mkdir -p /etc/pleroma/container + cp -a /opt/pleroma/installation/release-to-docker/* /etc/pleroma/container/ + ``` + +3. Build the shim image: + + ``` + cd /etc/pleroma/container + docker compose build + ``` + +4. Replace your systemd unit: + + ``` + cp /etc/pleroma/container/pleroma.service /etc/systemd/system/pleroma.service + systemctl daemon-reload + systemctl enable --now pleroma + journalctl -u pleroma -f + ``` + +## Running `pleroma_ctl` + +Since the host binary may not run on older `glibc`, run admin commands inside the +container: + +``` +cd /etc/pleroma/container +docker compose exec pleroma /opt/pleroma/bin/pleroma_ctl status +docker compose run --rm --no-deps pleroma /opt/pleroma/bin/pleroma_ctl migrate +``` + +## Configuration notes + +- Migrations run automatically by default. + - Set `PLEROMA_RUN_MIGRATIONS=0` in `docker-compose.yml` to disable. diff --git a/installation/release-to-docker/docker-compose.yml b/installation/release-to-docker/docker-compose.yml new file mode 100644 index 000000000..043803241 --- /dev/null +++ b/installation/release-to-docker/docker-compose.yml @@ -0,0 +1,22 @@ +services: + pleroma: + build: + context: . + dockerfile: Dockerfile + image: pleroma-host-release-wrapper:ubuntu24 + init: true + network_mode: host + restart: unless-stopped + environment: + HOME: /opt/pleroma + LANG: C.UTF-8 + LC_ALL: C.UTF-8 + ELIXIR_ERL_OPTIONS: "+fnu" + # Set to 0 to skip running migrations on container start. + PLEROMA_RUN_MIGRATIONS: "1" + volumes: + # Existing OTP release installation (host) + - /opt/pleroma:/opt/pleroma:rw + # Existing config + uploads + static (host) + - /etc/pleroma:/etc/pleroma:rw + - /var/lib/pleroma:/var/lib/pleroma:rw diff --git a/installation/release-to-docker/pleroma-host-release-entrypoint.sh b/installation/release-to-docker/pleroma-host-release-entrypoint.sh new file mode 100644 index 000000000..b9c035678 --- /dev/null +++ b/installation/release-to-docker/pleroma-host-release-entrypoint.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +uid="${PLEROMA_UID:-}" +gid="${PLEROMA_GID:-}" + +if [[ -z "${uid}" || -z "${gid}" ]]; then + uid="$(stat -c '%u' /opt/pleroma)" + gid="$(stat -c '%g' /opt/pleroma)" +fi + +export HOME="${HOME:-/opt/pleroma}" + +if [[ "${PLEROMA_RUN_MIGRATIONS:-1}" != "0" && "${1:-}" == "/opt/pleroma/bin/pleroma" && "${2:-}" == "start" ]]; then + echo "Running migrations..." + gosu "${uid}:${gid}" /opt/pleroma/bin/pleroma_ctl migrate +fi + +exec gosu "${uid}:${gid}" "$@" diff --git a/installation/release-to-docker/pleroma.service b/installation/release-to-docker/pleroma.service new file mode 100644 index 000000000..d3833363c --- /dev/null +++ b/installation/release-to-docker/pleroma.service @@ -0,0 +1,16 @@ +[Unit] +Description=Pleroma social network (OTP release via Docker glibc shim) +After=network.target docker.service postgresql.service nginx.service +Requires=docker.service + +[Service] +Type=simple +WorkingDirectory=/etc/pleroma/container +ExecStart=/usr/bin/docker compose up --build --remove-orphans +ExecStop=/usr/bin/docker compose down +Restart=on-failure +RestartSec=5s +TimeoutStartSec=0 + +[Install] +WantedBy=multi-user.target From 65456aed1279d989ffa46573e1e5b8f32893432e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 1 Jan 2026 09:00:53 +0400 Subject: [PATCH 123/317] Release-to-Docker: Add unzip / curl to make updates work --- installation/release-to-docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/installation/release-to-docker/Dockerfile b/installation/release-to-docker/Dockerfile index ddff4570d..0ffeb5d5b 100644 --- a/installation/release-to-docker/Dockerfile +++ b/installation/release-to-docker/Dockerfile @@ -14,6 +14,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ postgresql-client \ ffmpeg imagemagick libimage-exiftool-perl \ libvips42t64 \ + unzip \ + curl \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt/pleroma From 89ac0b8f0a71d271d32ee5151ed3e69a39091e71 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 1 Jan 2026 10:39:38 +0400 Subject: [PATCH 124/317] Add changelog --- changelog.d/release-to-docker.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/release-to-docker.add diff --git a/changelog.d/release-to-docker.add b/changelog.d/release-to-docker.add new file mode 100644 index 000000000..5fbf611a5 --- /dev/null +++ b/changelog.d/release-to-docker.add @@ -0,0 +1 @@ +Add instructions on how to run a release in docker, to make it easier to run on older distros. From 1085f6d7cd9fdf270464691cba47aa9bac89a888 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 5 Jan 2026 11:57:02 +0400 Subject: [PATCH 125/317] TransmogrifierTest: Add failing test for EmojiReact url encoding --- .../web/activity_pub/transmogrifier_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 6dc4423fa..b54196a3c 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -759,6 +759,22 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do ) end + test "EmojiReact custom emoji urls are URI encoded" do + user = insert(:user, local: true) + note_activity = insert(:note_activity) + + {:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:") + {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data) + + assert length(data["tag"]) == 1 + + tag = List.first(data["tag"]) + url = tag["icon"]["url"] + + assert url == "http://localhost:4001/emoji/dino%20walking.gif" + assert tag["id"] == "http://localhost:4001/emoji/dino%20walking.gif" + end + test "it prepares a quote post" do user = insert(:user) From 72fea0c901ea35c70ef5e672388b2a58d53f5556 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 5 Jan 2026 11:57:38 +0400 Subject: [PATCH 126/317] Emoji: Unify tag building, fix tests. --- lib/pleroma/emoji.ex | 12 ++++++++++++ lib/pleroma/web/activity_pub/builder.ex | 10 +--------- lib/pleroma/web/activity_pub/transmogrifier.ex | 17 +++-------------- .../transmogrifier/emoji_tag_building_test.exs | 2 +- .../views/notification_view_test.exs | 4 ++-- .../web/mastodon_api/views/status_view_test.exs | 4 ++-- .../emoji_reaction_controller_test.exs | 2 +- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 21bcb0111..8c0257ef4 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -189,6 +189,18 @@ defmodule Pleroma.Emoji do def emoji_url(_), do: nil + def build_emoji_tag({name, url}) do + url = URI.encode(url) + + %{ + "icon" => %{"url" => "#{url}", "type" => "Image"}, + "name" => ":" <> name <> ":", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z", + "id" => url + } + end + def emoji_name_with_instance(name, url) do url = url |> URI.parse() |> Map.get(:host) "#{name}@#{url}" diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 046316024..4146fd6ab 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -64,15 +64,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do defp add_emoji_content(data, emoji, url) do tag = [ - %{ - "id" => url, - "type" => "Emoji", - "name" => Emoji.maybe_quote(emoji), - "icon" => %{ - "type" => "Image", - "url" => url - } - } + Emoji.build_emoji_tag({Emoji.maybe_strip_name(emoji), url}) ] data diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6e04d95e6..a4b1ae349 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do """ @behaviour Pleroma.Web.ActivityPub.Transmogrifier.API alias Pleroma.Activity + alias Pleroma.Emoji alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Maps alias Pleroma.Object @@ -1005,32 +1006,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def take_emoji_tags(%User{emoji: emoji}) do emoji |> Map.to_list() - |> Enum.map(&build_emoji_tag/1) + |> Enum.map(&Emoji.build_emoji_tag/1) end # TODO: we should probably send mtime instead of unix epoch time for updated def add_emoji_tags(%{"emoji" => emoji} = object) do tags = object["tag"] || [] - out = Enum.map(emoji, &build_emoji_tag/1) + out = Enum.map(emoji, &Emoji.build_emoji_tag/1) Map.put(object, "tag", tags ++ out) end def add_emoji_tags(object), do: object - def build_emoji_tag({name, url}) do - url = URI.encode(url) - - %{ - "icon" => %{"url" => "#{url}", "type" => "Image"}, - "name" => ":" <> name <> ":", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z", - "id" => url - } - end - def set_conversation(object) do Map.put(object, "conversation", object["context"]) end diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs index c632c199c..a6c5d689c 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do name = "hanapog" url = "https://misskey.local.live/emojis/hana pog.png" - tag = Transmogrifier.build_emoji_tag({name, url}) + tag = Pleroma.Emoji.build_emoji_tag({name, url}) assert tag["id"] == "https://misskey.local.live/emojis/hana%20pog.png" end diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index ce5ddd0fc..c8287bb79 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -219,7 +219,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do data: %{ "reactions" => [ ["👍", [user.ap_id], nil], - ["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino walking.gif"] + ["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino%20walking.gif"] ] } ) @@ -243,7 +243,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do account: AccountView.render("show.json", %{user: other_user, for: user}), status: StatusView.render("show.json", %{activity: activity, for: user}), created_at: Utils.to_masto_date(notification.inserted_at), - emoji_url: "http://localhost:4001/emoji/dino walking.gif" + emoji_url: "http://localhost:4001/emoji/dino%20walking.gif" } test_notifications_rendering([notification], user, [expected]) 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 d7908886b..73cab817b 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -54,7 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do count: 2, me: false, name: "dinosaur", - url: "http://localhost:4001/emoji/dino walking.gif", + url: "http://localhost:4001/emoji/dino%20walking.gif", account_ids: [other_user.id, user.id] }, %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} @@ -70,7 +70,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do count: 2, me: true, name: "dinosaur", - url: "http://localhost:4001/emoji/dino walking.gif", + url: "http://localhost:4001/emoji/dino%20walking.gif", account_ids: [other_user.id, user.id] }, %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index 79b805aca..8fcdb233b 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -100,7 +100,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do "name" => "dinosaur", "count" => 1, "me" => true, - "url" => "http://localhost:4001/emoji/dino walking.gif", + "url" => "http://localhost:4001/emoji/dino%20walking.gif", "account_ids" => [other_user.id] } ] From b552c2503925a63ec9315680982081385199612e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 5 Jan 2026 12:12:21 +0400 Subject: [PATCH 127/317] Add changelog --- changelog.d/emoji-reaction-url-escape.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/emoji-reaction-url-escape.fix diff --git a/changelog.d/emoji-reaction-url-escape.fix b/changelog.d/emoji-reaction-url-escape.fix new file mode 100644 index 000000000..c3a1c8823 --- /dev/null +++ b/changelog.d/emoji-reaction-url-escape.fix @@ -0,0 +1 @@ +Encode custom emoji URLs in EmojiReact activity tags. From 33b8ccf21fbe1fb0f68455eb4858ba522a8a7d17 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:12:49 +0400 Subject: [PATCH 128/317] Emoji: Handle more edge cases for local emoji with strange filenames. --- lib/pleroma/emoji.ex | 19 +++++++++++- lib/pleroma/emoji/formatter.ex | 3 +- lib/pleroma/web/activity_pub/builder.ex | 3 +- .../mastodon_api/views/custom_emoji_view.ex | 3 +- .../pleroma_api/views/bookmark_folder_view.ex | 3 +- .../emoji_tag_building_test.exs | 30 +++++++++++++++++-- 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 8c0257ef4..9fead1010 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Emoji do alias Pleroma.Emoji.Combinations alias Pleroma.Emoji.Loader + alias Pleroma.Utils.URIEncoding + alias Pleroma.Web.Endpoint require Logger @@ -189,8 +191,23 @@ defmodule Pleroma.Emoji do def emoji_url(_), do: nil + @spec local_url(String.t() | nil) :: String.t() | nil + def local_url(nil), do: nil + + def local_url("http" <> _ = url) do + URIEncoding.encode_url(url) + end + + def local_url("/" <> _ = path) do + URIEncoding.encode_url(Endpoint.url() <> path, bypass_decode: true) + end + + def local_url(path) when is_binary(path) do + local_url("/" <> path) + end + def build_emoji_tag({name, url}) do - url = URI.encode(url) + url = URIEncoding.encode_url(url) %{ "icon" => %{"url" => "#{url}", "type" => "Image"}, diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex index 87fd35f13..9238bf912 100644 --- a/lib/pleroma/emoji/formatter.ex +++ b/lib/pleroma/emoji/formatter.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Emoji.Formatter do alias Pleroma.Emoji alias Pleroma.HTML - alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy def emojify(text) do @@ -44,7 +43,7 @@ defmodule Pleroma.Emoji.Formatter do Emoji.get_all() |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end) |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc -> - Map.put(acc, name, to_string(URI.merge(Endpoint.url(), file))) + Map.put(acc, name, Emoji.local_url(file)) end) end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 4146fd6ab..167c769a9 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -17,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI.ActivityDraft - alias Pleroma.Web.Endpoint require Pleroma.Constants @@ -105,7 +104,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do defp local_custom_emoji_react(data, emoji) do with %{file: path} = emojo <- Emoji.get(emoji) do - url = "#{Endpoint.url()}#{path}" + url = Emoji.local_url(path) add_emoji_content(data, emojo.code, url) else _ -> {:error, "Emoji does not exist"} diff --git a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex index cd59ab946..6ef48e98a 100644 --- a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex +++ b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex @@ -6,14 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiView do use Pleroma.Web, :view alias Pleroma.Emoji - alias Pleroma.Web.Endpoint def render("index.json", %{custom_emojis: custom_emojis}) do render_many(custom_emojis, __MODULE__, "show.json") end def render("show.json", %{custom_emoji: {shortcode, %Emoji{file: relative_url, tags: tags}}}) do - url = Endpoint.url() |> URI.merge(relative_url) |> to_string() + url = Emoji.local_url(relative_url) %{ "shortcode" => shortcode, diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex index 12decb816..29028781f 100644 --- a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex +++ b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do alias Pleroma.BookmarkFolder alias Pleroma.Emoji - alias Pleroma.Web.Endpoint def render("show.json", %{folder: %BookmarkFolder{} = folder}) do %{ @@ -33,7 +32,7 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do emoji = Emoji.get(emoji) if emoji != nil do - Endpoint.url() |> URI.merge(emoji.file) |> to_string() + Emoji.local_url(emoji.file) else nil end diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs index a6c5d689c..ff005b466 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs @@ -1,8 +1,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do use Pleroma.DataCase, async: true - alias Pleroma.Web.ActivityPub.Transmogrifier - test "it encodes the id to be a valid url" do name = "hanapog" url = "https://misskey.local.live/emojis/hana pog.png" @@ -11,4 +9,32 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do assert tag["id"] == "https://misskey.local.live/emojis/hana%20pog.png" end + + test "it does not double-encode already encoded urls" do + name = "hanapog" + url = "https://misskey.local.live/emojis/hana%20pog.png" + + tag = Pleroma.Emoji.build_emoji_tag({name, url}) + + assert tag["id"] == url + end + + test "it encodes disallowed path characters" do + name = "hanapog" + url = "https://example.com/emojis/hana[pog].png" + + tag = Pleroma.Emoji.build_emoji_tag({name, url}) + + assert tag["id"] == "https://example.com/emojis/hana%5Bpog%5D.png" + end + + test "local_url does not decode percent in filenames" do + url = Pleroma.Emoji.local_url("/emoji/hana%20pog.png") + + assert url == Pleroma.Web.Endpoint.url() <> "/emoji/hana%2520pog.png" + + tag = Pleroma.Emoji.build_emoji_tag({"hanapog", url}) + + assert tag["id"] == url + end end From 7ce7d4d319b822b2e29acf8fc6a60aa233c80375 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:38:15 +0400 Subject: [PATCH 129/317] Linting --- lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index a4b1ae349..ba6f4030d 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do """ @behaviour Pleroma.Web.ActivityPub.Transmogrifier.API alias Pleroma.Activity - alias Pleroma.Emoji alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Emoji alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment From b013ec9123db3f35be5fb497fcb715b933bd2375 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 7 Jan 2026 10:40:45 +0400 Subject: [PATCH 130/317] Emoji, AccountView, UtilController: Handle encoding of emoji --- lib/pleroma/emoji.ex | 3 ++- .../web/mastodon_api/views/account_view.ex | 15 ++++++++++++++- .../controllers/util_controller.ex | 11 +++++++++++ .../emoji_tag_building_test.exs | 6 ++++++ .../mastodon_api/views/account_view_test.exs | 19 +++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 9fead1010..2fe425955 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -199,7 +199,8 @@ defmodule Pleroma.Emoji do end def local_url("/" <> _ = path) do - URIEncoding.encode_url(Endpoint.url() <> path, bypass_decode: true) + path = URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + Endpoint.url() <> path end def local_url(path) when is_binary(path) do diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 03a2fc55a..d5f44676d 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do alias Pleroma.User alias Pleroma.UserNote alias Pleroma.UserRelationship + alias Pleroma.Utils.URIEncoding alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MediaProxy @@ -238,7 +239,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do emojis = Enum.map(user.emoji, fn {shortcode, raw_url} -> - url = MediaProxy.url(raw_url) + url = + raw_url + |> encode_emoji_url() + |> MediaProxy.url() %{ shortcode: shortcode, @@ -511,4 +515,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do # See https://git.pleroma.social/pleroma/pleroma-meta/-/issues/14 user.actor_type == "Service" || user.actor_type == "Group" end + + defp encode_emoji_url(nil), do: nil + defp encode_emoji_url("http" <> _ = url), do: URIEncoding.encode_url(url) + + defp encode_emoji_url("/" <> _ = path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + + defp encode_emoji_url(path) when is_binary(path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index aeafa195d..45df3d31c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Config alias Pleroma.Emoji alias Pleroma.Healthcheck + alias Pleroma.Utils.URIEncoding alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator @@ -180,12 +181,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do def emoji(conn, _params) do emoji = Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> + file = encode_emoji_url(file) Map.put(acc, code, %{image_url: file, tags: tags}) end) json(conn, emoji) end + defp encode_emoji_url(nil), do: nil + defp encode_emoji_url("http" <> _ = url), do: URIEncoding.encode_url(url) + + defp encode_emoji_url("/" <> _ = path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + + defp encode_emoji_url(path) when is_binary(path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + def update_notification_settings(%{assigns: %{user: user}} = conn, params) do with {:ok, _} <- User.update_notification_settings(user, params) do json(conn, %{status: "success"}) diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs index ff005b466..b91658ce4 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs @@ -37,4 +37,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do assert tag["id"] == url end + + test "local_url encodes question marks in filenames" do + url = Pleroma.Emoji.local_url("/emoji/file?name.png") + + assert url == Pleroma.Web.Endpoint.url() <> "/emoji/file%3Fname.png" + end end diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 5d24c0e9f..bd151cc61 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -105,6 +105,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end + test "encodes emoji urls in the emojis field" do + user = + insert(:user, + name: ":brackets: :percent:", + emoji: %{ + "brackets" => "/emoji/hana[pog].png", + "percent" => "/emoji/hana%20pog.png" + } + ) + + %{emojis: emojis} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + emoji_urls = Map.new(emojis, &{&1.shortcode, &1.url}) + + assert emoji_urls["brackets"] == "/emoji/hana%5Bpog%5D.png" + assert emoji_urls["percent"] == "/emoji/hana%2520pog.png" + end + describe "roles and privileges" do setup do clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator]) From 3d7d1197820aec9b10d8f004160a7cabbac87b34 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 7 Jan 2026 11:14:45 +0400 Subject: [PATCH 131/317] Linting --- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 45df3d31c..1c072f98a 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,8 +11,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Config alias Pleroma.Emoji alias Pleroma.Healthcheck - alias Pleroma.Utils.URIEncoding alias Pleroma.User + alias Pleroma.Utils.URIEncoding alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.CommonAPI From c07506ab6e8dfae3eb90ded86fcaf9146749090e Mon Sep 17 00:00:00 2001 From: floatingghost Date: Sat, 4 Feb 2023 20:51:17 +0000 Subject: [PATCH 132/317] paginate follow requests (#460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit matches https://docs.joinmastodon.org/methods/follow_requests/#get mostly Co-authored-by: FloatingGhost Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/460 Signed-off-by: nicole mikołajczyk --- lib/pleroma/following_relationship.ex | 18 +++++++-------- lib/pleroma/user.ex | 10 +++++++-- .../operations/follow_request_operation.ex | 19 ++++++++++++++++ .../pleroma_follow_request_operation.ex | 21 +++++++++++++++++- .../controllers/follow_request_controller.ex | 15 ++++++++++--- .../web/mastodon_api/views/account_view.ex | 3 ++- .../controllers/follow_request_controller.ex | 10 ++++++++- .../follow_request_controller_test.exs | 22 +++++++++++++++++++ 8 files changed, 100 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 653feb32f..e27f78d33 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -147,24 +147,22 @@ defmodule Pleroma.FollowingRelationship do |> Repo.aggregate(:count, :id) end - def get_follow_requests(%User{id: id}) do + def get_follow_requests_query(%User{id: id}) do __MODULE__ - |> join(:inner, [r], f in assoc(r, :follower)) + |> join(:inner, [r], f in assoc(r, :follower), as: :follower) |> where([r], r.state == ^:follow_pending) |> where([r], r.following_id == ^id) - |> where([r, f], f.is_active == true) - |> select([r, f], f) - |> Repo.all() + |> where([r, follower: f], f.is_active == true) + |> select([r, follower: f], f) end - def get_outgoing_follow_requests(%User{id: id}) do + def get_outgoing_follow_requests_query(%User{id: id}) do __MODULE__ - |> join(:inner, [r], f in assoc(r, :following)) + |> join(:inner, [r], f in assoc(r, :following), as: :following) |> where([r], r.state == ^:follow_pending) |> where([r], r.follower_id == ^id) - |> where([r, f], f.is_active == true) - |> select([r, f], f) - |> Repo.all() + |> where([r, following: f], f.is_active == true) + |> select([r, following: f], f) end def following?(%User{id: follower_id}, %User{id: followed_id}) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 904e9e056..75da41da9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -287,8 +287,14 @@ defmodule Pleroma.User do defdelegate following(user), to: FollowingRelationship defdelegate following?(follower, followed), to: FollowingRelationship defdelegate following_ap_ids(user), to: FollowingRelationship - defdelegate get_follow_requests(user), to: FollowingRelationship - defdelegate get_outgoing_follow_requests(user), to: FollowingRelationship + defdelegate get_follow_requests_query(user), to: FollowingRelationship + defdelegate get_outgoing_follow_requests_query(user), to: FollowingRelationship + + def get_follow_requests(user) do + get_follow_requests_query(user) + |> Repo.all() + end + defdelegate search(query, opts \\ []), to: User.Search @doc """ diff --git a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex index 72dc8b5fa..fbb997447 100644 --- a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex +++ b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do summary: "Retrieve follow requests", security: [%{"oAuth" => ["read:follows", "follow"]}], operationId: "FollowRequestController.index", + parameters: pagination_params(), responses: %{ 200 => Operation.response("Array of Account", "application/json", %Schema{ @@ -62,4 +63,22 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do required: true ) end + + defp pagination_params do + [ + Operation.parameter(:max_id, :query, :string, "Return items older than this ID"), + Operation.parameter( + :since_id, + :query, + :string, + "Return the oldest items newer than this ID" + ), + Operation.parameter( + :limit, + :query, + %Schema{type: :integer, default: 20}, + "Maximum number of items to return. Will be ignored if it's more than 40" + ) + ] + end end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex index b5a413490..5e2cf6a78 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex @@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaFollowRequestOperation do tags: ["Follow requests"], summary: "Retrieve outgoing follow requests", security: [%{"oAuth" => ["read:follows", "follow"]}], - operationId: "PleromaFollowRequestController.outgoing", + operationId: "PleromaFollowRequestController.outgoing",, + parameters: pagination_params(), responses: %{ 200 => Operation.response("Array of Account", "application/json", %Schema{ @@ -28,4 +29,22 @@ defmodule Pleroma.Web.ApiSpec.PleromaFollowRequestOperation do } } end + + defp pagination_params do + [ + Operation.parameter(:max_id, :query, :string, "Return items older than this ID"), + Operation.parameter( + :since_id, + :query, + :string, + "Return the oldest items newer than this ID" + ), + Operation.parameter( + :limit, + :query, + %Schema{type: :integer, default: 20}, + "Maximum number of items to return. Will be ignored if it's more than 40" + ) + ] + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 6eee55d1b..a15029d92 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -5,6 +5,10 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do use Pleroma.Web, :controller + import Pleroma.Web.ControllerHelper, + only: [add_link_headers: 2] + + alias Pleroma.Pagination alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug @@ -24,10 +28,15 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation @doc "GET /api/v1/follow_requests" - def index(%{assigns: %{user: followed}} = conn, _params) do - follow_requests = User.get_follow_requests(followed) + def index(%{assigns: %{user: followed}} = conn, params) do + follow_requests = + followed + |> User.get_follow_requests_query() + |> Pagination.fetch_paginated(params, :keyset, :follower) - render(conn, "index.json", for: followed, users: follow_requests, as: :user) + conn + |> add_link_headers(follow_requests) + |> render("index.json", for: followed, users: follow_requests, as: :user) end @doc "POST /api/v1/follow_requests/:id/authorize" diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index d5f44676d..988384489 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -360,7 +360,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do %User{id: user_id} ) do count = - User.get_follow_requests(user) + user + |> User.get_follow_requests() |> length() data diff --git a/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex index 656d477da..387309cd8 100644 --- a/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex @@ -5,6 +5,10 @@ defmodule Pleroma.Web.PleromaAPI.FollowRequestController do use Pleroma.Web, :controller + import Pleroma.Web.ControllerHelper, + only: [add_link_headers: 2] + + alias Pleroma.Pagination alias Pleroma.User alias Pleroma.Web.Plugs.OAuthScopesPlug @@ -18,10 +22,14 @@ defmodule Pleroma.Web.PleromaAPI.FollowRequestController do @doc "GET /api/v1/pleroma/outgoing_follow_requests" def outgoing(%{assigns: %{user: follower}} = conn, _params) do - follow_requests = User.get_outgoing_follow_requests(follower) + follow_requests = + follower + |> User.get_outgoing_follow_requests_query() + |> Pagination.fetch_paginated(params, :keyset, :follower) conn |> put_view(Pleroma.Web.MastodonAPI.FollowRequestView) + |> add_link_headers(follow_requests) |> render("index.json", for: follower, users: follow_requests, as: :user) end end diff --git a/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs index b7c7ccae0..276866d75 100644 --- a/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -10,6 +10,11 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do import Pleroma.Factory + defp extract_next_link_header(header) do + [_, next_link] = Regex.run(~r{<(?.*)>; rel="next"}, header) + next_link + end + describe "locked accounts" do setup do user = insert(:user, is_locked: true) @@ -31,6 +36,23 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do assert to_string(other_user.id) == relationship["id"] end + test "/api/v1/follow_requests paginates", %{user: user, conn: conn} do + for _ <- 1..21 do + other_user = insert(:user) + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + {:ok, _, _} = User.follow(other_user, user, :follow_pending) + end + + conn = get(conn, "/api/v1/follow_requests") + assert length(json_response_and_validate_schema(conn, 200)) == 20 + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ "rel=\"next\"" + next_link = extract_next_link_header(link_header) + assert next_link =~ "/api/v1/follow_requests" + conn = get(conn, next_link) + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do other_user = insert(:user) From 5068e31583d7adb05cc2e4141eace09752da56d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 14 Oct 2025 07:38:59 +0200 Subject: [PATCH 133/317] optimize follow_request_count for own account view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/mastodon_api/views/account_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 988384489..a7d994593 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -361,8 +361,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do ) do count = user - |> User.get_follow_requests() - |> length() + |> User.get_follow_requests_query() + |> Pleroma.Repo.aggregate(:count) data |> Kernel.put_in([:follow_requests_count], count) From 520bac27dcf97678d7aff59b692bbea8761ac9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 7 Jan 2026 15:34:31 +0100 Subject: [PATCH 134/317] Add changelog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/paginate-follow-requests.change | 1 + .../api_spec/operations/pleroma_follow_request_operation.ex | 2 +- .../web/pleroma_api/controllers/follow_request_controller.ex | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/paginate-follow-requests.change diff --git a/changelog.d/paginate-follow-requests.change b/changelog.d/paginate-follow-requests.change new file mode 100644 index 000000000..1a88995b7 --- /dev/null +++ b/changelog.d/paginate-follow-requests.change @@ -0,0 +1 @@ +Paginate follow requests diff --git a/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex index 5e2cf6a78..b3fa0457d 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaFollowRequestOperation do tags: ["Follow requests"], summary: "Retrieve outgoing follow requests", security: [%{"oAuth" => ["read:follows", "follow"]}], - operationId: "PleromaFollowRequestController.outgoing",, + operationId: "PleromaFollowRequestController.outgoing", parameters: pagination_params(), responses: %{ 200 => diff --git a/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex index 387309cd8..c72b6941b 100644 --- a/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex @@ -21,11 +21,11 @@ defmodule Pleroma.Web.PleromaAPI.FollowRequestController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaFollowRequestOperation @doc "GET /api/v1/pleroma/outgoing_follow_requests" - def outgoing(%{assigns: %{user: follower}} = conn, _params) do + def outgoing(%{assigns: %{user: follower}} = conn, params) do follow_requests = follower |> User.get_outgoing_follow_requests_query() - |> Pagination.fetch_paginated(params, :keyset, :follower) + |> Pagination.fetch_paginated(params, :keyset, :following) conn |> put_view(Pleroma.Web.MastodonAPI.FollowRequestView) From 6a81e4fe003792ae95ba950dd2d3a3dcc2801e8a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 7 Jan 2026 17:59:37 +0400 Subject: [PATCH 135/317] Tests: Syncify tests that mutate global state. --- test/mix/pleroma_test.exs | 2 +- test/mix/tasks/pleroma/app_test.exs | 2 +- test/mix/tasks/pleroma/database_test.exs | 2 +- test/pleroma/http_test.exs | 2 +- .../pleroma/repo/migrations/autolinker_to_linkify_test.exs | 2 +- .../web/activity_pub/mrf/remote_report_policy_test.exs | 2 +- test/pleroma/web/activity_pub/utils_test.exs | 2 +- test/pleroma/web/activity_pub/views/user_view_test.exs | 2 +- .../pleroma_api/views/chat_message_reference_view_test.exs | 7 ++++++- test/pleroma/web/rich_media/card_test.exs | 4 +++- test/pleroma/web/web_finger_test.exs | 3 ++- test/pleroma/workers/publisher_worker_test.exs | 2 +- test/pleroma/workers/reachability_worker_test.exs | 2 +- test/pleroma/workers/receiver_worker_test.exs | 2 +- test/pleroma/workers/remote_fetcher_worker_test.exs | 2 +- 15 files changed, 23 insertions(+), 15 deletions(-) diff --git a/test/mix/pleroma_test.exs b/test/mix/pleroma_test.exs index e362223b2..e8f801913 100644 --- a/test/mix/pleroma_test.exs +++ b/test/mix/pleroma_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.PleromaTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false import Mix.Pleroma setup_all do diff --git a/test/mix/tasks/pleroma/app_test.exs b/test/mix/tasks/pleroma/app_test.exs index 65245eadd..6156214c1 100644 --- a/test/mix/tasks/pleroma/app_test.exs +++ b/test/mix/tasks/pleroma/app_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.AppTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false setup_all do Mix.shell(Mix.Shell.Process) diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 38ed096ae..19df17b60 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.DatabaseTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Activity diff --git a/test/pleroma/http_test.exs b/test/pleroma/http_test.exs index 7b6847cf9..e673e6591 100644 --- a/test/pleroma/http_test.exs +++ b/test/pleroma/http_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTPTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false use Pleroma.Tests.Helpers import Tesla.Mock diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs index 99522994a..c15967eee 100644 --- a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs +++ b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false import Pleroma.Factory import Pleroma.Tests.Helpers alias Pleroma.ConfigDB diff --git a/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs b/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs index 8d2a6b4fa..270c650e0 100644 --- a/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs @@ -1,5 +1,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicyTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index a48639c38..f162f3684 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.UtilsTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Repo diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index 7ac5f7c0f..a3f807ca9 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.UserViewTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false import Pleroma.Factory alias Pleroma.User 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 c78c03aba..8c3682798 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 @@ -4,7 +4,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do alias Pleroma.NullCache - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Chat alias Pleroma.Chat.MessageReference @@ -18,6 +18,11 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do import Mox import Pleroma.Factory + setup do + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) + :ok + end + setup do: clear_config([:rich_media, :enabled], true) test "it displays a chat message" do diff --git a/test/pleroma/web/rich_media/card_test.exs b/test/pleroma/web/rich_media/card_test.exs index c69f85323..723446c86 100644 --- a/test/pleroma/web/rich_media/card_test.exs +++ b/test/pleroma/web/rich_media/card_test.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Web.RichMedia.CardTest do use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Tests.ObanHelpers alias Pleroma.UnstubbedConfigMock, as: ConfigMock @@ -19,6 +19,8 @@ defmodule Pleroma.Web.RichMedia.CardTest do setup do mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) + ConfigMock |> stub_with(Pleroma.Test.StaticConfig) diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index eb03c736e..da10abdd2 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -3,12 +3,13 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.WebFingerTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Web.WebFinger import Pleroma.Factory import Tesla.Mock setup do + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) mock(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end diff --git a/test/pleroma/workers/publisher_worker_test.exs b/test/pleroma/workers/publisher_worker_test.exs index ca432d9bf..4519864a5 100644 --- a/test/pleroma/workers/publisher_worker_test.exs +++ b/test/pleroma/workers/publisher_worker_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.PublisherWorkerTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory diff --git a/test/pleroma/workers/reachability_worker_test.exs b/test/pleroma/workers/reachability_worker_test.exs index 4854aff77..c641ccf65 100644 --- a/test/pleroma/workers/reachability_worker_test.exs +++ b/test/pleroma/workers/reachability_worker_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReachabilityWorkerTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo import Mock diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 7f4789f91..12abc1a27 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReceiverWorkerTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo import Mock diff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs index 6eb6932cb..a3b900a9b 100644 --- a/test/pleroma/workers/remote_fetcher_worker_test.exs +++ b/test/pleroma/workers/remote_fetcher_worker_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.RemoteFetcherWorkerTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Workers.RemoteFetcherWorker From 7283d4f9beaad241b0c039938131f5b2ccffb74e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 8 Jan 2026 13:39:55 +0400 Subject: [PATCH 136/317] Config: Make streaming in tests actually synchronous --- config/test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/test.exs b/config/test.exs index 4da2b4f57..9652b7d4b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -102,7 +102,6 @@ config :pleroma, :http, send_user_agent: false rum_enabled = System.get_env("RUM_ENABLED") == "true" config :pleroma, :database, rum_enabled: rum_enabled -IO.puts("RUM enabled: #{rum_enabled}") config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp35v0RK9SO8WTPr6QZ" @@ -192,7 +191,7 @@ config :pleroma, Pleroma.Application, streamer_registry: false, test_http_pools: true -config :pleroma, Pleroma.Web.Streaming, sync_streaming: true +config :pleroma, Pleroma.Web.Streamer, sync_streaming: true config :pleroma, Pleroma.Uploaders.Uploader, timeout: 1_000 @@ -207,8 +206,9 @@ config :pleroma, Pleroma.User.Backup, tempdir: "test/tmp" if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" -else - IO.puts( - "You may want to create test.secret.exs to declare custom database connection parameters." - ) end + +# Avoid noisy shutdown logs from os_mon during tests. +config :os_mon, + start_cpu_sup: false, + start_memsup: false From 02185ec7114b5ea0faba79dee2cc427f122421b9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 8 Jan 2026 13:40:25 +0400 Subject: [PATCH 137/317] StripLocation, ReadDescription: Silence noisy errors. --- lib/mix/tasks/pleroma/config.ex | 8 ++++- .../filter/exiftool/read_description.ex | 32 +++++++++++-------- .../upload/filter/exiftool/strip_location.ex | 5 +-- .../filter/exiftool/strip_location_test.exs | 4 +-- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 8b3b2f18b..834b4fe14 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -330,7 +330,13 @@ defmodule Mix.Tasks.Pleroma.Config do |> Enum.each(&write_and_delete(&1, file, opts[:delete])) :ok = File.close(file) - System.cmd("mix", ["format", path]) + + # Ensure `mix format` runs in the same env as the current task and doesn't + # emit config-time stderr noise (e.g. dev secret warnings) into `mix test`. + System.cmd("mix", ["format", path], + env: [{"MIX_ENV", to_string(Mix.env())}], + stderr_to_stdout: true + ) end defp config_header, do: "import Config\r\n\r\n" diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex index 8c1ed82f8..8283b8643 100644 --- a/lib/pleroma/upload/filter/exiftool/read_description.ex +++ b/lib/pleroma/upload/filter/exiftool/read_description.ex @@ -29,22 +29,26 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do do: current_description defp read_when_empty(_, file, tag) do - try do - {tag_content, 0} = - System.cmd("exiftool", ["-b", "-s3", tag, file], - stderr_to_stdout: false, - parallelism: true - ) + if File.exists?(file) do + try do + {tag_content, 0} = + System.cmd("exiftool", ["-m", "-b", "-s3", tag, file], + stderr_to_stdout: false, + parallelism: true + ) - tag_content = String.trim(tag_content) + tag_content = String.trim(tag_content) - if tag_content != "" and - String.length(tag_content) <= - Pleroma.Config.get([:instance, :description_limit]), - do: tag_content, - else: nil - rescue - _ in ErlangError -> nil + if tag_content != "" and + String.length(tag_content) <= + Pleroma.Config.get([:instance, :description_limit]), + do: tag_content, + else: nil + rescue + _ in ErlangError -> nil + end + else + nil end end end diff --git a/lib/pleroma/upload/filter/exiftool/strip_location.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex index 1744a286d..23346d234 100644 --- a/lib/pleroma/upload/filter/exiftool/strip_location.ex +++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex @@ -16,11 +16,12 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do try do - case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", "-png:all=", file], + case System.cmd("exiftool", ["-m", "-overwrite_original", "-gps:all=", "-png:all=", file], + stderr_to_stdout: true, parallelism: true ) do {_response, 0} -> {:ok, :filtered} - {error, 1} -> {:error, error} + {error, _} -> {:error, error} end rescue e in ErlangError -> diff --git a/test/pleroma/upload/filter/exiftool/strip_location_test.exs b/test/pleroma/upload/filter/exiftool/strip_location_test.exs index 4dcd4dce3..485060215 100644 --- a/test/pleroma/upload/filter/exiftool/strip_location_test.exs +++ b/test/pleroma/upload/filter/exiftool/strip_location_test.exs @@ -25,8 +25,8 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocationTest do assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :filtered} - {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.#{type}"]) - {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.#{type}"]) + {exif_original, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010.#{type}"]) + {exif_filtered, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010_tmp.#{type}"]) assert String.match?(exif_original, ~r/GPS/) refute String.match?(exif_filtered, ~r/GPS/) From b2be7d48bc5129fa60ebb6db2c77fb13115253fd Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 8 Jan 2026 13:40:43 +0400 Subject: [PATCH 138/317] Mix: Silence migrations --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ac998e9ee..dff40c675 100644 --- a/mix.exs +++ b/mix.exs @@ -230,7 +230,7 @@ defmodule Pleroma.Mixfile do "ecto.rollback": ["pleroma.ecto.rollback"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test --warnings-as-errors"], + test: ["ecto.create --quiet", "pleroma.ecto.migrate --quiet", "test --warnings-as-errors"], docs: ["pleroma.docs", "docs"], analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"], copyright: &add_copyright/1, From 343e42126a1ede3578c643c5d8a05ca92eb30119 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 8 Jan 2026 13:40:51 +0400 Subject: [PATCH 139/317] Add changelog --- changelog.d/reduce-flaky-tests.skip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/reduce-flaky-tests.skip diff --git a/changelog.d/reduce-flaky-tests.skip b/changelog.d/reduce-flaky-tests.skip new file mode 100644 index 000000000..0375762c0 --- /dev/null +++ b/changelog.d/reduce-flaky-tests.skip @@ -0,0 +1 @@ +Reduce the number of flaky tests by making them sync if they affect the global state, and silence noisy test output. From 033083d1d18dc038f5b6c13596f403272670c5de Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 8 Jan 2026 14:06:24 +0400 Subject: [PATCH 140/317] Streamer: Fix Marker streaming bug, fix caching in tests. --- lib/pleroma/web/streamer.ex | 2 +- test/pleroma/web/streamer_test.exs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index aba42ee78..120e94751 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -300,7 +300,7 @@ defmodule Pleroma.Web.Streamer do end) end - defp do_stream("user", item) do + defp do_stream("user", %Activity{} = item) do Logger.debug("Trying to push to users") recipient_topics = diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index 096ca2d2a..f5008f6b9 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -19,7 +19,12 @@ defmodule Pleroma.Web.StreamerTest do @moduletag needs_streamer: true, capture_log: true - setup do: clear_config([:instance, :skip_thread_containment]) + setup do + clear_config([:instance, :skip_thread_containment]) + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) + + :ok + end describe "get_topic/_ (unauthenticated)" do test "allows no stream" do From af4bed50e608bce0bc6aec467ddc3e3992b4947b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 7 Jan 2026 20:41:02 +0100 Subject: [PATCH 141/317] Add Oban Web and upgrade LiveView, plug --- lib/pleroma/web/router.ex | 2 ++ mix.exs | 3 ++- mix.lock | 6 ++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a0fa7b3e3..43b74c357 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.Router do use Pleroma.Web, :router import Phoenix.LiveDashboard.Router + import Oban.Web.Router pipeline :accepts_html do plug(:accepts, ["html"]) @@ -1045,6 +1046,7 @@ defmodule Pleroma.Web.Router do scope "/" do pipe_through([:pleroma_html, :authenticate, :require_admin]) live_dashboard("/phoenix/live_dashboard", additional_pages: [oban: Oban.LiveDashboard]) + oban_dashboard("/pleroma/oban") end # Test-only routes needed to test action dispatching and plug chain execution diff --git a/mix.exs b/mix.exs index dff40c675..95783cf4e 100644 --- a/mix.exs +++ b/mix.exs @@ -130,7 +130,7 @@ defmodule Pleroma.Mixfile do {:ecto_enum, "~> 1.4"}, {:postgrex, ">= 0.20.0"}, {:phoenix_html, "~> 3.3"}, - {:phoenix_live_view, "~> 0.19.0"}, + {:phoenix_live_view, "~> 1.1.0"}, {:phoenix_live_dashboard, "~> 0.8.0"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, @@ -140,6 +140,7 @@ defmodule Pleroma.Mixfile do {:oban_plugins_lazarus, git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", ref: "e49fc355baaf0e435208bf5f534d31e26e897711"}, + {:oban_web, "~> 2.11"}, {:gettext, "~> 0.20"}, {:bcrypt_elixir, "~> 2.2"}, {:trailing_format_plug, "~> 0.0.7"}, diff --git a/mix.lock b/mix.lock index b8c9e240d..a2c5e1ec7 100644 --- a/mix.lock +++ b/mix.lock @@ -95,7 +95,9 @@ "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"}, "oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"}, + "oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"}, "oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]}, + "oban_web": {:hex, :oban_web, "2.11.6", "53933cb4253c4d9f1098ee311c06f07935259f0e564dcf2d66bae4cc98e317fe", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "576d94b705688c313694c2c114ca21aa0f8f2ad1b9ca45c052c5ba316d3e8d10"}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, @@ -105,12 +107,12 @@ "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, From ac6ec02725c17741d93e166ef7b468ccc2f19072 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 7 Jan 2026 21:12:28 +0100 Subject: [PATCH 142/317] changelog --- changelog.d/oban-web.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/oban-web.add diff --git a/changelog.d/oban-web.add b/changelog.d/oban-web.add new file mode 100644 index 000000000..c59e2ebca --- /dev/null +++ b/changelog.d/oban-web.add @@ -0,0 +1 @@ +Added Oban Web dashboard located at /pleroma/oban From 421187dbfa4b7b5fe5e758666055ef417202110c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 8 Jan 2026 00:28:29 +0100 Subject: [PATCH 143/317] Remove /pleroma/oban and /phoenix/live_dashboard from API routes This is needed to prevent admin frontend overrides from misbehaving when overriding AdminFE located at /pleroma/admin, since API routes are interpreted as the first portion of their full path, ie: /api/v1/pleroma/admin -> /api --- lib/pleroma/web/router.ex | 6 ++++++ test/pleroma/web/plugs/frontend_static_plug_test.exs | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 43b74c357..df1812bd5 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1092,9 +1092,15 @@ defmodule Pleroma.Web.Router do options("/*path", RedirectController, :empty) end + # /pleroma/oban/* needs to get filtered out from api routes for frontend configuration + # to not drop admin overrides for /pleroma/admin. + # Also removing /phoenix since it is not an API route + @non_api_routes ["/phoenix/live_dashboard", "/pleroma/oban"] + def get_api_routes do Phoenix.Router.routes(__MODULE__) |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end) + |> Enum.reject(fn r -> String.starts_with?(r.path, @non_api_routes) end) |> Enum.map(fn r -> r.path |> String.split("/", trim: true) diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index cbe200738..e1e331c06 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -106,7 +106,6 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do "manifest.json", "auth", "proxy", - "phoenix", "test", "user_exists", "check_password" From f16dad287952532cf125c337bb4f66b9737be30f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 8 Jan 2026 22:23:38 +0100 Subject: [PATCH 144/317] Docs: Add admin documentation for LiveDashboard and Oban Web --- docs/administration/dashboards.md | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/administration/dashboards.md diff --git a/docs/administration/dashboards.md b/docs/administration/dashboards.md new file mode 100644 index 000000000..8e8987633 --- /dev/null +++ b/docs/administration/dashboards.md @@ -0,0 +1,47 @@ +# Dashboards + +Pleroma comes with two types of backend dashboards viewable to instance administrators: + +* [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard/Phoenix.LiveDashboard.html) - A general system oriented dashboard for viewing statistics about Pleroma resource consumption, Pleroma's database and Pleroma's job processor (Oban). +* [Oban Web](https://hexdocs.pm/oban_web/overview.html) - A dashboard specific to Oban for viewing Oban statistics, managing jobs and job queues. + +!!! note + Both dashboards require working Websockets. + If your browser or web server don't support Websockets, both dashboards either won't update or will not display all information. + +## Phoenix LiveDashboard + +Instance administrators can access this dashboard at `/phoenix/live_dashboard`, giving a simple overview of software versions including Erlang and Elixir versions, instance uptime and resource consumption. + +This dashboard gives insights into the current state of the BEAM VM running Pleroma code and database statistics including basic diagnostics. +It can be useful for troubleshooting of some issues namely regarding database performance. + +### Relevant dashboard tabs + +* Home - A general overview of system information including software versions, uptime and memory BEAM memory consumption. +* OS Data - Information about the OS and system such as CPU load, memory usage and disk usage. +* Ecto Stats - Information about the Pleroma database. + - Diagnose - Basic database diagnostics, including a `bloat` warning when an index or a table have excessive bloat, which can lead to bad database performance. + - Bloat - A table showing size of "bloat" (unused wasted space) in database tables and indexes. Very high bloat size in the `activities` and `objects` tables can lead to bad performance especially on slower disks such as on most VPS providers. + - Db settings - A small list of PostgreSQL settings mostly relevant to database performance. + - Total table size - Shows sizes of all database tables including indexes sorted by size, useful for quickly checking overall database size. + - Long running queries - A list of of slow database queries and their duration. Multiple entries with duration in multiple seconds indicate a slowly performing database. +* Oban - Shows a list of all Oban jobs. + +!!! note + The DB bloat warning for `index 'oban_jobs::oban_jobs_args_index'` in Ecto Stats can be safely ignored. + +## Oban Web + +An advanced dashboard and management console viewable to instance administrators specifically for Oban, Pleroma's job processor. +It allows managing jobs, including force retrying failed jobs and job deletion. +It can be accessed at `/pleroma/oban`. + +!!! danger + This dashboard is very powerful! If you are unsure what a certain feature does, don't use it. + Changing individual queue state/settings in the "Queues" view is heavily discouraged. + +* Shows a real time chart of either a number of executed jobs, or job execution/wait time per a given time frame and the state/queue/worker. +* Shows a list of jobs in each state, their argument, number of attempts and execution/scheduled time. +* Selecting one or multiple jobs in the list allows performing actions like canceling/deleting and retrying. +* Clicking on a job shows a detailed view including the full argument, when it was inserted, information about its attempts, and performing actions on it. From f6c410b06c9864a884ee9ab622f533a9340694d1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 11:41:12 +0100 Subject: [PATCH 145/317] Move LiveDashboard to /pleroma/live_dashboard --- changelog.d/phoenix-livedashboard-move.change | 1 + docs/administration/dashboards.md | 2 +- lib/pleroma/web/fallback/redirect_controller.ex | 5 +++++ lib/pleroma/web/router.ex | 8 ++++---- test/pleroma/web/fallback_test.exs | 4 ++++ 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 changelog.d/phoenix-livedashboard-move.change diff --git a/changelog.d/phoenix-livedashboard-move.change b/changelog.d/phoenix-livedashboard-move.change new file mode 100644 index 000000000..116b1523a --- /dev/null +++ b/changelog.d/phoenix-livedashboard-move.change @@ -0,0 +1 @@ +Moved Phoenix LiveDashboard to /pleroma/live_dashboard diff --git a/docs/administration/dashboards.md b/docs/administration/dashboards.md index 8e8987633..b95e0fac0 100644 --- a/docs/administration/dashboards.md +++ b/docs/administration/dashboards.md @@ -11,7 +11,7 @@ Pleroma comes with two types of backend dashboards viewable to instance administ ## Phoenix LiveDashboard -Instance administrators can access this dashboard at `/phoenix/live_dashboard`, giving a simple overview of software versions including Erlang and Elixir versions, instance uptime and resource consumption. +Instance administrators can access this dashboard at `/pleroma/live_dashboard`, giving a simple overview of software versions including Erlang and Elixir versions, instance uptime and resource consumption. This dashboard gives insights into the current state of the BEAM VM running Pleroma code and database statistics including basic diagnostics. It can be useful for troubleshooting of some issues namely regarding database performance. diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 60fc15b9e..d75a95fb3 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -29,6 +29,11 @@ defmodule Pleroma.Web.Fallback.RedirectController do ) end + def live_dashboard(conn, _params) do + conn + |> redirect(to: "/pleroma/live_dashboard") + end + def redirector(conn, _params, code \\ 200) do {:ok, index_content} = File.read(index_file_path(conn)) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index df1812bd5..008f48575 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1045,7 +1045,7 @@ defmodule Pleroma.Web.Router do scope "/" do pipe_through([:pleroma_html, :authenticate, :require_admin]) - live_dashboard("/phoenix/live_dashboard", additional_pages: [oban: Oban.LiveDashboard]) + live_dashboard("/pleroma/live_dashboard", additional_pages: [oban: Oban.LiveDashboard]) oban_dashboard("/pleroma/oban") end @@ -1087,15 +1087,15 @@ defmodule Pleroma.Web.Router do get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, []) get("/api/*path", RedirectController, :api_not_implemented) + get("/phoenix/live_dashboard", RedirectController, :live_dashboard) get("/*path", RedirectController, :redirector_with_preload) options("/*path", RedirectController, :empty) end - # /pleroma/oban/* needs to get filtered out from api routes for frontend configuration + # /pleroma/{phoenix,oban}/* need to get filtered out from api routes for frontend configuration # to not drop admin overrides for /pleroma/admin. - # Also removing /phoenix since it is not an API route - @non_api_routes ["/phoenix/live_dashboard", "/pleroma/oban"] + @non_api_routes ["/pleroma/live_dashboard", "/pleroma/oban"] def get_api_routes do Phoenix.Router.routes(__MODULE__) diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs index 9184cf8f1..6d0ba3d2a 100644 --- a/test/pleroma/web/fallback_test.exs +++ b/test/pleroma/web/fallback_test.exs @@ -77,6 +77,10 @@ defmodule Pleroma.Web.FallbackTest do assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" end + test "GET /phoenix/live_dashboard -> /pleroma/live_dashboard", %{conn: conn} do + assert redirected_to(get(conn, "/phoenix/live_dashboard")) =~ "/pleroma/live_dashboard" + end + test "OPTIONS /*path", %{conn: conn} do assert conn |> options("/foo") From 519ef4be5efd7abb664b1f4368883219dff871c3 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 14 Jan 2026 02:37:55 +0100 Subject: [PATCH 146/317] mix: upgrade vix from "~> 0.26.0" to "~> 0.36" Dropping the last zero should allow to get 0.x updates rather than only 0.36.x updates. Fixes: https://git.pleroma.social/pleroma/pleroma/-/issues/3393 --- changelog.d/vix-0.36.0.fix | 1 + mix.exs | 2 +- mix.lock | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/vix-0.36.0.fix diff --git a/changelog.d/vix-0.36.0.fix b/changelog.d/vix-0.36.0.fix new file mode 100644 index 000000000..43a8dd8f8 --- /dev/null +++ b/changelog.d/vix-0.36.0.fix @@ -0,0 +1 @@ +Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 diff --git a/mix.exs b/mix.exs index 95783cf4e..a4415fddc 100644 --- a/mix.exs +++ b/mix.exs @@ -193,7 +193,7 @@ defmodule Pleroma.Mixfile do {:majic, "~> 1.0"}, {:open_api_spex, "~> 3.16"}, {:ecto_psql_extras, "~> 0.8"}, - {:vix, "~> 0.26.0"}, + {:vix, "~> 0.36"}, {:elixir_make, "~> 0.7.7", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, {:exile, "~> 0.10.0"}, diff --git a/mix.lock b/mix.lock index a2c5e1ec7..8e1f684dc 100644 --- a/mix.lock +++ b/mix.lock @@ -152,7 +152,7 @@ "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, - "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"}, + "vix": {:hex, :vix, "0.36.0", "3132dc065beda06dab1895a53d8c852d8e6a5bbca375c609435e968b1290e113", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "92f912b4e90c453f92942742105bcdb367ad53406759da251bd2e587e33f4134"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, From 861a9f93659482739db09ace9fcf4a4df6276384 Mon Sep 17 00:00:00 2001 From: MediaFormat Date: Sun, 11 Jan 2026 01:12:42 +0000 Subject: [PATCH 147/317] Change redirect_uris to accept array of strings --- .../web/api_spec/operations/admin/o_auth_app_operation.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex index 2b2496c26..3dbef62cd 100644 --- a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex @@ -123,7 +123,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do name: %Schema{type: :string, description: "Application Name"}, scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, redirect_uris: %Schema{ - type: :string, + type: :array, items: %Schema{type: :string}, description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, @@ -141,7 +141,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do }, example: %{ "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", + "redirect_uris" => ["https://myapp.com/auth/callback"], "website" => "https://myapp.com/", "scopes" => ["read", "write"], "trusted" => true @@ -157,7 +157,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do name: %Schema{type: :string, description: "Application Name"}, scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, redirect_uris: %Schema{ - type: :string, + type: :array, items: %Schema{type: :string}, description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, @@ -175,7 +175,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do }, example: %{ "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", + "redirect_uris" => ["https://myapp.com/auth/callback"], "website" => "https://myapp.com/", "scopes" => ["read", "write"], "trusted" => true From ba280b2d0fda75faf7cdd4f9b3ee3d7269c6ecc3 Mon Sep 17 00:00:00 2001 From: MediaFormat Date: Sun, 11 Jan 2026 01:15:55 +0000 Subject: [PATCH 148/317] add changelog.d entry --- changelog.d/oauth-registration-redirect_uris.fix | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/oauth-registration-redirect_uris.fix diff --git a/changelog.d/oauth-registration-redirect_uris.fix b/changelog.d/oauth-registration-redirect_uris.fix new file mode 100644 index 000000000..e69de29bb From cb389e788d8759398bf44fe1c8b4755690ae7c3b Mon Sep 17 00:00:00 2001 From: MediaFormat Date: Sun, 11 Jan 2026 05:34:17 +0000 Subject: [PATCH 149/317] fix field type, fix formatting --- .../web/api_spec/operations/admin/o_auth_app_operation.ex | 6 ++++-- lib/pleroma/web/o_auth/app.ex | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex index 3dbef62cd..5d4306754 100644 --- a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex @@ -123,7 +123,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do name: %Schema{type: :string, description: "Application Name"}, scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, redirect_uris: %Schema{ - type: :array, items: %Schema{type: :string}, + type: :array, + items: %Schema{type: :string}, description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, @@ -157,7 +158,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do name: %Schema{type: :string, description: "Application Name"}, scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, redirect_uris: %Schema{ - type: :array, items: %Schema{type: :string}, + type: :array, + items: %Schema{type: :string}, description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex index 7661c2566..f1145d500 100644 --- a/lib/pleroma/web/o_auth/app.ex +++ b/lib/pleroma/web/o_auth/app.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.OAuth.App do schema "apps" do field(:client_name, :string) - field(:redirect_uris, :string) + field(:redirect_uris, {:array, :string}) field(:scopes, {:array, :string}, default: []) field(:website, :string) field(:client_id, :string) From ad5bb02bd6009a75d25ee57b0a2da26070ccd029 Mon Sep 17 00:00:00 2001 From: MediaFormat Date: Sun, 11 Jan 2026 17:47:27 +0000 Subject: [PATCH 150/317] fix tests --- test/pleroma/web/o_auth/app_test.exs | 14 +++++++------- test/pleroma/web/o_auth/o_auth_controller_test.exs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs index a69ba371e..f90bee358 100644 --- a/test/pleroma/web/o_auth/app_test.exs +++ b/test/pleroma/web/o_auth/app_test.exs @@ -10,20 +10,20 @@ defmodule Pleroma.Web.OAuth.AppTest do describe "get_or_make/2" do test "gets exist app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) {:ok, %App{} = exist_app} = App.get_or_make(attrs, []) assert exist_app == app end test "make app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) assert app.scopes == ["write"] end test "gets exist app and updates scopes" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"]) assert exist_app.id == app.id @@ -31,10 +31,10 @@ defmodule Pleroma.Web.OAuth.AppTest do end test "has unique client_id" do - insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") + insert(:oauth_app, client_name: "", redirect_uris: [""], client_id: "boop") error = - catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) + catch_error(insert(:oauth_app, client_name: "", redirect_uris: [""], client_id: "boop")) assert %Ecto.ConstraintError{} = error assert error.constraint == "apps_client_id_index" @@ -55,7 +55,7 @@ defmodule Pleroma.Web.OAuth.AppTest do end test "removes orphaned apps" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} {:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"]) # backdate the old app so it's within the threshold for being cleaned up @@ -66,7 +66,7 @@ defmodule Pleroma.Web.OAuth.AppTest do |> Pleroma.Repo.query([one_hour_ago, old_app.id]) # Create the new app after backdating the old one - attrs = %{client_name: "PleromaFE", redirect_uris: "."} + attrs = %{client_name: "PleromaFE", redirect_uris: ["."]} {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) # Ensure the new app has a recent timestamp diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index 260442771..3788b9c65 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -406,7 +406,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do describe "GET /oauth/authorize" do setup do [ - app: insert(:oauth_app, redirect_uris: "https://redirect.url"), + app: insert(:oauth_app, redirect_uris: ["https://redirect.url"]), conn: build_conn() |> Plug.Session.call(Plug.Session.init(@session_opts)) From 820a4cd97c7f933cfcba8a884b2cb83f980a642c Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 11:25:39 +0400 Subject: [PATCH 151/317] Fix OAuth registration redirect_uris array support --- .../oauth-registration-redirect_uris.fix | 1 + .../operations/admin/o_auth_app_operation.ex | 12 ++++++--- .../web/api_spec/operations/app_operation.ex | 5 +++- lib/pleroma/web/o_auth/app.ex | 25 ++++++++++++++++- .../o_auth_app_controller_test.exs | 22 +++++++++++++++ .../controllers/app_controller_test.exs | 27 +++++++++++++++++++ test/pleroma/web/o_auth/app_test.exs | 14 +++++----- .../web/o_auth/o_auth_controller_test.exs | 2 +- 8 files changed, 94 insertions(+), 14 deletions(-) diff --git a/changelog.d/oauth-registration-redirect_uris.fix b/changelog.d/oauth-registration-redirect_uris.fix index e69de29bb..76ace55df 100644 --- a/changelog.d/oauth-registration-redirect_uris.fix +++ b/changelog.d/oauth-registration-redirect_uris.fix @@ -0,0 +1 @@ +Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex index 5d4306754..7d83066ca 100644 --- a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex @@ -123,8 +123,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do name: %Schema{type: :string, description: "Application Name"}, scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, redirect_uris: %Schema{ - type: :array, - items: %Schema{type: :string}, + oneOf: [ + %Schema{type: :string}, + %Schema{type: :array, items: %Schema{type: :string}} + ], description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, @@ -158,8 +160,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do name: %Schema{type: :string, description: "Application Name"}, scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, redirect_uris: %Schema{ - type: :array, - items: %Schema{type: :string}, + oneOf: [ + %Schema{type: :string}, + %Schema{type: :array, items: %Schema{type: :string}} + ], description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex index dfa2237c0..71c15a665 100644 --- a/lib/pleroma/web/api_spec/operations/app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -97,7 +97,10 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do properties: %{ client_name: %Schema{type: :string, description: "A name for your application."}, redirect_uris: %Schema{ - type: :string, + oneOf: [ + %Schema{type: :string}, + %Schema{type: :array, items: %Schema{type: :string}} + ], description: "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." }, diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex index f1145d500..a2841b2bb 100644 --- a/lib/pleroma/web/o_auth/app.ex +++ b/lib/pleroma/web/o_auth/app.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.OAuth.App do schema "apps" do field(:client_name, :string) - field(:redirect_uris, {:array, :string}) + field(:redirect_uris, :string) field(:scopes, {:array, :string}, default: []) field(:website, :string) field(:client_id, :string) @@ -31,9 +31,32 @@ defmodule Pleroma.Web.OAuth.App do @spec changeset(t(), map()) :: Ecto.Changeset.t() def changeset(struct, params) do + params = normalize_redirect_uris_param(params) + cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted, :user_id]) end + defp normalize_redirect_uris_param(%{} = params) do + case params do + %{redirect_uris: redirect_uris} when is_list(redirect_uris) -> + Map.put(params, :redirect_uris, normalize_redirect_uris(redirect_uris)) + + %{"redirect_uris" => redirect_uris} when is_list(redirect_uris) -> + Map.put(params, "redirect_uris", normalize_redirect_uris(redirect_uris)) + + _ -> + params + end + end + + defp normalize_redirect_uris(redirect_uris) when is_list(redirect_uris) do + redirect_uris + |> Enum.filter(&is_binary/1) + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "")) + |> Enum.join("\n") + end + @spec register_changeset(t(), map()) :: Ecto.Changeset.t() def register_changeset(struct, params \\ %{}) do changeset = diff --git a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs index 10eefbeca..2c2d13bb7 100644 --- a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs @@ -57,6 +57,28 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do } = response end + test "success with redirect_uris array", %{conn: conn} do + base_url = Endpoint.url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: [base_url] + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => false + } = response + end + test "with trusted", %{conn: conn} do base_url = Endpoint.url() app_name = "Trusted app" diff --git a/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs index bc9d4048c..45902d7d9 100644 --- a/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs @@ -61,6 +61,33 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do assert app.user_id == nil end + test "creates an oauth app with redirect_uris array", %{conn: conn} do + app_attrs = build(:oauth_app) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/apps", %{ + client_name: app_attrs.client_name, + redirect_uris: [app_attrs.redirect_uris] + }) + + [app] = Repo.all(App) + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "client_id" => app.client_id, + "client_secret" => app.client_secret, + "id" => app.id |> to_string(), + "redirect_uri" => app.redirect_uris, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response_and_validate_schema(conn, 200) + assert app.user_id == nil + end + test "creates an oauth app with a user", %{conn: conn} do user = insert(:user) app_attrs = build(:oauth_app) diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs index f90bee358..a69ba371e 100644 --- a/test/pleroma/web/o_auth/app_test.exs +++ b/test/pleroma/web/o_auth/app_test.exs @@ -10,20 +10,20 @@ defmodule Pleroma.Web.OAuth.AppTest do describe "get_or_make/2" do test "gets exist app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) {:ok, %App{} = exist_app} = App.get_or_make(attrs, []) assert exist_app == app end test "make app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) assert app.scopes == ["write"] end test "gets exist app and updates scopes" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"]) assert exist_app.id == app.id @@ -31,10 +31,10 @@ defmodule Pleroma.Web.OAuth.AppTest do end test "has unique client_id" do - insert(:oauth_app, client_name: "", redirect_uris: [""], client_id: "boop") + insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") error = - catch_error(insert(:oauth_app, client_name: "", redirect_uris: [""], client_id: "boop")) + catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) assert %Ecto.ConstraintError{} = error assert error.constraint == "apps_client_id_index" @@ -55,7 +55,7 @@ defmodule Pleroma.Web.OAuth.AppTest do end test "removes orphaned apps" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: ["."]} + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} {:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"]) # backdate the old app so it's within the threshold for being cleaned up @@ -66,7 +66,7 @@ defmodule Pleroma.Web.OAuth.AppTest do |> Pleroma.Repo.query([one_hour_ago, old_app.id]) # Create the new app after backdating the old one - attrs = %{client_name: "PleromaFE", redirect_uris: ["."]} + attrs = %{client_name: "PleromaFE", redirect_uris: "."} {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) # Ensure the new app has a recent timestamp diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index 3788b9c65..260442771 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -406,7 +406,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do describe "GET /oauth/authorize" do setup do [ - app: insert(:oauth_app, redirect_uris: ["https://redirect.url"]), + app: insert(:oauth_app, redirect_uris: "https://redirect.url"), conn: build_conn() |> Plug.Session.call(Plug.Session.init(@session_opts)) From 029994aa75a41aeec3966b6fb8b029eea2cbbfa7 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 16:17:21 +0400 Subject: [PATCH 152/317] InstanceView: Omit comment if it's empty --- .../web/mastodon_api/views/instance_view.ex | 11 ++++++++--- .../controllers/instance_controller_test.exs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 18c6c0f80..0dc7a5fea 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -123,12 +123,17 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do host in exclusions or not Map.has_key?(@block_severities, rule) end) |> Enum.map(fn {host, reason} -> - %{ + domain_block = %{ domain: host, digest: :crypto.hash(:sha256, host) |> Base.encode16(case: :lower), - severity: Map.get(@block_severities, rule), - comment: reason + severity: Map.get(@block_severities, rule) } + + if not_empty_string(reason) do + Map.put(domain_block, :comment, reason) + else + domain_block + end end) end) |> List.flatten() diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index e0c9091e0..461b46066 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -171,6 +171,22 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do ] == json_response_and_validate_schema(conn, 200) end + test "omits comment field if comment is empty", %{conn: conn} do + clear_config([:mrf_simple, :reject], ["fediverse.pl"]) + + conn = get(conn, "/api/v1/instance/domain_blocks") + + assert [ + %{ + "digest" => "55e3f44aefe7eb022d3b1daaf7396cabf7f181bf6093c8ea841e30c9fc7d8226", + "domain" => "fediverse.pl", + "severity" => "suspend" + } = domain_block + ] = json_response_and_validate_schema(conn, 200) + + refute Map.has_key?(domain_block, "comment") + end + test "returns empty array if mrf transparency is disabled", %{conn: conn} do clear_config([:mrf, :transparency], false) From 0b813b9c4a8dfbc9d191f05e0e066d41f92598ad Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 02:24:07 +0400 Subject: [PATCH 153/317] mrf(media_proxy_warming): avoid adapter-level redirects Drop follow_redirect/force_redirect from the HTTP options used when warming MediaProxy, relying on Tesla middleware instead (Hackney redirect handling can crash behind CONNECT proxies). Also add a regression assertion in the policy test and document the upstream Hackney issues in ReverseProxy redirect handling. --- lib/pleroma/reverse_proxy/client/hackney.ex | 7 +++++-- .../mrf/media_proxy_warming_policy.ex | 9 +++++++- .../mrf/media_proxy_warming_policy_test.exs | 21 +++++++------------ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index 7ccd28bb1..7e1fca80d 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -5,9 +5,12 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @behaviour Pleroma.ReverseProxy.Client - # redirect handler from Pleb, slightly modified to work with Hackney + # In-app redirect handler to avoid Hackney redirect bugs: + # - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney) + # - https://github.com/benoitc/hackney/issues/273 (redirects not followed when using HTTP proxy) + # + # Based on a redirect handler from Pleb, slightly modified to work with Hackney: # https://declin.eu/objects/d4f38e62-5429-4614-86d1-e8fc16e6bf33 - # https://github.com/benoitc/hackney/issues/273 @redirect_statuses [301, 302, 303, 307, 308] defp absolute_redirect_url(original_url, resp_headers) do location = diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index b0d07a6f8..43c2c0449 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -27,7 +27,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do end defp fetch(url) do - http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media) + # This module uses Tesla (Pleroma.HTTP) to fetch the MediaProxy URL. + # Redirect following is handled by Tesla middleware, so we must not enable + # adapter-level redirect logic (Hackney can crash on relative redirects when proxied). + http_client_opts = + [:media_proxy, :proxy_opts, :http] + |> Pleroma.Config.get(pool: :media) + |> Keyword.drop([:follow_redirect, :force_redirect]) + HTTP.get(url, [], http_client_opts) end diff --git a/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs b/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs index 0da3afa3b..4b94b9ac7 100644 --- a/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs @@ -54,14 +54,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do setup do: clear_config([:media_proxy, :enabled], true) test "it prefetches media proxy URIs" do - Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} -> - {:ok, %Tesla.Env{status: 200, body: ""}} - end) - - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + with_mock HTTP, + get: fn _, _, opts -> + send(self(), {:prefetch_opts, opts}) + {:ok, []} + end do MediaProxyWarmingPolicy.filter(@message) assert called(HTTP.get(:_, :_, :_)) + assert_receive {:prefetch_opts, opts} + refute Keyword.has_key?(opts, :follow_redirect) + refute Keyword.has_key?(opts, :force_redirect) end end @@ -81,10 +84,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do end test "history-aware" do - Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} -> - {:ok, %Tesla.Env{status: 200, body: ""}} - end) - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history) @@ -93,10 +92,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do end test "works with Updates" do - Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} -> - {:ok, %Tesla.Env{status: 200, body: ""}} - end) - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history |> Map.put("type", "Update")) From 6439a5b334212eae54e5e336ce8c68862f0045fc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 22 Jan 2026 23:01:11 +0100 Subject: [PATCH 154/317] MastoAPI AccountView: Add mute/block expiry to the relationship key --- .../web/mastodon_api/views/account_view.ex | 40 ++++++---- .../mastodon_api/views/account_view_test.exs | 78 ++++++++++++++++++- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a7d994593..909212711 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -124,6 +124,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do target, &User.blocks_user?(&1, &2) ), + block_expires_at: maybe_put_block_expires_at(user_relationships, target, reading_user), blocked_by: UserRelationship.exists?( user_relationships, @@ -140,6 +141,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do target, &User.mutes?(&1, &2) ), + mute_expires_at: maybe_put_mute_expires_at(user_relationships, target, reading_user), muting_notifications: UserRelationship.exists?( user_relationships, @@ -343,8 +345,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) - |> maybe_put_mute_expires_at(user, opts[:for], opts) - |> maybe_put_block_expires_at(user, opts[:for], opts) |> maybe_show_birthday(user, opts[:for]) end @@ -472,26 +472,32 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_email_address(data, _, _), do: data - defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do - Map.put( - data, - :mute_expires_at, - UserRelationship.get_mute_expire_date(target, user) - ) + defp maybe_put_mute_expires_at(user_relationships, %User{} = target, %User{} = user) do + cond do + UserRelationship.exists?(user_relationships, :mute, user, target, &User.mutes_user?(&1, &2)) -> + UserRelationship.get_mute_expire_date(user, target) + + true -> + nil + end end - defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(user_relationships, %User{} = target, %User{} = user) do + cond do + UserRelationship.exists?( + user_relationships, + :block, + user, + target, + &User.blocks_user?(&1, &2) + ) -> + UserRelationship.get_block_expire_date(user, target) - defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do - Map.put( - data, - :block_expires_at, - UserRelationship.get_block_expire_date(target, user) - ) + true -> + nil + end end - defp maybe_put_block_expires_at(data, _, _, _), do: data - defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data |> Kernel.put_in([:pleroma, :birthday], user.birthday) diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index bd151cc61..c230cf653 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -439,8 +439,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do following: false, followed_by: false, blocking: false, + block_expires_at: nil, blocked_by: false, muting: false, + mute_expires_at: nil, muting_notifications: false, subscribing: false, notifying: false, @@ -536,6 +538,53 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do test_relationship_rendering(user, other_user, expected) end + test "represent a relationship for the blocking and blocked user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, user, other_user} = User.follow(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60}) + {:ok, _user_relationship} = User.block(other_user, user) + + expected = + Map.merge( + @blank_response, + %{ + following: false, + blocking: true, + block_expires_at: date, + blocked_by: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the muting user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, _user_relationship} = + User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) + + expected = + Map.merge( + @blank_response, + %{ + muting: true, + mute_expires_at: date, + muting_notifications: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + test "represent a relationship for the user blocking a domain" do user = insert(:user) other_user = insert(:user, ap_id: "https://bad.site/users/other_user") @@ -856,12 +905,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) %{ - mute_expires_at: mute_expires_at - } = AccountView.render("show.json", %{user: other_user, for: user, mutes: true}) + pleroma: %{ + relationship: %{ + mute_expires_at: mute_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( mute_expires_at, DateTime.utc_now() |> DateTime.add(24 * 60 * 60) ) in -3..3 end + + test "renders block expiration date" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = + User.block(user, other_user, %{duration: 24 * 60 * 60}) + + %{ + pleroma: %{ + relationship: %{ + block_expires_at: block_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) + + assert DateTime.diff( + block_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end end From f4a8c426df5eb6589ec3375ca8e450ab4720d960 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 24 Jan 2026 21:33:49 +0100 Subject: [PATCH 155/317] MastoAPI AccountView: Readd block/mute expiry outside the relationship --- .../web/mastodon_api/views/account_view.ex | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 909212711..e215b073e 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -345,6 +345,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) + |> maybe_put_mute_expires_at(user, opts[:for], relationship) + |> maybe_put_block_expires_at(user, opts[:for], relationship) |> maybe_show_birthday(user, opts[:for]) end @@ -482,6 +484,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end end + defp maybe_put_mute_expires_at(data, %User{} = target, %User{} = user, relationship) do + cond do + Map.has_key?(relationship, :mute_expires_at) -> + Map.put(data, :mute_expires_at, relationship.mute_expires_at) + + User.mutes_user?(user, target) -> + Map.put(data, :mute_expires_at, UserRelationship.get_mute_expire_date(user, target)) + + true -> + Map.put(data, :mute_expires_at, nil) + end + end + + defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(user_relationships, %User{} = target, %User{} = user) do cond do UserRelationship.exists?( @@ -498,6 +515,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end end + defp maybe_put_block_expires_at(data, %User{} = target, %User{} = user, relationship) do + cond do + Map.has_key?(relationship, :block_expires_at) -> + Map.put(data, :block_expires_at, relationship.block_expires_at) + + User.blocks_user?(user, target) -> + Map.put(data, :block_expires_at, UserRelationship.get_block_expire_date(user, target)) + + true -> + Map.put(data, :block_expires_at, nil) + end + end + + defp maybe_put_block_expires_at(data, _, _, _), do: data + defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data |> Kernel.put_in([:pleroma, :birthday], user.birthday) From 521fc70e4878fde6c2ef2aef5f640c9dc5e9dbf2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 24 Jan 2026 21:35:27 +0100 Subject: [PATCH 156/317] MastoAPI AccountView AccountController: Add more block/mute expiry tests --- .../controllers/account_controller_test.exs | 103 ++++++++++++++++-- .../mastodon_api/views/account_view_test.exs | 6 +- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 02da781dd..ea98b53a8 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1901,7 +1901,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationships} = User.mute(user, other_user1) {:ok, _user_relationships} = User.mute(user, other_user2) - {:ok, _user_relationships} = User.mute(user, other_user3) + {:ok, _user_relationships} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -1937,6 +1943,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id3}] = result + + result = + conn + |> get("/api/v1/mutes") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "mute_expires_at" => ^date}, + %{"id" => ^id2, "mute_expires_at" => nil}, + %{"id" => ^id1, "mute_expires_at" => nil} + ] = result end test "list of mutes with with_relationships parameter" do @@ -1951,20 +1968,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.mute(user, other_user1) {:ok, _} = User.mute(user, other_user2) - {:ok, _} = User.mute(user, other_user3) + {:ok, _} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => ^date, + "followed_by" => true + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } } ] = conn @@ -1980,7 +2021,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationship} = User.block(user, other_user1) {:ok, _user_relationship} = User.block(user, other_user3) - {:ok, _user_relationship} = User.block(user, other_user2) + {:ok, _user_relationship} = User.block(user, other_user2, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -2045,6 +2092,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id1}] = result + + result = + conn + |> assign(:user, user) + |> get("api/v1/blocks") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "block_expires_at" => nil}, + %{"id" => ^id2, "block_expires_at" => ^date}, + %{"id" => ^id1, "block_expires_at" => nil} + ] = result end test "list of blocks with with_relationships parameter" do @@ -2059,20 +2118,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.block(user, other_user1) {:ok, _} = User.block(user, other_user2) - {:ok, _} = User.block(user, other_user3) + {:ok, _} = User.block(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => ^date, + "followed_by" => false + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } } ] = conn diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c230cf653..6984442cc 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -909,7 +909,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ mute_expires_at: mute_expires_at } - } + }, + mute_expires_at: mute_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( @@ -930,7 +931,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ block_expires_at: block_expires_at } - } + }, + block_expires_at: block_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( From 80ede85f75f24c968bfa63fb52a7616c32fc788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 19 Sep 2025 16:43:55 +0200 Subject: [PATCH 157/317] Allow assigning users to reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/assign-users.add | 1 + docs/development/API/admin_api.md | 33 +++++++ lib/pleroma/constants.ex | 3 +- lib/pleroma/moderation_log.ex | 38 +++++++- lib/pleroma/web/activity_pub/activity_pub.ex | 9 ++ lib/pleroma/web/activity_pub/utils.ex | 28 ++++++ .../controllers/report_controller.ex | 55 ++++++++++- lib/pleroma/web/admin_api/report.ex | 13 ++- .../web/admin_api/views/report_view.ex | 16 +++- .../operations/admin/report_operation.ex | 55 ++++++++++- lib/pleroma/web/common_api.ex | 16 ++++ lib/pleroma/web/router.ex | 1 + ...00_add_activity_assigned_account_index.exs | 11 +++ test/pleroma/web/activity_pub/utils_test.exs | 13 +++ .../controllers/report_controller_test.exs | 92 +++++++++++++++++++ .../web/admin_api/views/report_view_test.exs | 2 + test/pleroma/web/common_api_test.exs | 23 +++++ 17 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 changelog.d/assign-users.add create mode 100644 priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs diff --git a/changelog.d/assign-users.add b/changelog.d/assign-users.add new file mode 100644 index 000000000..f50ad94c6 --- /dev/null +++ b/changelog.d/assign-users.add @@ -0,0 +1 @@ +Allow assigning users to reports \ No newline at end of file diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 64c06ca2b..3719ceeb9 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -665,6 +665,7 @@ Status: 404 - *optional* `limit`: **integer** the number of records to retrieve - *optional* `page`: **integer** page number - *optional* `page_size`: **integer** number of log entries per page (default is `50`) + - *optional* `assigned_account`: **string** assigned account ID - Response: - On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin - On success: JSON, returns a list of reports, where: @@ -749,6 +750,7 @@ Status: 404 "url": "https://pleroma.example.org/users/lain", "username": "lain" }, + "assigned_account": null, "content": "Please delete it", "created_at": "2019-04-29T19:48:15.000Z", "id": "9iJGOv1j8hxuw19bcm", @@ -868,6 +870,37 @@ Status: 404 ] ``` +- Response: + - On failure: + - 400 Bad Request, JSON: + + ```json + [ + { + `id`, // report id + `error` // error message + } + ] + ``` + + - On success: `204`, empty response + +## `POST /api/v1/pleroma/admin/reports/assign_account` + +### Assign account to one or multiple reports + +- Params: + +```json + `reports`: [ + { + `id`, // required, report id + `nickname` // account nickname, use null to unassign account + }, + ... + ] +``` + - Response: - On failure: - 400 Bad Request, JSON: diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index c0411edbf..f78b84099 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -22,7 +22,8 @@ defmodule Pleroma.Constants do "generator", "rules", "language", - "voters" + "voters", + "assigned_account" ] ) diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 52a71bc2d..90219312c 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -132,11 +132,18 @@ defmodule Pleroma.ModerationLog do end def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs) - when action in ["report_note_delete", "report_update", "report_note"] do + when action in [ + "report_note_delete", + "report_update", + "report_note", + "report_unassigned", + "report_assigned" + ] do data = attrs |> prepare_log_data |> Pleroma.Maps.put_if_present("text", attrs[:text]) + |> Pleroma.Maps.put_if_present("assigned_account", attrs[:assigned_account]) |> Map.merge(%{"subject" => report_to_map(subject)}) insert_log_entry_with_message(%ModerationLog{data: data}) @@ -441,6 +448,35 @@ defmodule Pleroma.ModerationLog do " with '#{state}' state" end + def get_log_entry_message( + %ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_assigned", + "subject" => %{"id" => subject_id, "type" => "report"}, + "assigned_account" => assigned_account + } + } = log + ) do + "@#{actor_nickname} assigned report ##{subject_id}" <> + subject_actor_nickname(log, " (on user ", ")") <> + " to user #{assigned_account}" + end + + def get_log_entry_message( + %ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_unassigned", + "subject" => %{"id" => subject_id, "type" => "report"} + } + } = log + ) do + "@#{actor_nickname} unassigned report ##{subject_id}" <> + subject_actor_nickname(log, " (on user ", ")") <> + " from a user" + end + def get_log_entry_message( %ModerationLog{ data: %{ diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e58e3dd57..44e9a22e5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1003,6 +1003,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_state(query, _), do: query + defp restrict_assigned_account(query, %{assigned_account: assigned_account}) do + from(activity in query, + where: fragment("?->>'assigned_account' = ?", activity.data, ^assigned_account) + ) + end + + defp restrict_assigned_account(query, _), do: query + defp restrict_favorited_by(query, %{favorited_by: ap_id}) do from( [_activity, object] in query, @@ -1471,6 +1479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_actor(opts) |> restrict_type(opts) |> restrict_state(opts) + |> restrict_assigned_account(opts) |> restrict_favorited_by(opts) |> restrict_blocked(restrict_blocked_opts) |> restrict_blockers_visibility(opts) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index c5a6901d4..43c0f456d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -863,6 +863,34 @@ defmodule Pleroma.Web.ActivityPub.Utils do def update_report_state(_, _), do: {:error, "Unsupported state"} + def assign_report_to_account(%Activity{} = activity, nil = _account) do + new_data = Map.delete(activity.data, "assigned_account") + + activity + |> Changeset.change(data: new_data) + |> Repo.update() + end + + def assign_report_to_account(%Activity{} = activity, account) do + new_data = Map.put(activity.data, "assigned_account", account) + + activity + |> Changeset.change(data: new_data) + |> Repo.update() + end + + def assign_report_to_account(activity_ids, account) do + activities_num = length(activity_ids) + + from(a in Activity, where: a.id in ^activity_ids) + |> update(set: [data: fragment("jsonb_set(data, '{assigned_account}', ?)", ^account)]) + |> Repo.update_all([]) + |> case do + {^activities_num, _} -> :ok + _ -> {:error, activity_ids} + end + end + def strip_report_status_data(%Activity{} = activity) do with {:ok, new_data} <- strip_report_status_data(activity.data) do {:ok, %{activity | data: new_data}} diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex index 89d8cc820..dbac03ef4 100644 --- a/lib/pleroma/web/admin_api/controllers/report_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do alias Pleroma.Activity alias Pleroma.ModerationLog alias Pleroma.ReportNote + alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report @@ -24,7 +25,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do plug( OAuthScopesPlug, %{scopes: ["admin:write:reports"]} - when action in [:update, :notes_create, :notes_delete] + when action in [:update, :assign_account, :notes_create, :notes_delete] ) action_fallback(AdminAPI.FallbackController) @@ -79,6 +80,22 @@ defmodule Pleroma.Web.AdminAPI.ReportController do end end + def assign_account( + %{ + assigns: %{user: admin}, + private: %{open_api_spex: %{body_params: %{reports: reports}}} + } = conn, + _ + ) do + result = Enum.map(reports, &do_assign_account(&1, admin)) + + if Enum.any?(result, &Map.has_key?(&1, :error)) do + json_response(conn, :bad_request, result) + else + json_response(conn, :no_content, "") + end + end + def notes_create( %{ assigns: %{user: user}, @@ -131,4 +148,40 @@ defmodule Pleroma.Web.AdminAPI.ReportController do _ -> json_response(conn, :bad_request, "") end end + + defp do_assign_account(%{assigned_account: nil, id: id}, admin) do + with {:ok, activity} <- CommonAPI.assign_report_to_account(id, nil), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_unassigned", + actor: admin, + subject: activity, + subject_actor: report.user_actor + }) + + activity + else + {:error, message} -> + %{id: id, error: message} + end + end + + defp do_assign_account(%{assigned_account: assigned_account, id: id}, admin) do + with %User{id: account} = user <- User.get_cached_by_nickname(assigned_account), + {:ok, activity} <- CommonAPI.assign_report_to_account(id, account), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_assigned", + actor: admin, + subject: activity, + subject_actor: report.user_actor, + assigned_account: user.nickname + }) + + activity + else + {:error, message} -> + %{id: id, error: message} + end + end end diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex index fa89e3405..753b92d88 100644 --- a/lib/pleroma/web/admin_api/report.ex +++ b/lib/pleroma/web/admin_api/report.ex @@ -13,6 +13,11 @@ defmodule Pleroma.Web.AdminAPI.Report do user = User.get_cached_by_ap_id(actor) account = User.get_cached_by_ap_id(account_ap_id) + assigned_account = + if Map.has_key?(report.data, "assigned_account") do + User.get_cached_by_id(report.data["assigned_account"]) + end + statuses = status_ap_ids |> Enum.reject(&is_nil(&1)) @@ -26,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.Report do Activity.get_by_ap_id_with_object(act) end) - %{report: report, user: user, account: account, statuses: statuses} + %{ + report: report, + user: user, + account: account, + statuses: statuses, + assigned_account: assigned_account + } end defp make_fake_activity(act, user) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index b4b0be267..da6166050 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -26,7 +26,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do } end - def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do + def render("show.json", %{ + report: report, + user: user, + account: account, + statuses: statuses, + assigned_account: assigned_account + }) do created_at = Utils.to_masto_date(report.data["published"]) content = @@ -36,6 +42,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do nil end + assigned_account = + if assigned_account do + merge_account_views(assigned_account) + end + %{ id: report.id, account: merge_account_views(account), @@ -49,7 +60,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do }), state: report.data["state"], notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}), - rules: rules(Map.get(report.data, "rules", nil)) + rules: rules(Map.get(report.data, "rules", nil)), + assigned_account: assigned_account } end diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex index 25a604beb..58669a1fc 100644 --- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do :query, %Schema{type: :integer, default: 50}, "Number number of log entries per page" + ), + Operation.parameter( + :assigned_account, + :query, + %Schema{type: :string}, + "Filter by assigned account ID" ) | admin_api_params() ], @@ -103,6 +109,22 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do } end + def assign_account_operation do + %Operation{ + tags: ["Report management"], + summary: "Assign account to specified reports", + operationId: "AdminAPI.ReportController.assign_account", + security: [%{"oAuth" => ["admin:write:reports"]}], + parameters: admin_api_params(), + requestBody: request_body("Parameters", assign_account_request(), required: true), + responses: %{ + 204 => no_content_response(), + 400 => Operation.response("Bad Request", "application/json", update_400_response()), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + def notes_create_operation do %Operation{ tags: ["Report management"], @@ -186,7 +208,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do hint: %Schema{type: :string, nullable: true} } } - } + }, + assigned_account: + account_admin() + |> Map.put(:nullable, true) } } end @@ -242,6 +267,34 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do } end + defp assign_account_request do + %Schema{ + type: :object, + required: [:reports], + properties: %{ + reports: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{allOf: [FlakeID], description: "Required, report ID"}, + assigned_account: %Schema{ + type: :string, + description: "User nickname", + nullable: true + } + } + }, + example: %{ + "reports" => [ + %{"id" => "123", "assigned_account" => "pleroma"} + ] + } + } + } + } + end + defp update_400_response do %Schema{ type: :array, diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 8e96ef5b6..04181ad8f 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -709,6 +709,22 @@ defmodule Pleroma.Web.CommonAPI do end end + def assign_report_to_account(activity_ids, user) when is_list(activity_ids) do + case Utils.assign_report_to_account(activity_ids, user) do + :ok -> {:ok, activity_ids} + _ -> {:error, dgettext("errors", "Could not assign account")} + end + end + + def assign_report_to_account(activity_id, user) do + with %Activity{} = activity <- Activity.get_by_id(activity_id) do + Utils.assign_report_to_account(activity, user) + else + nil -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not assign account")} + end + end + @spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()} def update_activity_scope(activity_id, opts \\ %{}) do with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 008f48575..20ac1c67b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -395,6 +395,7 @@ defmodule Pleroma.Web.Router do get("/reports", ReportController, :index) get("/reports/:id", ReportController, :show) patch("/reports", ReportController, :update) + post("/reports/assign_account", ReportController, :assign_account) post("/reports/:id/notes", ReportController, :notes_create) delete("/reports/:report_id/notes/:id", ReportController, :notes_delete) end diff --git a/priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs b/priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs new file mode 100644 index 000000000..b54daf43d --- /dev/null +++ b/priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.AddActivityAssignedAccountIndex do + use Ecto.Migration + + def change do + create_if_not_exists( + index(:activities, ["(data->>'assigned_account')"], + name: :activities_assigned_account_index + ) + ) + end +end diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index f162f3684..3b77f0867 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -671,6 +671,19 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end + describe "assign_report_to_account/2" do + test "assigns report to an account" do + reporter = insert(:user) + target_account = insert(:user) + %{id: assigned_id} = insert(:user) + + {:ok, report} = CommonAPI.report(reporter, %{account_id: target_account.id}) + {:ok, report} = Utils.assign_report_to_account(report, assigned_id) + + assert %{data: %{"assigned_account" => ^assigned_id}} = report + end + end + describe "maybe_anonymize_reporter/1" do setup do reporter = insert(:user) diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index b626ddf55..9fbb608c4 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -388,6 +388,38 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do |> json_response_and_validate_schema(:ok) end + test "returns reports with specified assigned user", %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, _report} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I don't like this user" + }) + + CommonAPI.assign_report_to_account(second_report_id, admin.id) + + response = + conn + |> get(report_path(conn, :index, %{assigned_account: admin.id})) + |> json_response_and_validate_schema(:ok) + + assert [open_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert open_report["id"] == second_report_id + + assert response["total"] == 1 + end + test "renders content correctly", %{conn: conn} do [reporter, target_user] = insert_pair(:user) note = insert(:note, user: target_user, data: %{"content" => "mew 1"}) @@ -467,6 +499,66 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do end end + describe "POST /api/pleroma/admin/reports/assign_account" do + test "assigns account to report", %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/assign_account", %{ + "reports" => [ + %{"assigned_account" => admin.nickname, "id" => report_id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id_with_user_actor(report_id) + assert activity.data["assigned_account"] == admin.id + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} assigned report ##{report_id} (on user @#{activity.user_actor.nickname}) to user #{admin.nickname}" + end + + test "unassigns account from report", %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + CommonAPI.assign_report_to_account(report_id, admin.id) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/assign_account", %{ + "reports" => [ + %{"assigned_account" => nil, "id" => report_id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id_with_user_actor(report_id) + assert activity.data["assigned_account"] == nil + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} unassigned report ##{report_id} (on user @#{activity.user_actor.nickname}) from a user" + end + end + describe "POST /api/pleroma/admin/reports/:id/notes" do setup %{conn: conn, admin: admin} do clear_config([:instance, :admin_privileges], [:reports_manage_reports]) diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs index 1b16aca6a..6e155ef58 100644 --- a/test/pleroma/web/admin_api/views/report_view_test.exs +++ b/test/pleroma/web/admin_api/views/report_view_test.exs @@ -36,6 +36,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do }), AdminAPI.AccountView.render("show.json", %{user: other_user}) ), + assigned_account: nil, statuses: [], notes: [], state: "open", @@ -75,6 +76,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do }), AdminAPI.AccountView.render("show.json", %{user: other_user}) ), + assigned_account: nil, statuses: [StatusView.render("show.json", %{activity: activity})], state: "open", notes: [], diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 52829b734..4eb057712 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1458,6 +1458,29 @@ defmodule Pleroma.Web.CommonAPITest do } } = flag_activity end + + test "assigns report to an account" do + [reporter, target_user] = insert_pair(:user) + %{id: assigned} = insert(:user) + + {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id}) + + {:ok, activity} = CommonAPI.assign_report_to_account(report_id, assigned) + + assert %{data: %{"assigned_account" => ^assigned}} = activity + end + + test "unassigns report from account" do + [reporter, target_user] = insert_pair(:user) + %{id: assigned} = insert(:user) + + {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id}) + + CommonAPI.assign_report_to_account(report_id, assigned) + {:ok, activity} = CommonAPI.assign_report_to_account(report_id, nil) + + refute Map.has_key?(activity.data, "assigned_account") + end end describe "reblog muting" do From 6fac6ff7f1aa61a80295097ef2760b98c00313ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 13:49:34 +0100 Subject: [PATCH 158/317] MastoAPI AccountView: Add mute/block expiry to the relationship object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/user_relationship.ex | 2 +- .../api_spec/schemas/account_relationship.ex | 4 +- .../web/mastodon_api/views/account_view.ex | 76 +++++++++++++------ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 07b6e46f7..cf5025670 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -45,7 +45,7 @@ defmodule Pleroma.UserRelationship do do: exists?(unquote(relationship_type), source, target) # `def get_block_expire_date/2`, `def get_mute_expire_date/2`, - # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`, + # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_expire_date/2`, # `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2` def unquote(:"get_#{relationship_type}_expire_date")(source, target), do: get_expire_date(unquote(relationship_type), source, target) diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex index 68219a099..247a94bb9 100644 --- a/lib/pleroma/web/api_spec/schemas/account_relationship.ex +++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex @@ -26,7 +26,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do requested: %Schema{type: :boolean}, showing_reblogs: %Schema{type: :boolean}, subscribing: %Schema{type: :boolean}, - notifying: %Schema{type: :boolean} + notifying: %Schema{type: :boolean}, + mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true}, + block_expires_at: %Schema{type: :string, format: "date-time", nullable: true} }, example: %{ "blocked_by" => false, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a7d994593..bfcc170d6 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -96,6 +96,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followed_by = FollowingRelationship.following?(target, reading_user) following = FollowingRelationship.following?(reading_user, target) + blocking = + UserRelationship.exists?( + user_relationships, + :block, + reading_user, + target, + &User.blocks_user?(&1, &2) + ) + + muting = + UserRelationship.exists?( + user_relationships, + :mute, + reading_user, + target, + &User.mutes?(&1, &2) + ) + requested = cond do following -> false @@ -116,14 +134,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do id: to_string(target.id), following: following, followed_by: followed_by, - blocking: - UserRelationship.exists?( - user_relationships, - :block, - reading_user, - target, - &User.blocks_user?(&1, &2) - ), + blocking: blocking, blocked_by: UserRelationship.exists?( user_relationships, @@ -132,14 +143,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do reading_user, &User.blocks_user?(&1, &2) ), - muting: - UserRelationship.exists?( - user_relationships, - :mute, - reading_user, - target, - &User.mutes?(&1, &2) - ), + muting: muting, muting_notifications: UserRelationship.exists?( user_relationships, @@ -174,6 +178,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do &User.endorses?(&1, &2) ) } + |> maybe_put_mute_expires_at(target, reading_user, %{mutes: muting}) + |> maybe_put_block_expires_at(target, reading_user, %{blocks: blocking}) end def render("relationships.json", %{user: user, targets: targets} = opts) do @@ -343,8 +349,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) - |> maybe_put_mute_expires_at(user, opts[:for], opts) - |> maybe_put_block_expires_at(user, opts[:for], opts) + |> maybe_put_mute_expires_at(user, opts[:for], opts, relationship) + |> maybe_put_block_expires_at(user, opts[:for], opts, relationship) |> maybe_show_birthday(user, opts[:for]) end @@ -472,25 +478,47 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_email_address(data, _, _), do: data - defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do + defp maybe_put_mute_expires_at(data, target, user, opts, relationship \\ nil) + + defp maybe_put_mute_expires_at(data, _target, _user, %{mutes: true}, %{ + mute_expires_at: mute_expires_at + }) do + Map.put(data, :mute_expires_at, mute_expires_at) + end + + defp maybe_put_mute_expires_at(data, %User{} = target, user, %{mutes: true}, _relationship) do Map.put( data, :mute_expires_at, - UserRelationship.get_mute_expire_date(target, user) + UserRelationship.get_mute_expire_date(user, target) ) end - defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_mute_expires_at(data, _, _, _, _), do: data - defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do + defp maybe_put_block_expires_at(data, target, user, opts, relationship \\ nil) + + defp maybe_put_block_expires_at(data, _target, _user, %{blocks: true}, %{ + block_expires_at: block_expires_at + }) do + Map.put(data, :block_expires_at, block_expires_at) + end + + defp maybe_put_block_expires_at( + data, + %User{} = target, + %User{} = user, + %{blocks: true}, + _relationship + ) do Map.put( data, :block_expires_at, - UserRelationship.get_block_expire_date(target, user) + UserRelationship.get_block_expire_date(user, target) ) end - defp maybe_put_block_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(data, _, _, _, _), do: data defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data From e7a4d5ea66af84aca8f972e65308c231abb4d2a7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 22 Jan 2026 23:01:11 +0100 Subject: [PATCH 159/317] MastoAPI AccountView: Add mute/block expiry to the relationship key --- .../mastodon_api/views/account_view_test.exs | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index bd151cc61..c230cf653 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -439,8 +439,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do following: false, followed_by: false, blocking: false, + block_expires_at: nil, blocked_by: false, muting: false, + mute_expires_at: nil, muting_notifications: false, subscribing: false, notifying: false, @@ -536,6 +538,53 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do test_relationship_rendering(user, other_user, expected) end + test "represent a relationship for the blocking and blocked user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, user, other_user} = User.follow(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60}) + {:ok, _user_relationship} = User.block(other_user, user) + + expected = + Map.merge( + @blank_response, + %{ + following: false, + blocking: true, + block_expires_at: date, + blocked_by: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the muting user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, _user_relationship} = + User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) + + expected = + Map.merge( + @blank_response, + %{ + muting: true, + mute_expires_at: date, + muting_notifications: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + test "represent a relationship for the user blocking a domain" do user = insert(:user) other_user = insert(:user, ap_id: "https://bad.site/users/other_user") @@ -856,12 +905,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) %{ - mute_expires_at: mute_expires_at - } = AccountView.render("show.json", %{user: other_user, for: user, mutes: true}) + pleroma: %{ + relationship: %{ + mute_expires_at: mute_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( mute_expires_at, DateTime.utc_now() |> DateTime.add(24 * 60 * 60) ) in -3..3 end + + test "renders block expiration date" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = + User.block(user, other_user, %{duration: 24 * 60 * 60}) + + %{ + pleroma: %{ + relationship: %{ + block_expires_at: block_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) + + assert DateTime.diff( + block_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end end From c1e33bfadbc82cc7f019927b4b77a07883501cb7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 24 Jan 2026 21:35:27 +0100 Subject: [PATCH 160/317] MastoAPI AccountView AccountController: Add more block/mute expiry tests --- .../controllers/account_controller_test.exs | 103 ++++++++++++++++-- .../mastodon_api/views/account_view_test.exs | 6 +- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 02da781dd..ea98b53a8 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1901,7 +1901,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationships} = User.mute(user, other_user1) {:ok, _user_relationships} = User.mute(user, other_user2) - {:ok, _user_relationships} = User.mute(user, other_user3) + {:ok, _user_relationships} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -1937,6 +1943,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id3}] = result + + result = + conn + |> get("/api/v1/mutes") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "mute_expires_at" => ^date}, + %{"id" => ^id2, "mute_expires_at" => nil}, + %{"id" => ^id1, "mute_expires_at" => nil} + ] = result end test "list of mutes with with_relationships parameter" do @@ -1951,20 +1968,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.mute(user, other_user1) {:ok, _} = User.mute(user, other_user2) - {:ok, _} = User.mute(user, other_user3) + {:ok, _} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => ^date, + "followed_by" => true + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } } ] = conn @@ -1980,7 +2021,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationship} = User.block(user, other_user1) {:ok, _user_relationship} = User.block(user, other_user3) - {:ok, _user_relationship} = User.block(user, other_user2) + {:ok, _user_relationship} = User.block(user, other_user2, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -2045,6 +2092,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id1}] = result + + result = + conn + |> assign(:user, user) + |> get("api/v1/blocks") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "block_expires_at" => nil}, + %{"id" => ^id2, "block_expires_at" => ^date}, + %{"id" => ^id1, "block_expires_at" => nil} + ] = result end test "list of blocks with with_relationships parameter" do @@ -2059,20 +2118,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.block(user, other_user1) {:ok, _} = User.block(user, other_user2) - {:ok, _} = User.block(user, other_user3) + {:ok, _} = User.block(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => ^date, + "followed_by" => false + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } } ] = conn diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c230cf653..6984442cc 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -909,7 +909,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ mute_expires_at: mute_expires_at } - } + }, + mute_expires_at: mute_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( @@ -930,7 +931,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ block_expires_at: block_expires_at } - } + }, + block_expires_at: block_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( From bc0c7fb3105d1c9fb0f54390f37f35b613062976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 13:58:33 +0100 Subject: [PATCH 161/317] Fix tests, relationship should always define `_expires_at` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 ++ test/pleroma/web/mastodon_api/views/account_view_test.exs | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index bfcc170d6..29c63eb60 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -143,6 +143,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do reading_user, &User.blocks_user?(&1, &2) ), + block_expires_at: nil, muting: muting, muting_notifications: UserRelationship.exists?( @@ -152,6 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do target, &User.muted_notifications?(&1, &2) ), + mute_expires_at: nil, subscribing: subscribing, notifying: subscribing, requested: requested, diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 6984442cc..c230cf653 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -909,8 +909,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ mute_expires_at: mute_expires_at } - }, - mute_expires_at: mute_expires_at + } } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( @@ -931,8 +930,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ block_expires_at: block_expires_at } - }, - block_expires_at: block_expires_at + } } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( From 5001fb3a78737273fef7c36a9507ba0e35fd47f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 14:02:55 +0100 Subject: [PATCH 162/317] Update changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/relationship-expires-at.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/relationship-expires-at.change diff --git a/changelog.d/relationship-expires-at.change b/changelog.d/relationship-expires-at.change new file mode 100644 index 000000000..286dba197 --- /dev/null +++ b/changelog.d/relationship-expires-at.change @@ -0,0 +1 @@ +Add mute/block expiry to the relationship object From bd30d461b05cc8b29f5a0818596ea640e2484487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 22:02:22 +0100 Subject: [PATCH 163/317] Add /api/v2/instance profile fields limits info used by Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/instance-profile-fields.add | 1 + .../web/api_spec/operations/instance_operation.ex | 12 ++++++++++++ lib/pleroma/web/mastodon_api/views/instance_view.ex | 9 +++++++++ 3 files changed, 22 insertions(+) create mode 100644 changelog.d/instance-profile-fields.add diff --git a/changelog.d/instance-profile-fields.add b/changelog.d/instance-profile-fields.add new file mode 100644 index 000000000..712bd68d9 --- /dev/null +++ b/changelog.d/instance-profile-fields.add @@ -0,0 +1 @@ +Add /api/v2/instance profile fields limits info used by Mastodon diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index d8b2901d3..6be4ea996 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -342,6 +342,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do max_pinned_statuses: %Schema{ type: :integer, description: "The maximum number of pinned statuses for each account." + }, + max_profile_fields: %Schema{ + type: :integer, + description: "The maximum number of custom profile fields allowed to be set." + }, + profile_field_name_limit: %Schema{ + type: :integer, + description: "The maximum size of a profile field name, in characters." + }, + profile_field_value_limit: %Schema{ + type: :integer, + description: "The maximum size of a profile field value, in characters." } } }, diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 0dc7a5fea..d64ce4fb5 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -303,6 +303,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do defp configuration2 do configuration() |> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0)) + |> put_in([:accounts, :max_profile_fields], Config.get([:instance, :max_account_fields])) + |> put_in( + [:accounts, :profile_field_name_limit], + Config.get([:instance, :account_field_name_length]) + ) + |> put_in( + [:accounts, :profile_field_value_limit], + Config.get([:instance, :account_field_value_length]) + ) |> put_in([:statuses, :characters_reserved_per_url], 0) |> Map.merge(%{ urls: %{ From feda4d0718b06732bee9011bc95485159f5f61f5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 9 Feb 2026 09:01:16 +0400 Subject: [PATCH 164/317] CI: Add basic woodpecker file --- .woodpecker.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 000000000..9cc4f7261 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,29 @@ +when: + - event: + - push + - pull_request + +steps: + test: + image: elixir:1.15-alpine + environment: + MIX_ENV: test + DB_HOST: postgres + DB_PORT: 5432 + commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix deps.get" + - su testuser -c "HOME=/home/testuser mix test" + +services: + postgres: + image: postgres:13-alpine + environment: + POSTGRES_DB: pleroma_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres From 4693dc837b8d8da7276b7d1816eea684b7b6bdfa Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 9 Feb 2026 10:13:29 +0400 Subject: [PATCH 165/317] CI: Only run on PR --- .woodpecker.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 9cc4f7261..4ceb1cab5 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,6 +1,5 @@ when: - event: - - push - pull_request steps: From 2e80c786bb27dc2fceb3269b92db05b04aad9ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 14:37:30 +0100 Subject: [PATCH 166/317] Update comment for prepare_object, rename prepare_outgoing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/update-comment.ignore | 1 + lib/pleroma/user/backup.ex | 2 +- lib/pleroma/web/activity_pub/publisher.ex | 6 +-- .../web/activity_pub/transmogrifier.ex | 20 ++++---- .../web/activity_pub/transmogrifier/api.ex | 2 +- .../web/activity_pub/views/object_view.ex | 2 +- .../web/activity_pub/views/user_view.ex | 2 +- .../activity_pub/mrf/hashtag_policy_test.exs | 4 +- .../article_note_page_validator_test.exs | 2 +- .../update_handling_test.exs | 2 +- .../web/activity_pub/publisher_test.exs | 4 +- .../transmogrifier/answer_handling_test.exs | 2 +- .../transmogrifier/note_handling_test.exs | 4 +- .../web/activity_pub/transmogrifier_test.exs | 48 +++++++++---------- 14 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 changelog.d/update-comment.ignore diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore new file mode 100644 index 000000000..733e813b3 --- /dev/null +++ b/changelog.d/update-comment.ignore @@ -0,0 +1 @@ +Update comment for prepare_object, rename prepare_outgoing diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 3f67cdf0c..35535528b 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -342,7 +342,7 @@ defmodule Pleroma.User.Backup do dir, "outbox", fn a -> - with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do + with {:ok, activity} <- Transmogrifier.prepare_activity(a.data) do {:ok, Map.delete(activity, "@context")} end end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index b6c814aed..e3c4e01a4 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do Determine if an activity can be represented by running it through Transmogrifier. """ def representable?(%Activity{} = activity) do - with {:ok, _data} <- @transmogrifier_impl.prepare_outgoing(activity.data) do + with {:ok, _data} <- @transmogrifier_impl.prepare_activity(activity.data) do true else _e -> @@ -102,14 +102,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do Logger.debug("Federating #{ap_id} to #{inbox}") uri = %{path: path} = URI.parse(inbox) - {:ok, data} = @transmogrifier_impl.prepare_outgoing(activity.data) + {:ok, data} = @transmogrifier_impl.prepare_activity(activity.data) {actor, data} = with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do {actor, data} else {:actor_changed?, true} -> - # If prepare_outgoing changes the actor, re-get it from the db + # If prepare_activity changes the actor, re-get it from the db new_actor = User.get_cached_by_ap_id(data["actor"]) {new_actor, data} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ba6f4030d..6af5ee89d 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -783,7 +783,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_replies(obj_data), do: obj_data - # Prepares the object of an outgoing create activity. + # Prepares and sanitizes the object for federation. def prepare_object(object) do object |> add_hashtags @@ -824,7 +824,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # internal -> Mastodon # """ - def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data) + def prepare_activity(%{"type" => activity_type, "object" => object_id} = data) when activity_type in ["Create", "Listen"] do object = object_id @@ -840,7 +840,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end - def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) + def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) when objtype in Pleroma.Constants.updatable_object_types() do data = data @@ -851,7 +851,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end - def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) + def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) when objtype in Pleroma.Constants.actor_types() do object = object @@ -868,11 +868,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end - def prepare_outgoing(%{"type" => "Update", "object" => %{}} = data) do + def prepare_activity(%{"type" => "Update", "object" => %{}} = data) do raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}" end - def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do + def prepare_activity(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do object = object_id |> Object.normalize(fetch: false) @@ -895,7 +895,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs, # because of course it does. - def prepare_outgoing(%{"type" => "Accept"} = data) do + def prepare_activity(%{"type" => "Accept"} = data) do with follow_activity <- Activity.normalize(data["object"]) do object = %{ "actor" => follow_activity.actor, @@ -913,7 +913,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def prepare_outgoing(%{"type" => "Reject"} = data) do + def prepare_activity(%{"type" => "Reject"} = data) do with follow_activity <- Activity.normalize(data["object"]) do object = %{ "actor" => follow_activity.actor, @@ -931,7 +931,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def prepare_outgoing(%{"type" => "Flag"} = data) do + def prepare_activity(%{"type" => "Flag"} = data) do with {:ok, stripped_activity} <- Utils.strip_report_status_data(data), stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity), stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do @@ -939,7 +939,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def prepare_outgoing(%{"type" => _type} = data) do + def prepare_activity(%{"type" => _type} = data) do data = data |> strip_internal_fields diff --git a/lib/pleroma/web/activity_pub/transmogrifier/api.ex b/lib/pleroma/web/activity_pub/transmogrifier/api.ex index b9f65d17c..f2e416575 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/api.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/api.ex @@ -7,5 +7,5 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.API do Behaviour for the subset of Transmogrifier used by Publisher. """ - @callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()} + @callback prepare_activity(map()) :: {:ok, map()} | {:error, term()} end diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index a672ccc8b..9334b797a 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do end def render("object.json", %{object: %Activity{} = activity}) do - {:ok, ap_data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, ap_data} = Transmogrifier.prepare_activity(activity.data) ap_data end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4362db324..c7fb2a1d7 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -283,7 +283,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do }) do collection = Enum.map(activities, fn activity -> - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) data end) diff --git a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs index 32991c966..e231f88a8 100644 --- a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["object"]["sensitive"] end @@ -94,7 +94,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "#cofe hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) refute modified["object"]["sensitive"] 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 3c7ff0eeb..c32811c5b 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 @@ -67,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest {:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"}) {:ok, %{"object" => external_rep}} = - Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data) + Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data) %{external_rep: external_rep} end diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index c88339d14..347c5b578 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -133,7 +133,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do user = insert(:user) {:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"}) {:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"}) - {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data) + {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data) %{external_rep: external_rep} end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 1f9e0bfe5..7f07ee3a7 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -259,7 +259,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do ) end - test "Publishes with the new actor if prepare_outgoing changes the actor." do + test "Publishes with the new actor if prepare_activity changes the actor." do mock(fn %{method: :post, url: "https://domain.com/users/nick1/inbox", body: body} -> {:ok, %Tesla.Env{status: 200, body: body}} @@ -281,7 +281,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do ) Pleroma.Web.ActivityPub.TransmogrifierMock - |> Mox.expect(:prepare_outgoing, fn data -> + |> Mox.expect(:prepare_activity, fn data -> {:ok, Map.put(data, "actor", replaced_actor.ap_id)} end) diff --git a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs index 39a1598a8..913d143a5 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs @@ -72,7 +72,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do |> Kernel.put_in(["object", "to"], user.ap_id) {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) assert data["object"]["type"] == "Note" end diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 403c98a2d..31b9a699d 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -508,7 +508,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do {:ok, activity} = Transmogrifier.handle_incoming(message) - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, _} = Transmogrifier.prepare_activity(activity.data) end test "successfully reserializes a message with AS2 objects in IR" do @@ -537,7 +537,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do {:ok, activity} = Transmogrifier.handle_incoming(message) - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, _} = Transmogrifier.prepare_activity(activity.data) end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index b54196a3c..f0a64e781 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -433,7 +433,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) - {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(announce_activity.data) assert modified["object"]["content"] == "hey" assert modified["object"]["actor"] == modified["object"]["attributedTo"] @@ -448,7 +448,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do with_mock Pleroma.Notification, get_notified_from_activity: fn _, _ -> [] end do - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) object = modified["object"] @@ -474,7 +474,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["@context"] == Utils.make_json_ld_header()["@context"] @@ -485,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["object"]["actor"] == modified["object"]["attributedTo"] end @@ -501,7 +501,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do "name" => "#2hu" } - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["object"]["tag"] == [expected_tag] end @@ -524,7 +524,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do url: "https://pleroma.social" } == activity.object.data["generator"] - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert length(modified["object"]["tag"]) == 2 @@ -541,7 +541,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do test "it strips internal fields of article" do activity = insert(:article_activity) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert length(modified["object"]["tag"]) == 2 @@ -558,13 +558,13 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["directMessage"] == false {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["directMessage"] == false @@ -574,7 +574,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do visibility: "direct" }) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["directMessage"] == true end @@ -585,7 +585,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert is_nil(modified["bcc"]) end @@ -594,7 +594,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do listen_activity = insert(:listen) # This has an inlined object as in ObjectView - {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(listen_activity.data) assert modified["type"] == "Listen" @@ -610,7 +610,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do object_type = activity.object.data["type"] # This does not have an inlined object - {:ok, modified2} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified2} = Transmogrifier.prepare_activity(activity.data) assert match?( %{ @@ -640,7 +640,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"}) - {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, prepared} = Transmogrifier.prepare_activity(activity.data) assert length(prepared["object"]["tag"]) == 1 @@ -655,7 +655,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"}) {:ok, update} = CommonAPI.update(activity, user, %{status: "mew mew :blank:"}) - {:ok, prepared} = Transmogrifier.prepare_outgoing(update.data) + {:ok, prepared} = Transmogrifier.prepare_activity(update.data) assert %{ "content" => "mew mew :blank:", @@ -689,7 +689,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user_update_changeset: changeset ) - assert {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data) + assert {:ok, prepared} = Transmogrifier.prepare_activity(activity.data) assert prepared["type"] == "Update" assert prepared["@context"] assert prepared["object"]["type"] == user.actor_type @@ -704,7 +704,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker) {:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker) - {:ok, data} = Transmogrifier.prepare_outgoing(undo_activity.data) + {:ok, data} = Transmogrifier.prepare_activity(undo_activity.data) block_ap_id = block_activity.data["id"] assert is_binary(block_ap_id) @@ -738,7 +738,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert is_binary(note_ap_id) {:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈") - {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data) + {:ok, data} = Transmogrifier.prepare_activity(react_activity.data) assert match?( %{ @@ -764,7 +764,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do note_activity = insert(:note_activity) {:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:") - {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data) + {:ok, data} = Transmogrifier.prepare_activity(react_activity.data) assert length(data["tag"]) == 1 @@ -781,7 +781,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"}) {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quoted_status_id: quoted_post.id}) - {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) + {:ok, modified} = Transmogrifier.prepare_activity(quote_post.data) %{data: %{"id" => quote_id}} = Object.normalize(quoted_post) @@ -793,7 +793,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.object.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.object.data) assert [_, _, %{"@language" => "pl"}] = modified["@context"] end @@ -802,7 +802,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert [_, _, %{"@language" => "pl"}] = modified["@context"] end @@ -825,7 +825,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do content: content }) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) expected_data = activity.data @@ -859,7 +859,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do clear_config([:activitypub, :anonymize_reporter], true) clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) expected_data = activity.data From b798f7d6e9f57f80851eb5e1eacf49711e4f5f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 14:51:37 +0100 Subject: [PATCH 167/317] Add issue and pull request templates for Forgejo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .forgejo/issue_template/bug.yaml | 25 +++++++++++++++++++++++++ .forgejo/pull_request_template.md | 10 ++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .forgejo/issue_template/bug.yaml create mode 100644 .forgejo/pull_request_template.md diff --git a/.forgejo/issue_template/bug.yaml b/.forgejo/issue_template/bug.yaml new file mode 100644 index 000000000..1b92658f7 --- /dev/null +++ b/.forgejo/issue_template/bug.yaml @@ -0,0 +1,25 @@ +name: 'Bug report' +about: 'Report a bug in Pleroma' +body: +- type: markdown + attributes: + value: | + ### Precheck + + * For support use https://git.pleroma.social/pleroma/pleroma-support or [community channels](https://git.pleroma.social/pleroma/pleroma#community-channels). + * Please do a quick search to ensure no similar bug has been reported before. If the bug has not been addressed after 2 weeks, it's fine to bump it. + * Try to ensure that the bug is actually related to the Pleroma backend. For example, if a bug happens in Pleroma-FE but not in Mastodon-FE or mobile clients, it's likely that the bug should be filed in [Pleroma-FE](https://git.pleroma.social/pleroma/pleroma-fe/issues/new) repository. +- type: textarea + id: environment + attributes: + label: Environment + value: | + * Installation type (OTP or From Source): + * Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE): + * Elixir version (`elixir -v` for from source installations, N/A for OTP): + * Operating system: + * PostgreSQL version (`psql -V`): +- type: textarea + id: bug-description + attributes: + label: Bug description \ No newline at end of file diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md new file mode 100644 index 000000000..799da5355 --- /dev/null +++ b/.forgejo/pull_request_template.md @@ -0,0 +1,10 @@ +### Checklist +- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `.`. + + `` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name. + + `` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the PR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change. + + In the file, write the changelog entry. For example, if a PR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it. + + If one changelog entry is not enough, you may add more. But that might mean you can split it into two PRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring). \ No newline at end of file From 1c685ea41a771d74a1d4e1769a28911d738591b0 Mon Sep 17 00:00:00 2001 From: feld Date: Thu, 12 Feb 2026 00:34:08 +0000 Subject: [PATCH 168/317] Update README.md fix logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a5eb238f..982a7249a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + ## About From f80c5744b14f14211ab972573ab1cf3bebdb018e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 12 Feb 2026 18:45:36 +0100 Subject: [PATCH 169/317] Normalize Hubzilla alsoKnownAs from string to array --- changelog.d/hubzilla-alsoknownas.fix | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 6 +++- .../hubzilla-actor-alsoknownas-string.json | 1 + .../web/activity_pub/activity_pub_test.exs | 29 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changelog.d/hubzilla-alsoknownas.fix create mode 100644 test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json diff --git a/changelog.d/hubzilla-alsoknownas.fix b/changelog.d/hubzilla-alsoknownas.fix new file mode 100644 index 000000000..2a2969807 --- /dev/null +++ b/changelog.d/hubzilla-alsoknownas.fix @@ -0,0 +1 @@ +Fix fetching Hubzilla Actors with alsoKnownAs as string diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 44e9a22e5..071d634db 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1618,6 +1618,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image() defp normalize_image(_), do: nil + defp normalize_also_known_as(urls) when is_list(urls), do: urls + defp normalize_also_known_as(url) when is_binary(url), do: [url] + defp normalize_also_known_as(nil), do: [] + defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do Map.put(map, "name", description) end @@ -1693,7 +1697,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do featured_address: featured_address, bio: data["summary"] || "", actor_type: actor_type, - also_known_as: Map.get(data, "alsoKnownAs", []), + also_known_as: normalize_also_known_as(data["alsoKnownAs"]), public_key: public_key, inbox: data["inbox"], shared_inbox: shared_inbox, diff --git a/test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json b/test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json new file mode 100644 index 000000000..086db73b1 --- /dev/null +++ b/test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://purl.archive.org/socialweb/webfinger",{"zot":"https://hub.netzgemeinde.eu/apschema#","contextHistory":"https://w3id.org/fep/171b/contextHistory","schema":"http://schema.org#","ostatus":"http://ostatus.org#","diaspora":"https://diasporafoundation.org/ns/","litepub":"http://litepub.social/ns#","toot":"http://joinmastodon.org/ns#","commentPolicy":"zot:commentPolicy","Bookmark":"zot:Bookmark","Category":"zot:Category","Emoji":"toot:Emoji","directMessage":"litepub:directMessage","PropertyValue":"schema:PropertyValue","value":"schema:value","uuid":"schema:identifier","conversation":"ostatus:conversation","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","quoteUri":"http://fedibird.com/ns#quoteUri"}],"type":"Person","manuallyApprovesFollowers":true,"id":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","preferredUsername":"jupiter_rowland","name":"Jupiter Rowland","updated":"0001-01-01T00:00:00Z","icon":{"type":"Image","mediaType":"image/png","updated":"2024-10-15T21:10:02Z","url":"https://hub.netzgemeinde.eu/photo/profile/l/1035?rev=1729019402","height":300,"width":300},"url":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","publicKey":{"id":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","owner":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","signatureAlgorithm":"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmDB9nkcdhjzcfSPQG5q3\nxRVAYaWa+pKC38NhhRMbmd7+P8+8be3HUuX97bwhLdSA3+IRMz9JmX8bqqtKPK8A\nKxWFdUxoZKefwAAVpT+R89hHvi6Ib56Tp5lNlUSTIg3QXfm2gyRc3ehYbI9i6+Wr\nuFgCzdYT9bIeLqhZdFabPP4NORnyPgBQtktXcQUoDOKSaaKuitxFP1dhM9Dco3uX\nLcpQOLdY6yct5J+1Y6+/GUZgtO2pjMtkoEo6Ro0+Wlo7xfvdPnk5moljDVzPFoQE\nhUao5amorPhm1/iEpQXI0eEUW8IXdObFS8gyQJLmS30AjMkaWfwM9HFGmUmn8CSw\nKGBaKDN2C93fvmLOLpaoIRqgVTHBxfv3bN/CtvTRP83/eWvmhnlYo0fmE49tj2ZH\neCHCvRPJ+XM44WntbrUmwJ61+6nO/Io7qe7zmLm+0ew1VD9xTWAd96isW8HEmhcu\nO2iP2GALb0PqE7mgQmV1x/WAYB40+29C03UINHAnZY+nvBW6xd0wAOFiYXqjJF/4\nxEOWPvwcwOtltX+dTerFBC2KzLQQVk/CK2JdKtD2ssiYrvdC/IqPQzVMz3EAtVoP\n8sq/lzffCD3T+zhhnLhgxu7p9JNjRq83jMOOB3DUSd5izO+u6TBn5lasfoa7Eh+X\nQezGvMXhRQR4mnzrSLwZZ1ECAwEAAQ==\n-----END PUBLIC KEY-----\n"},"tag":[{"type":"PropertyValue","name":"Protocol","value":"zot6"},{"type":"PropertyValue","name":"Protocol","value":"activitypub"}],"outbox":"https://hub.netzgemeinde.eu/outbox/jupiter_rowland","webfinger":"jupiter_rowland@hub.netzgemeinde.eu","inbox":"https://hub.netzgemeinde.eu/inbox/jupiter_rowland","followers":"https://hub.netzgemeinde.eu/followers/jupiter_rowland","following":"https://hub.netzgemeinde.eu/following/jupiter_rowland","endpoints":{"sharedInbox":"https://hub.netzgemeinde.eu/inbox"},"discoverable":true,"assertionMethod":[{"id":"https://hub.netzgemeinde.eu/channel/jupiter_rowland#z6Mkw5vE2YnuCwke9VY6Xn1jnaXqLgDCKoJsRE2PjDmAUw9w","type":"Multikey","controller":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","publicKeyMultibase":"z6Mkw5vE2YnuCwke9VY6Xn1jnaXqLgDCKoJsRE2PjDmAUw9w"}],"copiedTo":"https://hub.hubzilla.de/channel/jupiter_rowland","alsoKnownAs":"https://hub.hubzilla.de/channel/jupiter_rowland","image":{"type":"Image","mediaType":"image/jpeg","url":"https://hub.netzgemeinde.eu/photo/5bd87e54-e328-47d3-a8ed-7a33547ad882-7"},"summary":"An avatar roaming the decentralised and federated 3-D virtual worlds based on OpenSimulator, a free and open-source server-side re-implementation of Second Life. Mostly talking about OpenSim, sometimes about other virtual worlds, occasionally about the Fediverse beyond Mastodon. No, the Fediverse is not only Mastodon.

If you're looking for real-life people posting about real-life topics, go look somewhere else. This channel is never about real life.

Even if you see me on Mastodon, I'm not on Mastodon myself. I'm on Hubzilla which is neither a Mastodon instance nor a Mastodon fork. In fact, it's older and much more powerful than Mastodon. And it has always been connected to Mastodon.

I regularly write posts with way more than 500 characters. If that disturbs you, block me now, but don't complain. I'm not on Mastodon, I don't have a character limit here.

I rather give too many content warnings than too few. But I have absolutely no means of blanking out pictures for Mastodon users.

I always describe my images, no matter how long it takes. My posts with image descriptions tend to be my longest. Don't go looking for my image descriptions in the alt-text; they're always in the post text which is always hidden behind a content warning due to being over 500 characters long.

If you follow me, and I "follow" you back, I don't actually follow you and receive your posts. Unless you've got something to say that's interesting to me within the scope of this channel, or I know you from OpenSim, I'll most likely deny you the permission to send me your posts. I only "follow" you back because Hubzilla requires me to do that to allow you to follow me. But I do let you send me your comments and direct messages. If you boost a lot of uninteresting stuff, I'll block you boosts.

My "birthday" isn't my actual birthday but my rezday. My first avatar has been around since that day.

If you happen to know German, maybe my "homepage" is something for you, a blog which, much like this channel, is about OpenSim and generally virtual worlds.

#OpenSim #OpenSimulator #VirtualWorlds #Metaverse #SocialVR  #fedi22","proof":{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-02-12T17:18:56Z","verificationMethod":"https://hub.netzgemeinde.eu/channel/jupiter_rowland#z6Mkw5vE2YnuCwke9VY6Xn1jnaXqLgDCKoJsRE2PjDmAUw9w","proofPurpose":"assertionMethod","proofValue":"z65hrf1X7c2kZgiyAvBnZ1pd16LGRaLYN4zt6kLdPQtYoYpxuVYgL7zZGFEh3h6gcsq8f6FL5XDjXVNivwg9eJL3u"},"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"87c52f614ebea65c8ea41d99487e8f8c8883bd5bc65b2538438df3870c532823","creator":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","created":"2026-02-12T17:18:56Z","signatureValue":"FtiD/BaezB1SFiYj18hfAuyLgYQ0c+Mjc9CXiB+ikd8re/uXV5jr+kMgpYzY+nUB3vpU03XnWVi+lHkJ55vzcTjfbUi7puspF7e88yjIFJcHhs+fc9/+bzBbL2PSQ026bTgH3N1Hj2I7qQ3NcmvKmDF/OovWDks/HPGFiiS9uUezZHRF5be7KfUgEJjESk+5zKGkr7yG4M+f23xaScGxT+/uGK69/RT7QbgSiF2A1IiD2gtwVqxWe9eP//HeoXPCT+O+MdKLWtSZwwzfboO/iwNehMtSAAq+POBW7emHTcM1FEZD6abEQAQwpTq28qAnjuYNCU4TzBdKg1q4akhqnTWEyqpscS9xVUUv/iRiyXvWh9WtcGSCvcMb6avUsxGmdk0nO8hv2zigR/s/ZZJhjXUrID+/bwP2o9Uie6ibdwmt1bCHR8U6z7NCQXEJXw9qZuZYpiTFeuNSftICVRuE/NIYpSOPyqERlRnjU6AX6qEQXHQsOnvhB8BUKcRXpWv0xuzWN3Sr2MZyul8A98nvla9Uwqh9BYn70nRcpRGfiY3m41N4kn5WbPw5PezOB8RSFZjji963oum8tu0/NjzKQ/5UOh3aPO+h+AJsnjau2KOeRHLeChalOOLrt3KFalkyGxLzxt5o/OcRvyM37nnih14lqeTNo60PmR/l/QpiwBs="}} \ No newline at end of file diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 73f53db56..895e01be1 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -499,6 +499,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "https://queef.in/storage/banner.gif" end + test "works with alsoKnownAs as string" do + user_id = "https://hub.netzgemeinde.eu/channel/jupiter_rowland" + + user_data = + "test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json" + |> File.read!() + + user_data_decoded = + user_data + |> Jason.decode!() + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^user_id + } -> + %Tesla.Env{ + status: 200, + body: user_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + + assert is_list(user.also_known_as) + assert user.also_known_as == [user_data_decoded["alsoKnownAs"]] + end + test "it fetches the appropriate tag-restricted posts" do user = insert(:user) From eed4f4bba84bb9b5cedafe707bfd0a5a6a91b134 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 17 Feb 2026 00:35:15 +0100 Subject: [PATCH 170/317] Gopher: Fix crash on (re)boot when ConfigDB is enabled Ranch listener wasn't being properly stopped when the Gopher GenServer received a shutdown message due Restarter rebooting Pleroma to apply ConfigDB configuration (originating from Config.TransferTask.load_and_update_env/2) when ConfigDB is enabled. Handle by trapping exits in the GenServer, which causes the terminate/2 function to be called and the Ranch listener to be stopped from there. 23:22:29.871 [error] GenServer Restarter.Pleroma terminating ** (MatchError) no match of right hand side value: {:error, {{:shutdown, {:failed_to_start_child, Pleroma.Gopher.Server, {{:badmatch, {:error, {:already_started, #PID<0.4801.0>}}}, [ {Pleroma.Gopher.Server, :init, 1, [file: ~c"lib/pleroma/gopher/server.ex", line: 25]}, {:gen_server, :init_it, 2, [file: ~c"gen_server.erl", line: 2276]}, {:gen_server, :init_it, 6, [file: ~c"gen_server.erl", line: 2236]}, {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]} ]}}}, {Pleroma.Application, :start, [:normal, []]}}} (restarter 0.1.0) lib/pleroma.ex:104: Restarter.Pleroma.do_restart/1 (restarter 0.1.0) lib/pleroma.ex:96: Restarter.Pleroma.handle_cast/2 (stdlib 7.2) gen_server.erl:2460: :gen_server.try_handle_cast/3 (stdlib 7.2) gen_server.erl:2418: :gen_server.handle_msg/3 (stdlib 7.2) proc_lib.erl:333: :proc_lib.init_p_do_apply/3 Last message: {:"$gen_cast", {:after_boot, :dev}} State: %{rebooted: false, need_reboot: false, after_boot: false} --- changelog.d/gopher-genserver-crash-on-boot.fix | 1 + lib/pleroma/gopher/server.ex | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog.d/gopher-genserver-crash-on-boot.fix diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix new file mode 100644 index 000000000..3b51662be --- /dev/null +++ b/changelog.d/gopher-genserver-crash-on-boot.fix @@ -0,0 +1 @@ +Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index add3ba925..7b6985efe 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -21,10 +21,13 @@ defmodule Pleroma.Gopher.Server do def init([ip, port]) do Logger.info("Starting gopher server on #{port}") + Process.flag(:trap_exit, true) + + listener = :gopher {:ok, _pid} = :ranch.start_listener( - :gopher, + listener, :ranch_tcp, %{ num_acceptors: 100, @@ -35,7 +38,11 @@ defmodule Pleroma.Gopher.Server do [] ) - {:ok, %{ip: ip, port: port}} + {:ok, %{ip: ip, port: port, listener: listener}} + end + + def terminate(_reason, state) do + :ranch.stop_listener(state.listener) end end From 3d9ac413af0626bc60176b205eb2f23a8c721ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 17 Feb 2026 14:00:21 +0100 Subject: [PATCH 171/317] Move avatar_description and header_description fields to the account object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/avatar-description-mastodon-api.change | 1 + docs/development/API/differences_in_mastoapi_responses.md | 2 -- lib/pleroma/web/api_spec/schemas/account.ex | 6 ++++-- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 ++ test/pleroma/web/mastodon_api/views/account_view_test.exs | 4 ++++ 5 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 changelog.d/avatar-description-mastodon-api.change diff --git a/changelog.d/avatar-description-mastodon-api.change b/changelog.d/avatar-description-mastodon-api.change new file mode 100644 index 000000000..6a454c01e --- /dev/null +++ b/changelog.d/avatar-description-mastodon-api.change @@ -0,0 +1 @@ +Move avatar_description and header_description fields to the account object diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 052b2716b..358d05bf4 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -127,8 +127,6 @@ Has these additional fields under the `pleroma` object: - `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned. - `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user - `favicon`: nullable URL string, Favicon image of the user's instance -- `avatar_description`: string, image description for user avatar, defaults to empty string -- `header_description`: string, image description for user banner, defaults to empty string ### Source diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 7d0b83afe..efdced316 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do acct: %Schema{type: :string}, avatar_static: %Schema{type: :string, format: :uri}, avatar: %Schema{type: :string, format: :uri}, + avatar_description: %Schema{type: :string}, bot: %Schema{type: :boolean}, created_at: %Schema{type: :string, format: "date-time"}, display_name: %Schema{type: :string}, @@ -31,6 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do following_count: %Schema{type: :integer}, header_static: %Schema{type: :string, format: :uri}, header: %Schema{type: :string, format: :uri}, + header_description: %Schema{type: :string}, id: FlakeID, locked: %Schema{type: :boolean}, note: %Schema{type: :string, format: :html}, @@ -111,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do nullable: true, description: "Favicon image of the user's instance" }, - avatar_description: %Schema{type: :string}, - header_description: %Schema{type: :string} + avatar_description: %Schema{type: :string, deprecated: true}, + header_description: %Schema{type: :string, deprecated: true} } }, source: %Schema{ diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 29c63eb60..5386c5a6c 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -300,8 +300,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do note: user.bio, url: user.uri || user.ap_id, avatar: avatar, + avatar_description: avatar_description, avatar_static: avatar_static, header: header, + header_description: header_description, header_static: header_static, emojis: emojis, fields: user.fields, diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c230cf653..f34a801c1 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -54,8 +54,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do note: "valid html. a
b
c
d
f '&<>"", url: user.ap_id, avatar: "http://localhost:4001/images/avi.png", + avatar_description: "", avatar_static: "http://localhost:4001/images/avi.png", header: "http://localhost:4001/images/banner.png", + header_description: "", header_static: "http://localhost:4001/images/banner.png", emojis: [ %{ @@ -326,8 +328,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do note: user.bio, url: user.ap_id, avatar: "http://localhost:4001/images/avi.png", + avatar_description: "", avatar_static: "http://localhost:4001/images/avi.png", header: "http://localhost:4001/images/banner.png", + header_description: "", header_static: "http://localhost:4001/images/banner.png", emojis: [], fields: [], From 699a7e57e82e71b9e706ded28185ee490a903c6d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 17:04:00 +0100 Subject: [PATCH 172/317] Fix LiveDashboard redirect not working when user added a path segment /phoenix/live_dashboard -> /pleroma/live_dashboard would work /phoenix/live_dashboard/ecto_stats -> /pleroma/live_dashboard/ecto_stats would not work and instead reply with FE. --- changelog.d/live-dashboard-redirect.fix | 1 + lib/pleroma/web/fallback/redirect_controller.ex | 13 +++++++++++-- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/fallback_test.exs | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changelog.d/live-dashboard-redirect.fix diff --git a/changelog.d/live-dashboard-redirect.fix b/changelog.d/live-dashboard-redirect.fix new file mode 100644 index 000000000..10588d89e --- /dev/null +++ b/changelog.d/live-dashboard-redirect.fix @@ -0,0 +1 @@ +Fix /phoenix/live_dashboard redirect not working when user added a path segment diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index d75a95fb3..c7f80ad77 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -29,9 +29,18 @@ defmodule Pleroma.Web.Fallback.RedirectController do ) end - def live_dashboard(conn, _params) do + def live_dashboard(conn, %{"path" => path}) do + query_params = conn.query_string + + redirect_path = + if query_params == "" do + "/pleroma/live_dashboard/#{path}" + else + "/pleroma/live_dashboard/#{path}?#{query_params}" + end + conn - |> redirect(to: "/pleroma/live_dashboard") + |> redirect(to: redirect_path) end def redirector(conn, _params, code \\ 200) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 20ac1c67b..3ef60c2d9 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1088,7 +1088,7 @@ defmodule Pleroma.Web.Router do get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, []) get("/api/*path", RedirectController, :api_not_implemented) - get("/phoenix/live_dashboard", RedirectController, :live_dashboard) + get("/phoenix/live_dashboard/*path", RedirectController, :live_dashboard) get("/*path", RedirectController, :redirector_with_preload) options("/*path", RedirectController, :empty) diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs index 6d0ba3d2a..bc3052959 100644 --- a/test/pleroma/web/fallback_test.exs +++ b/test/pleroma/web/fallback_test.exs @@ -79,6 +79,12 @@ defmodule Pleroma.Web.FallbackTest do test "GET /phoenix/live_dashboard -> /pleroma/live_dashboard", %{conn: conn} do assert redirected_to(get(conn, "/phoenix/live_dashboard")) =~ "/pleroma/live_dashboard" + assert redirected_to(get(conn, "/phoenix/live_dashboard/")) =~ "/pleroma/live_dashboard/" + end + + test "GET /phoenix/live_dashboard/* -> /pleroma/live_dashboard/*", %{conn: conn} do + assert redirected_to(get(conn, "/phoenix/live_dashboard/ecto_stats?nav=diagnose")) =~ + "/pleroma/live_dashboard/ecto_stats?nav=diagnose" end test "OPTIONS /*path", %{conn: conn} do From 0b950f62533579960970a80e11d875036444cd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 18 Feb 2026 13:37:10 +0100 Subject: [PATCH 173/317] comment out stuff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .forgejo/pull_request_template.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md index 799da5355..5bb204a14 100644 --- a/.forgejo/pull_request_template.md +++ b/.forgejo/pull_request_template.md @@ -1,10 +1,13 @@ ### Checklist + - [ ] Adding a changelog: In the `changelog.d` directory, create a file named `.`. + From 23a4d68c97f710f753b42180ea9cbda067b192a3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 28 Jan 2026 22:06:10 +0100 Subject: [PATCH 174/317] ReverseProxy: Follow redirects recursively until redirect_limit Test post: https://possum.city/notes/ahqdvbhu3wug0at2 --- .../reverseproxy-recursive-redirect.fix | 1 + lib/pleroma/reverse_proxy/client/hackney.ex | 40 +++++++++------ ...ackney_follow_redirect_regression_test.exs | 49 +++++++++++++++++++ 3 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 changelog.d/reverseproxy-recursive-redirect.fix diff --git a/changelog.d/reverseproxy-recursive-redirect.fix b/changelog.d/reverseproxy-recursive-redirect.fix new file mode 100644 index 000000000..744109fd6 --- /dev/null +++ b/changelog.d/reverseproxy-recursive-redirect.fix @@ -0,0 +1 @@ +ReverseProxy: Recursively follow redirects until redirect_limit is reached diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index 7e1fca80d..c39406106 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -4,6 +4,7 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @behaviour Pleroma.ReverseProxy.Client + @redirect_limit 5 # In-app redirect handler to avoid Hackney redirect bugs: # - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney) @@ -31,26 +32,15 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do if opts[:follow_redirect] != false do {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) + env = %{method: method, headers: headers, body: body, req_opts: req_opts} res = :hackney.request(method, url, headers, body, req_opts) case res do {:ok, code, resp_headers, _client} when code in @redirect_statuses -> - :hackney.request( - method, - absolute_redirect_url(url, resp_headers), - headers, - body, - req_opts - ) + redirect(url, resp_headers, env, @redirect_limit) {:ok, code, resp_headers} when code in @redirect_statuses -> - :hackney.request( - method, - absolute_redirect_url(url, resp_headers), - headers, - body, - req_opts - ) + redirect(url, resp_headers, env, @redirect_limit) _ -> res @@ -71,4 +61,26 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @impl true def close(ref), do: :hackney.close(ref) + + defp redirect(url, resp_headers, env, limit) when limit == 0 do + new_url = absolute_redirect_url(url, resp_headers) + + :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) + end + + defp redirect(url, resp_headers, env, limit) do + new_url = absolute_redirect_url(url, resp_headers) + res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) + + case res do + {:ok, code, new_resp_headers, _client} when code in @redirect_statuses -> + redirect(new_url, new_resp_headers, env, limit - 1) + + {:ok, code, new_resp_headers} when code in @redirect_statuses -> + redirect(new_url, new_resp_headers, env, limit - 1) + + _ -> + res + end + end end diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs index 71fda4479..7ac040324 100644 --- a/test/pleroma/http/hackney_follow_redirect_regression_test.exs +++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs @@ -88,6 +88,49 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do Pleroma.ReverseProxy.Client.Hackney.close(ref) end + test "hackney reverse proxy follows nested redirects via proxy", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/nested_redirect" + + opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: true + ] + + assert {:ok, 200, _headers, ref} = + Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts) + + assert collect_body(ref) == "ok" + Pleroma.ReverseProxy.Client.Hackney.close(ref) + end + + test "hackney reverse proxy stop following redirects after limit", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/infinite_redirect" + + opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: true + ] + + assert {:ok, 302, _headers, ref} = + Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts) + + Pleroma.ReverseProxy.Client.Hackney.close(ref) + end + defp collect_body(ref, acc \\ "") do case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do :done -> acc @@ -148,6 +191,12 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do {:ok, data} <- recv_ssl_headers(ssl_socket), {:ok, path} <- parse_path(data) do case path do + "/infinite_redirect" -> + send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "") + + "/nested_redirect" -> + send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/redirect"}], "") + "/redirect" -> send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "") From cbc2ea331594305c29fb737adcd7e8203af6ab5e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 28 Jan 2026 22:48:35 +0100 Subject: [PATCH 175/317] typo --- test/pleroma/http/hackney_follow_redirect_regression_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs index 7ac040324..4fef26af6 100644 --- a/test/pleroma/http/hackney_follow_redirect_regression_test.exs +++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs @@ -110,7 +110,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do Pleroma.ReverseProxy.Client.Hackney.close(ref) end - test "hackney reverse proxy stop following redirects after limit", %{ + test "hackney reverse proxy stops following redirects after limit is reached", %{ tls_server: tls_server, proxy: proxy } do From 95c8b4732f5a44031221cd73a86578afc4cc1b71 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 11:20:04 +0100 Subject: [PATCH 176/317] ReverseProxy Hackney: Add redirect handling logging --- lib/pleroma/reverse_proxy/client/hackney.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index c39406106..4c81a6225 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -6,6 +6,8 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @behaviour Pleroma.ReverseProxy.Client @redirect_limit 5 + require Logger + # In-app redirect handler to avoid Hackney redirect bugs: # - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney) # - https://github.com/benoitc/hackney/issues/273 (redirects not followed when using HTTP proxy) @@ -65,11 +67,17 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do defp redirect(url, resp_headers, env, limit) when limit == 0 do new_url = absolute_redirect_url(url, resp_headers) + Logger.debug( + "#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect" + ) + :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) end defp redirect(url, resp_headers, env, limit) do new_url = absolute_redirect_url(url, resp_headers) + Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}") + res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) case res do From e32ab8aef278ebcef7ecbc9ae67f417b6862cff9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 14 Oct 2025 22:59:15 +0200 Subject: [PATCH 177/317] DB prune: Check if user follows hashtag with no objects before deletion --- changelog.d/prune-hashtag-follow-3376.fix | 1 + lib/mix/tasks/pleroma/database.ex | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/prune-hashtag-follow-3376.fix diff --git a/changelog.d/prune-hashtag-follow-3376.fix b/changelog.d/prune-hashtag-follow-3376.fix new file mode 100644 index 000000000..cdb4e9a79 --- /dev/null +++ b/changelog.d/prune-hashtag-follow-3376.fix @@ -0,0 +1 @@ +DB prune: Check if user follows hashtag with no objects before deletion diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index e52b5e0a7..396536827 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -226,7 +226,12 @@ defmodule Mix.Tasks.Pleroma.Database do DELETE FROM hashtags AS ht WHERE NOT EXISTS ( SELECT 1 FROM hashtags_objects hto - WHERE ht.id = hto.hashtag_id) + WHERE ht.id = hto.hashtag_id + ) + AND NOT EXISTS ( + SELECT 1 FROM user_follows_hashtag ufh + WHERE ht.id = ufh.hashtag_id + ) """ |> Repo.query() From ef7be0a1e513841beba15ccce6b6211b7369332a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 15 Oct 2025 23:14:52 +0200 Subject: [PATCH 178/317] DB prune: Add test for hashtags --- test/mix/tasks/pleroma/database_test.exs | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 19df17b60..5b567325c 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -8,6 +8,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do alias Pleroma.Activity alias Pleroma.Bookmark + alias Pleroma.Hashtag alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -550,6 +551,39 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do assert length(activities) == 3 end + + test "it prunes hashtags with no objects associated", %{old_insert_date: old_insert_date} do + user = insert(:user) + + {:ok, hashtag_post_activity} = + CommonAPI.post(user, %{status: "morning #cofe", local: true}) + + hashtag_post_object = Object.normalize(hashtag_post_activity) + + {:ok, hashtag_post2_activity} = + CommonAPI.post(user, %{status: "morning #cawfee", local: true}) + + hashtag_post2_object = Object.normalize(hashtag_post2_activity) + + hashtag_post_object + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + hashtag_post2_object + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + # Test whether hashtags with follow relationships are kept + User.follow_hashtag(user, Hashtag.get_by_name("cofe")) + + assert length(Repo.all(Hashtag)) == 2 + assert length(Repo.all(Object)) == 2 + + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + assert length(Repo.all(Hashtag)) == 1 + assert length(Repo.all(Object)) == 0 + assert Repo.one(Hashtag) |> Map.fetch!(:name) == "cofe" + end end describe "running update_users_following_followers_counts" do From c392b21db18e416f2af7e917cb1a7f3a6cfa130d Mon Sep 17 00:00:00 2001 From: mkljczk Date: Fri, 27 Feb 2026 09:22:14 +0000 Subject: [PATCH 179/317] Update docs on scrobbles --- docs/development/API/pleroma_api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 7946ba1f6..b19523bce 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -690,6 +690,7 @@ Audio scrobbling in Pleroma is **deprecated**. * `album`: the album of the media playing [optional] * `artist`: the artist of the media playing [optional] * `length`: the length of the media playing [optional] + * `external_link`: a URL referencing the media playing [optional] * Response: the newly created media metadata entity representing the Listen activity # Emoji Reactions From 938ee4cb01c12a4243d3518d8bba7dc817f34071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 12:04:25 +0100 Subject: [PATCH 180/317] mix.exs: use correct override value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/mix-exs-fix.skip | 0 mix.exs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/mix-exs-fix.skip diff --git a/changelog.d/mix-exs-fix.skip b/changelog.d/mix-exs-fix.skip new file mode 100644 index 000000000..e69de29bb diff --git a/mix.exs b/mix.exs index a4415fddc..0532bffd7 100644 --- a/mix.exs +++ b/mix.exs @@ -154,7 +154,7 @@ defmodule Pleroma.Mixfile do {:gun, "~> 2.2"}, {:finch, "~> 0.15"}, {:jason, "~> 1.2"}, - {:mogrify, "~> 0.9.0", override: "true"}, + {:mogrify, "~> 0.9.0", override: true}, {:ex_aws, "~> 2.1.6"}, {:ex_aws_s3, "~> 2.0"}, {:sweet_xml, "~> 0.7.2"}, From a9b5a28c26324bcb02ed20c39ace80d33a636244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 16:24:10 +0100 Subject: [PATCH 181/317] Do not use Enum.map for side-effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/map-side-effects.skip | 0 lib/mix/tasks/pleroma/openapi_spec.ex | 2 +- lib/pleroma/activity/html.ex | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog.d/map-side-effects.skip diff --git a/changelog.d/map-side-effects.skip b/changelog.d/map-side-effects.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mix/tasks/pleroma/openapi_spec.ex b/lib/mix/tasks/pleroma/openapi_spec.ex index 1ea468476..852e1e9af 100644 --- a/lib/mix/tasks/pleroma/openapi_spec.ex +++ b/lib/mix/tasks/pleroma/openapi_spec.ex @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do else {_, errors} -> IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"])) - Enum.map(errors, &IO.puts/1) + Enum.each(errors, &IO.puts/1) raise "Spec check failed" end diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex index ba284b4d5..c83889c87 100644 --- a/lib/pleroma/activity/html.ex +++ b/lib/pleroma/activity/html.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Activity.HTML do def invalidate_cache_for(activity_id) do keys = get_cache_keys_for(activity_id) - Enum.map(keys, &@cachex.del(:scrubber_cache, &1)) + Enum.each(keys, &@cachex.del(:scrubber_cache, &1)) @cachex.del(:scrubber_management_cache, activity_id) end From 120719f28c9aba33a57d07c5fd28ddcb1d8a3db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 19:53:25 +0100 Subject: [PATCH 182/317] Don't use the confusing TwitterAPI namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/twitter-api.skip | 0 ...operation.ex => pleroma_util_operation.ex} | 74 ++----- .../remote_interaction_operation.ex | 61 ++++++ .../controllers/account_controller.ex | 6 +- .../controllers/auth_controller.ex | 4 +- .../password_controller.ex | 6 +- .../views => o_auth}/password_view.ex | 2 +- .../token_controller.ex} | 4 +- .../views => o_auth}/token_view.ex | 4 +- .../controllers/password_controller.ex | 52 +++++ .../controllers/token_controller.ex | 59 +++++ .../controllers/util_controller.ex | 135 +----------- .../web/pleroma_api/views/util_view.ex | 13 ++ lib/pleroma/web/preload/providers/instance.ex | 2 +- .../twitter_api.ex => registration.ex} | 2 +- .../remote_interaction_controller.ex} | 131 +++++++++++- .../remote_interaction_view.ex} | 4 +- lib/pleroma/web/router.ex | 42 ++-- .../password/invalid_token.html.eex | 0 .../password/reset.html.eex | 0 .../password/reset_failed.html.eex | 0 .../password/reset_success.html.eex | 0 .../remote_interaction}/follow.html.eex | 2 +- .../remote_interaction}/follow_login.html.eex | 2 +- .../remote_interaction}/follow_mfa.html.eex | 2 +- .../remote_interaction}/followed.html.eex | 0 .../status_interact.html.eex | 2 +- .../remote_interaction}/subscribe.html.eex | 2 +- .../web/twitter_api/views/util_view.ex | 31 --- .../password_controller_test.exs | 2 +- .../token_controller_test.exs} | 2 +- .../controllers}/util_controller_test.exs | 149 +------------ ...ter_api_test.exs => registration_test.exs} | 42 ++-- .../remote_interaction_controller_test.exs} | 202 +++++++++++++++--- 34 files changed, 578 insertions(+), 461 deletions(-) create mode 100644 changelog.d/twitter-api.skip rename lib/pleroma/web/api_spec/operations/{twitter_util_operation.ex => pleroma_util_operation.ex} (84%) create mode 100644 lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex rename lib/pleroma/web/{twitter_api/controllers => o_auth}/password_controller.ex (90%) rename lib/pleroma/web/{twitter_api/views => o_auth}/password_view.ex (83%) rename lib/pleroma/web/{twitter_api/controller.ex => o_auth/token_controller.ex} (94%) rename lib/pleroma/web/{twitter_api/views => o_auth}/token_view.ex (81%) create mode 100644 lib/pleroma/web/pleroma_api/controllers/password_controller.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/token_controller.ex rename lib/pleroma/web/{twitter_api => pleroma_api}/controllers/util_controller.ex (65%) create mode 100644 lib/pleroma/web/pleroma_api/views/util_view.ex rename lib/pleroma/web/{twitter_api/twitter_api.ex => registration.ex} (98%) rename lib/pleroma/web/{twitter_api/controllers/remote_follow_controller.ex => remote_interaction/remote_interaction_controller.ex} (57%) rename lib/pleroma/web/{twitter_api/views/remote_follow_view.ex => remote_interaction/remote_interaction_view.ex} (75%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/invalid_token.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/reset.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/reset_failed.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/reset_success.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/follow.html.eex (83%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/follow_login.html.eex (88%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/follow_mfa.html.eex (86%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/followed.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api/util => remote_interaction/remote_interaction}/status_interact.html.eex (88%) rename lib/pleroma/web/templates/{twitter_api/util => remote_interaction/remote_interaction}/subscribe.html.eex (85%) delete mode 100644 lib/pleroma/web/twitter_api/views/util_view.ex rename test/pleroma/web/{twitter_api => o_auth}/password_controller_test.exs (99%) rename test/pleroma/web/{twitter_api/controller_test.exs => o_auth/token_controller_test.exs} (97%) rename test/pleroma/web/{twitter_api => pleroma_api/controllers}/util_controller_test.exs (85%) rename test/pleroma/web/{twitter_api/twitter_api_test.exs => registration_test.exs} (90%) rename test/pleroma/web/{twitter_api/remote_follow_controller_test.exs => remote_interaction/remote_interaction_controller_test.exs} (68%) diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex similarity index 84% rename from lib/pleroma/web/api_spec/operations/twitter_util_operation.ex rename to lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex index 724d873c0..4bb4f112c 100644 --- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do +defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.ApiError @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do %Operation{ tags: ["Custom emojis"], summary: "List all custom emojis", - operationId: "UtilController.emoji", + operationId: "PleromaAPI.UtilController.emoji", parameters: [], responses: %{ 200 => @@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do %Operation{ tags: ["Others"], summary: "Dump frontend configurations", - operationId: "UtilController.frontend_configurations", + operationId: "PleromaAPI.UtilController.frontend_configurations", parameters: [], responses: %{ 200 => @@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Change account password", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.change_password", + operationId: "PleromaAPI.UtilController.change_password", requestBody: request_body("Parameters", change_password_request(), required: true), responses: %{ 200 => @@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Change account email", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.change_email", + operationId: "PleromaAPI.UtilController.change_email", requestBody: request_body("Parameters", change_email_request(), required: true), responses: %{ 200 => @@ -141,7 +141,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Settings"], summary: "Update Notification Settings", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.update_notification_settings", + operationId: "PleromaAPI.UtilController.update_notification_settings", parameters: [ Operation.parameter( :block_from_strangers, @@ -173,7 +173,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Disable Account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.disable_account", + operationId: "PleromaAPI.UtilController.disable_account", parameters: [ Operation.parameter(:password, :query, :string, "Password") ], @@ -193,7 +193,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Delete Account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.delete_account", + operationId: "PleromaAPI.UtilController.delete_account", parameters: [ Operation.parameter(:password, :query, :string, "Password") ], @@ -212,7 +212,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do def captcha_operation do %Operation{ summary: "Get a captcha", - operationId: "UtilController.captcha", + operationId: "PleromaAPI.UtilController.captcha", tags: ["Others"], parameters: [], responses: %{ @@ -226,7 +226,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Move account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.move_account", + operationId: "PleromaAPI.UtilController.move_account", requestBody: request_body("Parameters", move_account_request(), required: true), responses: %{ 200 => @@ -262,7 +262,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "List account aliases", security: [%{"oAuth" => ["read:accounts"]}], - operationId: "UtilController.list_aliases", + operationId: "PleromaAPI.UtilController.list_aliases", responses: %{ 200 => Operation.response("Success", "application/json", %Schema{ @@ -286,7 +286,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Add an alias to this account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.add_alias", + operationId: "PleromaAPI.UtilController.add_alias", requestBody: request_body("Parameters", add_alias_request(), required: true), responses: %{ 200 => @@ -326,7 +326,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Delete an alias from this account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.delete_alias", + operationId: "PleromaAPI.UtilController.delete_alias", requestBody: request_body("Parameters", delete_alias_request(), required: true), responses: %{ 200 => @@ -366,7 +366,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Others"], summary: "Quick status check on the instance", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.healthcheck", + operationId: "PleromaAPI.UtilController.healthcheck", parameters: [], responses: %{ 200 => Operation.response("Healthy", "application/json", %Schema{type: :object}), @@ -376,52 +376,6 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do } end - def remote_subscribe_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Remote Subscribe", - operationId: "UtilController.remote_subscribe", - parameters: [], - responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})} - } - end - - def remote_interaction_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Remote interaction", - operationId: "UtilController.remote_interaction", - requestBody: request_body("Parameters", remote_interaction_request(), required: true), - responses: %{ - 200 => - Operation.response("Remote interaction URL", "application/json", %Schema{type: :object}) - } - } - end - - defp remote_interaction_request do - %Schema{ - title: "RemoteInteractionRequest", - description: "POST body for remote interaction", - type: :object, - required: [:ap_id, :profile], - properties: %{ - ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"}, - profile: %Schema{type: :string, description: "Remote profile webfinger"} - } - } - end - - def show_subscribe_form_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Show remote subscribe form", - operationId: "UtilController.show_subscribe_form", - parameters: [], - responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})} - } - end - defp delete_account_request do %Schema{ title: "AccountDeleteRequest", diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex new file mode 100644 index 000000000..a490bd491 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def remote_subscribe_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Remote Subscribe", + operationId: "RemoteInteractionController.remote_subscribe", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end + + def remote_interaction_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Remote interaction", + operationId: "RemoteInteractionController.remote_interaction", + requestBody: request_body("Parameters", remote_interaction_request(), required: true), + responses: %{ + 200 => + Operation.response("Remote interaction URL", "application/json", %Schema{type: :object}) + } + } + end + + defp remote_interaction_request do + %Schema{ + title: "RemoteInteractionRequest", + description: "POST body for remote interaction", + type: :object, + required: [:ap_id, :profile], + properties: %{ + ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"}, + profile: %Schema{type: :string, description: "Remote profile webfinger"} + } + } + end + + def show_subscribe_form_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Show remote subscribe form", + operationId: "RemoteInteractionController.show_subscribe_form", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 6dc731ed4..6d5851029 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration alias Pleroma.Web.Utils.Params plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) @@ -111,8 +111,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do _params ) do with :ok <- validate_email_param(params), - :ok <- TwitterAPI.validate_captcha(app, params), - {:ok, user} <- TwitterAPI.register_user(params), + :ok <- Registration.validate_captcha(app, params), + {:ok, user} <- Registration.register_user(params), {_, {:ok, token}} <- {:login, OAuthController.login(user, app, app.scopes)} do OAuthController.after_token_exchange(conn, %{user: user, token: token}) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index fbb54a171..653b5fc29 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration action_fallback(Pleroma.Web.MastodonAPI.FallbackController) @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do def password_reset(conn, params) do nickname_or_email = params["email"] || params["nickname"] - TwitterAPI.password_reset(nickname_or_email) + Registration.password_reset(nickname_or_email) json_response(conn, :no_content, "") end diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/o_auth/password_controller.ex similarity index 90% rename from lib/pleroma/web/twitter_api/controllers/password_controller.ex rename to lib/pleroma/web/o_auth/password_controller.ex index e5482de9d..b209e7564 100644 --- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex +++ b/lib/pleroma/web/o_auth/password_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.PasswordController do +defmodule Pleroma.Web.OAuth.PasswordController do @moduledoc """ The module contains functions for password reset. """ @@ -16,7 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do alias Pleroma.PasswordResetToken alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request) @@ -24,7 +24,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do def request(conn, params) do nickname_or_email = params["email"] || params["nickname"] - TwitterAPI.password_reset(nickname_or_email) + Registration.password_reset(nickname_or_email) json_response(conn, :no_content, "") end diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/o_auth/password_view.ex similarity index 83% rename from lib/pleroma/web/twitter_api/views/password_view.ex rename to lib/pleroma/web/o_auth/password_view.ex index 55790941f..0b85c76e8 100644 --- a/lib/pleroma/web/twitter_api/views/password_view.ex +++ b/lib/pleroma/web/o_auth/password_view.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.PasswordView do +defmodule Pleroma.Web.OAuth.PasswordView do use Pleroma.Web, :view import Phoenix.HTML.Form alias Pleroma.Web.Gettext diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/o_auth/token_controller.ex similarity index 94% rename from lib/pleroma/web/twitter_api/controller.ex rename to lib/pleroma/web/o_auth/token_controller.ex index 6db3d6067..37d08eea3 100644 --- a/lib/pleroma/web/twitter_api/controller.ex +++ b/lib/pleroma/web/o_auth/token_controller.ex @@ -2,13 +2,13 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.Controller do +defmodule Pleroma.Web.OAuth.TokenController do use Pleroma.Web, :controller alias Pleroma.User alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.TwitterAPI.TokenView + alias Pleroma.Web.OAuth.TokenView require Logger diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/o_auth/token_view.ex similarity index 81% rename from lib/pleroma/web/twitter_api/views/token_view.ex rename to lib/pleroma/web/o_auth/token_view.ex index 36776ce3b..f9894399a 100644 --- a/lib/pleroma/web/twitter_api/views/token_view.ex +++ b/lib/pleroma/web/o_auth/token_view.ex @@ -2,12 +2,12 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.TokenView do +defmodule Pleroma.Web.OAuth.TokenView do use Pleroma.Web, :view def render("index.json", %{tokens: tokens}) do tokens - |> render_many(Pleroma.Web.TwitterAPI.TokenView, "show.json") + |> render_many(Pleroma.Web.OAuth.TokenView, "show.json") |> Enum.filter(&Enum.any?/1) end diff --git a/lib/pleroma/web/pleroma_api/controllers/password_controller.ex b/lib/pleroma/web/pleroma_api/controllers/password_controller.ex new file mode 100644 index 000000000..444477245 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/password_controller.ex @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.PasswordController do + @moduledoc """ + The module contains functions for password reset. + """ + + use Pleroma.Web, :controller + + require Logger + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.PasswordResetToken + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Registration + + plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request) + + @doc "POST /auth/password" + def request(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + Registration.password_reset(nickname_or_email) + + json_response(conn, :no_content, "") + end + + def reset(conn, %{"token" => token}) do + with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), + false <- PasswordResetToken.expired?(token), + %User{} = user <- User.get_cached_by_id(token.user_id) do + render(conn, "reset.html", %{ + token: token, + user: user + }) + else + _e -> render(conn, "invalid_token.html") + end + end + + def do_reset(conn, %{"data" => data}) do + with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do + render(conn, "reset_success.html") + else + _e -> render(conn, "reset_failed.html") + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex new file mode 100644 index 000000000..581dd569a --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.TokenController do + use Pleroma.Web, :controller + + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.PleromaAPI.TokenView + + require Logger + + plug(:skip_auth when action == :confirm_email) + plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) + + action_fallback(:errors) + + def confirm_email(conn, %{"user_id" => uid, "token" => token}) do + with %User{} = user <- User.get_cached_by_id(uid), + true <- user.local and !user.is_confirmed and user.confirmation_token == token, + {:ok, _} <- User.confirm(user) do + redirect(conn, to: "/") + end + end + + def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do + with oauth_tokens <- Token.get_user_tokens(user) do + conn + |> put_view(TokenView) + |> render("index.json", %{tokens: oauth_tokens}) + end + end + + def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + Token.delete_user_token(user, id) + + json_reply(conn, 201, "") + end + + defp errors(conn, {:param_cast, _}) do + conn + |> put_status(400) + |> json("Invalid parameters") + end + + defp errors(conn, _) do + conn + |> put_status(500) + |> json("Something went wrong") + end + + defp json_reply(conn, status, json) do + conn + |> put_resp_content_type("application/json") + |> send_resp(status, json) + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex similarity index 65% rename from lib/pleroma/web/twitter_api/controllers/util_controller.ex rename to lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 1c072f98a..9528d5564 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.UtilController do +defmodule Pleroma.Web.PleromaAPI.UtilController do use Pleroma.Web, :controller require Logger @@ -17,19 +17,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.WebFinger - plug( - Pleroma.Web.ApiSpec.CastAndValidate, - [replace_params: false] - when action != :remote_subscribe and action != :show_subscribe_form - ) - - plug( - Pleroma.Web.Plugs.FederatingPlug - when action == :remote_subscribe - when action == :show_subscribe_form - ) + plug(Pleroma.Web.ApiSpec.CastAndValidate, [replace_params: false]) plug( OAuthScopesPlug, @@ -54,125 +43,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do ] ) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation - - def show_subscribe_form(conn, %{"nickname" => nick}) do - with %User{} = user <- User.get_cached_by_nickname(nick), - avatar = User.avatar_url(user) do - conn - |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false}) - else - _e -> - render(conn, "subscribe.html", %{ - nickname: nick, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "remote follow error message - user not found", - "Could not find user" - ) - }) - end - end - - def show_subscribe_form(conn, %{"status_id" => id}) do - with %Activity{} = activity <- Activity.get_by_id(id), - {:ok, ap_id} <- get_ap_id(activity), - %User{} = user <- User.get_cached_by_ap_id(activity.actor), - avatar = User.avatar_url(user) do - conn - |> render("status_interact.html", %{ - status_link: ap_id, - status_id: id, - nickname: user.nickname, - avatar: avatar, - error: false - }) - else - _e -> - render(conn, "status_interact.html", %{ - status_id: id, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "status interact error message - status not found", - "Could not find status" - ) - }) - end - end - - def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do - show_subscribe_form(conn, %{"nickname" => nick}) - end - - def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do - show_subscribe_form(conn, %{"status_id" => id}) - end - - def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do - with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), - %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do - conn - |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) - else - _e -> - render(conn, "subscribe.html", %{ - nickname: nick, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "remote follow error message - unknown error", - "Something went wrong." - ) - }) - end - end - - def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do - with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), - %Activity{} = activity <- Activity.get_by_id(id), - {:ok, ap_id} <- get_ap_id(activity) do - conn - |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) - else - _e -> - render(conn, "status_interact.html", %{ - status_id: id, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "status interact error message - unknown error", - "Something went wrong." - ) - }) - end - end - - def remote_interaction( - %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn, - _params - ) do - with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do - conn - |> json(%{url: String.replace(template, "{uri}", ap_id)}) - else - _e -> json(conn, %{error: "Couldn't find user"}) - end - end - - defp get_ap_id(activity) do - object = Pleroma.Object.normalize(activity, fetch: false) - - case object do - %{data: %{"id" => ap_id}} -> {:ok, ap_id} - _ -> {:no_ap_id, nil} - end - end + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaUtilOperation def frontend_configurations(conn, _params) do render(conn, "frontend_configurations.json") diff --git a/lib/pleroma/web/pleroma_api/views/util_view.ex b/lib/pleroma/web/pleroma_api/views/util_view.ex new file mode 100644 index 000000000..b5e07c006 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/util_view.ex @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.UtilView do + use Pleroma.Web, :view + alias Pleroma.Config + + def render("frontend_configurations.json", _) do + Config.get(:frontend_configurations, %{}) + |> Enum.into(%{}) + end +end diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex index 6183f7b70..d5417af30 100644 --- a/lib/pleroma/web/preload/providers/instance.ex +++ b/lib/pleroma/web/preload/providers/instance.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Plugs.InstanceStatic alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.TwitterAPI.UtilView + alias Pleroma.Web.PleromaAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/registration.ex similarity index 98% rename from lib/pleroma/web/twitter_api/twitter_api.ex rename to lib/pleroma/web/registration.ex index ef2eb75f4..df71300ce 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/registration.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.TwitterAPI do +defmodule Pleroma.Web.Registration do import Pleroma.Web.Gettext alias Pleroma.Emails.Mailer diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex similarity index 57% rename from lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex rename to lib/pleroma/web/remote_interaction/remote_interaction_controller.ex index 38ebc8c5d..8592fc990 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do +defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionController do use Pleroma.Web, :controller require Logger @@ -14,9 +14,16 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.Auth.WrapperAuthenticator alias Pleroma.Web.CommonAPI + alias Pleroma.Web.WebFinger @status_types ["Article", "Event", "Note", "Video", "Page", "Question"] + plug( + Pleroma.Web.ApiSpec.CastAndValidate, + [replace_params: false] + when action == :remote_interaction + ) + plug(Pleroma.Web.Plugs.FederatingPlug) # Note: follower can submit the form (with password auth) not being signed in (having no token) @@ -26,6 +33,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do when action in [:do_follow] ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.RemoteInteractionOperation + # GET /ostatus_subscribe # def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do @@ -125,7 +134,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do # def authorize_interaction(conn, %{"uri" => uri}) do conn - |> redirect(to: Routes.remote_follow_path(conn, :follow, %{acct: uri})) + |> redirect(to: Routes.remote_interaction_path(conn, :follow, %{acct: uri})) end defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do @@ -162,4 +171,122 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do Logger.debug("Remote follow failed with error #{inspect(error)}") render(conn, "followed.html", %{error: "Something went wrong."}) end + + def show_subscribe_form(conn, %{"nickname" => nick}) do + with %User{} = user <- User.get_cached_by_nickname(nick), + avatar = User.avatar_url(user) do + conn + |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false}) + else + _e -> + render(conn, "subscribe.html", %{ + nickname: nick, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "remote follow error message - user not found", + "Could not find user" + ) + }) + end + end + + def show_subscribe_form(conn, %{"status_id" => id}) do + with %Activity{} = activity <- Activity.get_by_id(id), + {:ok, ap_id} <- get_ap_id(activity), + %User{} = user <- User.get_cached_by_ap_id(activity.actor), + avatar = User.avatar_url(user) do + conn + |> render("status_interact.html", %{ + status_link: ap_id, + status_id: id, + nickname: user.nickname, + avatar: avatar, + error: false + }) + else + _e -> + render(conn, "status_interact.html", %{ + status_id: id, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "status interact error message - status not found", + "Could not find status" + ) + }) + end + end + + def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do + show_subscribe_form(conn, %{"nickname" => nick}) + end + + def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do + show_subscribe_form(conn, %{"status_id" => id}) + end + + def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do + with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), + %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do + conn + |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) + else + _e -> + render(conn, "subscribe.html", %{ + nickname: nick, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "remote follow error message - unknown error", + "Something went wrong." + ) + }) + end + end + + def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do + with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), + %Activity{} = activity <- Activity.get_by_id(id), + {:ok, ap_id} <- get_ap_id(activity) do + conn + |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) + else + _e -> + render(conn, "status_interact.html", %{ + status_id: id, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "status interact error message - unknown error", + "Something went wrong." + ) + }) + end + end + + def remote_interaction( + %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn, + _params + ) do + with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do + conn + |> json(%{url: String.replace(template, "{uri}", ap_id)}) + else + _e -> json(conn, %{error: "Couldn't find user"}) + end + end + + defp get_ap_id(activity) do + object = Pleroma.Object.normalize(activity, fetch: false) + + case object do + %{data: %{"id" => ap_id}} -> {:ok, ap_id} + _ -> {:no_ap_id, nil} + end + end end diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/remote_interaction/remote_interaction_view.ex similarity index 75% rename from lib/pleroma/web/twitter_api/views/remote_follow_view.ex rename to lib/pleroma/web/remote_interaction/remote_interaction_view.ex index 8902261b0..6e7f40749 100644 --- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex +++ b/lib/pleroma/web/remote_interaction/remote_interaction_view.ex @@ -2,9 +2,11 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do +defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionView do use Pleroma.Web, :view + import Phoenix.HTML import Phoenix.HTML.Form + import Phoenix.HTML.Link alias Pleroma.Web.Gettext def avatar_url(user) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 20ac1c67b..22e82568a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -226,23 +226,27 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.StaticFEPlug) end - scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do + scope "/api/v1/pleroma", Pleroma.Web.OAuth do pipe_through(:pleroma_api) get("/password_reset/:token", PasswordController, :reset, as: :reset_password) post("/password_reset", PasswordController, :do_reset, as: :reset_password) - get("/emoji", UtilController, :emoji) - get("/captcha", UtilController, :captcha) - get("/healthcheck", UtilController, :healthcheck) - post("/remote_interaction", UtilController, :remote_interaction) end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do - pipe_through(:pleroma_api) + get("/emoji", UtilController, :emoji) + get("/captcha", UtilController, :captcha) + get("/healthcheck", UtilController, :healthcheck) get("/federation_status", InstancesController, :show) end + scope "/api/v1/pleroma", Pleroma.Web.RemoteInteraction do + pipe_through(:pleroma_api) + + post("/remote_interaction", RemoteInteractionController, :remote_interaction) + end + scope "/api/v1/pleroma", Pleroma.Web do pipe_through(:pleroma_api) post("/uploader_callback/:upload_path", UploaderController, :callback) @@ -484,18 +488,18 @@ defmodule Pleroma.Web.Router do end end - scope "/", Pleroma.Web.TwitterAPI do + scope "/", Pleroma.Web.RemoteInteraction do pipe_through(:pleroma_html) - post("/main/ostatus", UtilController, :remote_subscribe) - get("/main/ostatus", UtilController, :show_subscribe_form) - get("/ostatus_subscribe", RemoteFollowController, :follow) - post("/ostatus_subscribe", RemoteFollowController, :do_follow) + post("/main/ostatus", RemoteInteractionController, :remote_subscribe) + get("/main/ostatus", RemoteInteractionController, :show_subscribe_form) + get("/ostatus_subscribe", RemoteInteractionController, :follow) + post("/ostatus_subscribe", RemoteInteractionController, :do_follow) - get("/authorize_interaction", RemoteFollowController, :authorize_interaction) + get("/authorize_interaction", RemoteInteractionController, :authorize_interaction) end - scope "/api/pleroma", Pleroma.Web.TwitterAPI do + scope "/api/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:authenticated_api) post("/change_email", UtilController, :change_email) @@ -853,7 +857,7 @@ defmodule Pleroma.Web.Router do scope "/api", Pleroma.Web do pipe_through(:config) - get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) + get("/pleroma/frontend_configurations", PleromaAPI.UtilController, :frontend_configurations) end scope "/api", Pleroma.Web do @@ -861,7 +865,7 @@ defmodule Pleroma.Web.Router do get( "/account/confirm_email/:user_id/:token", - TwitterAPI.Controller, + OAuth.TokenController, :confirm_email, as: :confirm_email ) @@ -873,11 +877,11 @@ defmodule Pleroma.Web.Router do get("/openapi", OpenApiSpex.Plug.RenderSpec, []) end - scope "/api", Pleroma.Web, as: :authenticated_twitter_api do + scope "/api", Pleroma.Web, as: :authenticated_pleroma_api do pipe_through(:authenticated_api) - get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens) - delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token) + get("/oauth_tokens", OAuth.TokenController, :oauth_tokens) + delete("/oauth_tokens/:id", OAuth.TokenController, :revoke_token) end scope "/", Pleroma.Web do @@ -1026,7 +1030,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do pipe_through(:pleroma_html) - post("/auth/password", TwitterAPI.PasswordController, :request) + post("/auth/password", OAuth.PasswordController, :request) end scope "/proxy/", Pleroma.Web do diff --git a/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex b/lib/pleroma/web/templates/o_auth/password/invalid_token.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex rename to lib/pleroma/web/templates/o_auth/password/invalid_token.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex b/lib/pleroma/web/templates/o_auth/password/reset.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/reset.html.eex rename to lib/pleroma/web/templates/o_auth/password/reset.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex b/lib/pleroma/web/templates/o_auth/password/reset_failed.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex rename to lib/pleroma/web/templates/o_auth/password/reset_failed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex b/lib/pleroma/web/templates/o_auth/password/reset_success.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex rename to lib/pleroma/web/templates/o_auth/password/reset_success.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex similarity index 83% rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex index e2d251fac..00cc7d383 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex @@ -4,7 +4,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %>

<%= @followee.nickname %>

- <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %> + <%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "user"], fn f -> %> <%= hidden_input f, :id, value: @followee.id %> <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %> <% end %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex similarity index 88% rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex index 26340a906..e5fe720e8 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex @@ -4,7 +4,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %>

<%= @followee.nickname %>

-<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %> +<%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "authorization"], fn f -> %> <%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex similarity index 86% rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex index 638212c1e..e5f26f104 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex @@ -4,7 +4,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %>

<%= @followee.nickname %>

-<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %> +<%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "mfa"], fn f -> %> <%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
<%= hidden_input f, :id, value: @followee.id %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/followed.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/followed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex similarity index 88% rename from lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex index d77174967..b3d590186 100644 --- a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex @@ -2,7 +2,7 @@

<%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %>

<% else %>

<%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %>

- <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %> + <%= form_for @conn, Routes.remote_interaction_path(@conn, :remote_subscribe), [as: "status"], fn f -> %> <%= hidden_input f, :status_id, value: @status_id %> <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> <%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %> diff --git a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex similarity index 85% rename from lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex index 848660f26..7e1dd723c 100644 --- a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex @@ -2,7 +2,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %>

<% else %>

<%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %>

- <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %> + <%= form_for @conn, Routes.remote_interaction_path(@conn, :remote_subscribe), [as: "user"], fn f -> %> <%= hidden_input f, :nickname, value: @nickname %> <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %> diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex deleted file mode 100644 index 31b7c0c0c..000000000 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.UtilView do - use Pleroma.Web, :view - import Phoenix.HTML - import Phoenix.HTML.Form - import Phoenix.HTML.Link - alias Pleroma.Config - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Gettext - - def status_net_config(instance) do - """ - - - #{Keyword.get(instance, :name)} - #{Endpoint.url()} - #{Keyword.get(instance, :limit)} - #{!Keyword.get(instance, :registrations_open)} - - - """ - end - - def render("frontend_configurations.json", _) do - Config.get(:frontend_configurations, %{}) - |> Enum.into(%{}) - end -end diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/o_auth/password_controller_test.exs similarity index 99% rename from test/pleroma/web/twitter_api/password_controller_test.exs rename to test/pleroma/web/o_auth/password_controller_test.exs index 26cca1345..bb61b24fd 100644 --- a/test/pleroma/web/twitter_api/password_controller_test.exs +++ b/test/pleroma/web/o_auth/password_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do +defmodule Pleroma.Web.OAuth.PasswordControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Config diff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/o_auth/token_controller_test.exs similarity index 97% rename from test/pleroma/web/twitter_api/controller_test.exs rename to test/pleroma/web/o_auth/token_controller_test.exs index 494be9ec7..5c64cb394 100644 --- a/test/pleroma/web/twitter_api/controller_test.exs +++ b/test/pleroma/web/o_auth/token_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.ControllerTest do +defmodule Pleroma.Web.OAuth.TokenControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Repo diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/util_controller_test.exs similarity index 85% rename from test/pleroma/web/twitter_api/util_controller_test.exs rename to test/pleroma/web/pleroma_api/controllers/util_controller_test.exs index d06ae71aa..6e817df56 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/util_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do +defmodule Pleroma.Web.PleromaAPI.UtilControllerTest do use Pleroma.Web.ConnCase use Oban.Testing, repo: Pleroma.Repo @@ -182,153 +182,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end end - describe "POST /main/ostatus - remote_subscribe/2" do - setup do: clear_config([:instance, :federating], true) - - test "renders subscribe form", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) - |> response(:ok) - - refute response =~ "Could not find user" - assert response =~ "Remotely follow #{user.nickname}" - end - - test "renders subscribe form with error when user not found", %{conn: conn} do - response = - conn - |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) - |> response(:ok) - - assert response =~ "Could not find user" - refute response =~ "Remotely follow" - end - - test "it redirect to webfinger url", %{conn: conn} do - user = insert(:user) - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - conn = - conn - |> post("/main/ostatus", %{ - "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} - }) - - assert redirected_to(conn) == - "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" - end - - test "it renders form with error when user not found", %{conn: conn} do - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - response = - conn - |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) - |> response(:ok) - - assert response =~ "Something went wrong." - end - end - - describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do - setup do: clear_config([:instance, :federating], true) - - test "renders subscribe form", %{conn: conn} do - user = insert(:user) - status = insert(:note_activity, %{user: user}) - status_id = status.id - - assert is_binary(status_id) - - response = - conn - |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""}) - |> response(:ok) - - refute response =~ "Could not find status" - assert response =~ "Interacting with" - end - - test "renders subscribe form with error when status not found", %{conn: conn} do - response = - conn - |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""}) - |> response(:ok) - - assert response =~ "Could not find status" - refute response =~ "Interacting with" - end - - test "it redirect to webfinger url", %{conn: conn} do - user = insert(:user) - status = insert(:note_activity, %{user: user}) - status_id = status.id - status_ap_id = status.data["object"] - - assert is_binary(status_id) - assert is_binary(status_ap_id) - - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - conn = - conn - |> post("/main/ostatus", %{ - "status" => %{"status_id" => status_id, "profile" => user2.ap_id} - }) - - assert redirected_to(conn) == - "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}" - end - - test "it renders form with error when status not found", %{conn: conn} do - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - response = - conn - |> post("/main/ostatus", %{ - "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id} - }) - |> response(:ok) - - assert response =~ "Something went wrong." - end - end - - describe "GET /main/ostatus - show_subscribe_form/2" do - setup do: clear_config([:instance, :federating], true) - - test "it works with users", %{conn: conn} do - user = insert(:user) - - response = - conn - |> get("/main/ostatus", %{"nickname" => user.nickname}) - |> response(:ok) - - refute response =~ "Could not find user" - assert response =~ "Remotely follow #{user.nickname}" - end - - test "it works with statuses", %{conn: conn} do - user = insert(:user) - status = insert(:note_activity, %{user: user}) - status_id = status.id - - assert is_binary(status_id) - - response = - conn - |> get("/main/ostatus", %{"status_id" => status_id}) - |> response(:ok) - - refute response =~ "Could not find status" - assert response =~ "Interacting with" - end - end - test "it returns new captcha", %{conn: conn} do with_mock Pleroma.Captcha, new: fn -> "test_captcha" end do diff --git a/test/pleroma/web/twitter_api/twitter_api_test.exs b/test/pleroma/web/registration_test.exs similarity index 90% rename from test/pleroma/web/twitter_api/twitter_api_test.exs rename to test/pleroma/web/registration_test.exs index b3cd80146..896e1a600 100644 --- a/test/pleroma/web/twitter_api/twitter_api_test.exs +++ b/test/pleroma/web/registration_test.exs @@ -2,14 +2,14 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do +defmodule Pleroma.Web.RegistrationTest do use Pleroma.DataCase import Pleroma.Factory alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.UserInviteToken - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -25,7 +25,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("lain") end @@ -40,7 +40,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("lain") end @@ -57,7 +57,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) ObanHelpers.perform_all() refute user.is_confirmed @@ -89,7 +89,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :reason => "I love anime" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) ObanHelpers.perform_all() refute user.is_approved @@ -125,7 +125,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user1} = TwitterAPI.register_user(data1) + {:ok, user1} = Registration.register_user(data1) data2 = %{ :username => "lain", @@ -136,7 +136,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user2} = TwitterAPI.register_user(data2) + {:ok, user2} = Registration.register_user(data2) expected_text = ~s(@john test) @@ -160,7 +160,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") @@ -179,7 +179,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => "DudeLetMeInImAFairy" } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Invalid token" refute User.get_cached_by_nickname("GrimReaper") @@ -199,7 +199,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -221,7 +221,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do check_fn = fn invite -> data = Map.put(data, :token, invite.token) - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") end @@ -254,7 +254,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do data = Map.put(data, "token", invite.token) - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("vinny") @@ -282,7 +282,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -298,7 +298,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -321,7 +321,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -343,7 +343,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -359,7 +359,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -379,7 +379,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -401,7 +401,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -416,7 +416,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :bio => "close the world." } - {:error, error} = TwitterAPI.register_user(data) + {:error, error} = Registration.register_user(data) assert is_binary(error) refute User.get_cached_by_nickname("lain") diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs similarity index 68% rename from test/pleroma/web/twitter_api/remote_follow_controller_test.exs rename to test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs index f762b1356..116609013 100644 --- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs +++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do +defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do use Pleroma.Web.ConnCase alias Pleroma.MFA @@ -16,6 +16,11 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do import Mox import Pleroma.Factory + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + setup_all do: clear_config([:instance, :federating], true) setup do: clear_config([:user, :deny_follow_blocked]) @@ -49,7 +54,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do assert conn |> get( - remote_follow_path(conn, :follow, %{ + remote_interaction_path(conn, :follow, %{ acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" }) ) @@ -78,7 +83,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) |> html_response(200) assert response =~ "Log in to follow" @@ -109,7 +114,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) |> html_response(200) assert response =~ "Remote follow" @@ -130,7 +135,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, user) |> get( - remote_follow_path(conn, :follow, %{ + remote_interaction_path(conn, :follow, %{ acct: "https://mastodon.social/users/not_found" }) ) @@ -152,7 +157,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, user) |> assign(:token, read_token) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -167,7 +172,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, user) |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) assert redirected_to(conn) == "/users/#{user2.id}" end @@ -179,7 +184,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -195,7 +200,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -207,7 +212,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) |> response(200) assert response =~ "Error following account" @@ -222,7 +227,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, refresh_record(user)) |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) assert redirected_to(conn) == "/users/#{user2.id}" end @@ -244,7 +249,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) |> response(200) @@ -272,7 +277,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} }) |> response(200) @@ -300,7 +305,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn = conn |> post( - remote_follow_path(conn, :do_follow), + remote_interaction_path(conn, :do_follow), %{ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} } @@ -329,7 +334,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> post( - remote_follow_path(conn, :do_follow), + remote_interaction_path(conn, :do_follow), %{ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} } @@ -348,7 +353,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) @@ -361,7 +366,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} }) |> response(200) @@ -374,7 +379,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} }) |> response(200) @@ -388,7 +393,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} }) |> response(200) @@ -404,7 +409,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) |> response(200) @@ -423,7 +428,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) assert avatar_url == "https://remote.org/avatar.png" end @@ -440,7 +445,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) url = Pleroma.Web.Endpoint.url() assert String.starts_with?(avatar_url, url) @@ -455,7 +460,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]} }) - avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png" end @@ -485,13 +490,160 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn = conn |> get( - remote_follow_path(conn, :authorize_interaction, %{ + remote_interaction_path(conn, :authorize_interaction, %{ uri: "https://mastodon.social/users/emelie" }) ) assert redirected_to(conn) == - remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when user not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + + assert is_binary(status_id) + + response = + conn + |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find status" + assert response =~ "Interacting with" + end + + test "renders subscribe form with error when status not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find status" + refute response =~ "Interacting with" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + status_ap_id = status.data["object"] + + assert is_binary(status_id) + assert is_binary(status_ap_id) + + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "status" => %{"status_id" => status_id, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}" + end + + test "it renders form with error when status not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{ + "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id} + }) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + describe "GET /main/ostatus - show_subscribe_form/2" do + setup do: clear_config([:instance, :federating], true) + + test "it works with users", %{conn: conn} do + user = insert(:user) + + response = + conn + |> get("/main/ostatus", %{"nickname" => user.nickname}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "it works with statuses", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + + assert is_binary(status_id) + + response = + conn + |> get("/main/ostatus", %{"status_id" => status_id}) + |> response(:ok) + + refute response =~ "Could not find status" + assert response =~ "Interacting with" end end end From 37041aae6020af547a03126e4e916d04de0c27d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 2 Mar 2026 22:50:49 +0100 Subject: [PATCH 183/317] update mix.exs deps versions to match mix.lock so they don't look that scary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/mix-exs-update.skip | 0 mix.exs | 86 ++++++++++++++++----------------- 2 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 changelog.d/mix-exs-update.skip diff --git a/changelog.d/mix-exs-update.skip b/changelog.d/mix-exs-update.skip new file mode 100644 index 000000000..e69de29bb diff --git a/mix.exs b/mix.exs index 0532bffd7..dc9fa31f5 100644 --- a/mix.exs +++ b/mix.exs @@ -114,7 +114,7 @@ defmodule Pleroma.Mixfile do if Version.match?(System.version(), "<1.15.0-rc.0") do [] else - [{:logger_backends, "~> 1.0"}] + [{:logger_backends, "~> 1.0.0"}] end end @@ -125,61 +125,61 @@ defmodule Pleroma.Mixfile do [ {:phoenix, git: "https://github.com/feld/phoenix", branch: "v1.7.14-websocket-headers", override: true}, - {:phoenix_ecto, "~> 4.4"}, - {:ecto_sql, "~> 3.10"}, + {:phoenix_ecto, "~> 4.6"}, + {:ecto_sql, "~> 3.13"}, {:ecto_enum, "~> 1.4"}, - {:postgrex, ">= 0.20.0"}, + {:postgrex, ">= 0.21.1"}, {:phoenix_html, "~> 3.3"}, - {:phoenix_live_view, "~> 1.1.0"}, - {:phoenix_live_dashboard, "~> 0.8.0"}, + {:phoenix_live_view, "~> 1.1.19"}, + {:phoenix_live_dashboard, "~> 0.8.7"}, {:telemetry_metrics, "~> 0.6"}, - {:telemetry_poller, "~> 1.0"}, - {:tzdata, "~> 1.0.3"}, + {:telemetry_poller, "~> 1.3"}, + {:tzdata, "~> 1.0.5"}, {:plug_cowboy, "~> 2.7"}, - {:oban, "~> 2.19.0"}, + {:oban, "~> 2.19.4"}, {:oban_plugins_lazarus, git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", ref: "e49fc355baaf0e435208bf5f534d31e26e897711"}, {:oban_web, "~> 2.11"}, - {:gettext, "~> 0.20"}, - {:bcrypt_elixir, "~> 2.2"}, + {:gettext, "~> 0.24"}, + {:bcrypt_elixir, "~> 2.3"}, {:trailing_format_plug, "~> 0.0.7"}, - {:fast_sanitize, "~> 0.2.0"}, + {:fast_sanitize, "~> 0.2.3"}, {:html_entities, "~> 0.5", override: true}, {:calendar, "~> 1.0"}, - {:cachex, "~> 3.2"}, - {:tesla, "~> 1.11"}, + {:cachex, "~> 3.6"}, + {:tesla, "~> 1.15"}, {:castore, "~> 1.0"}, {:cowlib, "~> 2.15"}, {:gun, "~> 2.2"}, - {:finch, "~> 0.15"}, - {:jason, "~> 1.2"}, - {:mogrify, "~> 0.9.0", override: true}, - {:ex_aws, "~> 2.1.6"}, - {:ex_aws_s3, "~> 2.0"}, - {:sweet_xml, "~> 0.7.2"}, + {:finch, "~> 0.20"}, + {:jason, "~> 1.4"}, + {:mogrify, "~> 0.9.3", override: true}, + {:ex_aws, "~> 2.1.9"}, + {:ex_aws_s3, "~> 2.5"}, + {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, - {:swoosh, "~> 1.16.9"}, - {:phoenix_swoosh, "~> 1.1"}, - {:gen_smtp, "~> 0.13"}, - {:mua, "~> 0.2.0"}, - {:mail, "~> 0.3.0"}, - {:ex_syslogger, "~> 1.4"}, - {:floki, "~> 0.35"}, - {:timex, "~> 3.6"}, - {:ueberauth, "~> 0.4"}, + {:swoosh, "~> 1.16.12"}, + {:phoenix_swoosh, "~> 1.2"}, + {:gen_smtp, "~> 0.15"}, + {:mua, "~> 0.2.4"}, + {:mail, "~> 0.3.1"}, + {:ex_syslogger, "~> 1.5"}, + {:floki, "~> 0.38"}, + {:timex, "~> 3.7"}, + {:ueberauth, "~> 0.10"}, {:linkify, "~> 0.5.3"}, {:http_signatures, "~> 0.1.2"}, {:telemetry, "~> 1.0.0", override: true}, {:poolboy, "~> 1.5"}, {:prom_ex, "~> 1.9"}, {:recon, "~> 2.5"}, - {:joken, "~> 2.0"}, + {:joken, "~> 2.6"}, {:pot, "~> 1.0"}, - {:ex_const, "~> 0.2"}, + {:ex_const, "~> 0.3"}, {:plug_static_index_html, "~> 1.0.0"}, {:flake_id, "~> 0.1.0"}, {:concurrent_limiter, "~> 0.1.1"}, @@ -190,31 +190,31 @@ defmodule Pleroma.Mixfile do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, - {:majic, "~> 1.0"}, - {:open_api_spex, "~> 3.16"}, + {:majic, "~> 1.1"}, + {:open_api_spex, "~> 3.22"}, {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.36"}, - {:elixir_make, "~> 0.7.7", override: true}, + {:elixir_make, "~> 0.7.8", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, {:exile, "~> 0.10.0"}, - {:bandit, "~> 1.5.2"}, - {:websock_adapter, "~> 0.5.6"}, + {:bandit, "~> 1.5.7"}, + {:websock_adapter, "~> 0.5.8"}, {:oban_live_dashboard, "~> 0.1.1"}, {:multipart, "~> 0.4.0", optional: true}, - {:argon2_elixir, "~> 4.0"}, + {:argon2_elixir, "~> 4.1"}, ## dev & test {:phoenix_live_reload, "~> 1.3.3", only: :dev}, - {:poison, "~> 3.0", only: :test}, - {:ex_doc, "~> 0.22", only: :dev, runtime: false}, - {:ex_machina, "~> 2.4", only: :test}, + {:poison, "~> 3.1", only: :test}, + {:ex_doc, "~> 0.38", only: :dev, runtime: false}, + {:ex_machina, "~> 2.8", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:mock, "~> 0.3.5", only: :test}, + {:mock, "~> 0.3.9", only: :test}, {:covertool, "~> 2.0", only: :test}, {:hackney, "~> 1.25.0", override: true}, - {:mox, "~> 1.0", only: :test}, + {:mox, "~> 1.2", only: :test}, {:websockex, "~> 0.4.3", only: :test}, - {:benchee, "~> 1.0", only: :benchmark}, + {:benchee, "~> 1.4", only: :benchmark}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} ] ++ oauth_deps() ++ logger_deps() end From 2086561fbd0956422744831ac9db2ee26b8f46d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 17 Feb 2026 20:02:21 +0100 Subject: [PATCH 184/317] Various bookmark folders-related improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/bookmark-folders.ignore | 1 + .../API/differences_in_mastoapi_responses.md | 2 +- lib/pleroma/bookmark.ex | 11 ++------- lib/pleroma/bookmark_folder.ex | 2 +- .../web/api_spec/schemas/bookmark_folder.ex | 10 ++++++-- .../controllers/status_controller.ex | 2 -- .../controllers/bookmark_folder_controller.ex | 2 -- .../pleroma_api/views/bookmark_folder_view.ex | 23 ++++++++----------- 8 files changed, 23 insertions(+), 30 deletions(-) create mode 100644 changelog.d/bookmark-folders.ignore diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore new file mode 100644 index 000000000..8705ac00b --- /dev/null +++ b/changelog.d/bookmark-folders.ignore @@ -0,0 +1 @@ +Various bookmark folders-related improvements diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 052b2716b..0d6fc09c8 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -74,7 +74,7 @@ Pleroma does not process remote images and therefore cannot include fields such The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID. -The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. +The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. Bookmarking an already bookmarked post will update the folder association, or remove it if `folder_id` is omitted or `null`. ## Accounts diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex index 1a2a63b82..1c78d495f 100644 --- a/lib/pleroma/bookmark.ex +++ b/lib/pleroma/bookmark.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Bookmark do schema "bookmarks" do belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) - belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType) + belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.Type) timestamps() end @@ -38,7 +38,7 @@ defmodule Pleroma.Bookmark do |> validate_required([:user_id, :activity_id]) |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index) |> Repo.insert( - on_conflict: [set: [folder_id: folder_id]], + on_conflict: [set: [folder_id: folder_id, updated_at: NaiveDateTime.utc_now()]], conflict_target: [:user_id, :activity_id] ) end @@ -76,11 +76,4 @@ defmodule Pleroma.Bookmark do |> Repo.one() |> Repo.delete() end - - def set_folder(bookmark, folder_id) do - bookmark - |> cast(%{folder_id: folder_id}, [:folder_id]) - |> validate_required([:folder_id]) - |> Repo.update() - end end diff --git a/lib/pleroma/bookmark_folder.ex b/lib/pleroma/bookmark_folder.ex index 14d37e197..65856bb29 100644 --- a/lib/pleroma/bookmark_folder.ex +++ b/lib/pleroma/bookmark_folder.ex @@ -14,7 +14,7 @@ defmodule Pleroma.BookmarkFolder do alias Pleroma.User @type t :: %__MODULE__{} - @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} schema "bookmark_folders" do field(:name, :string) diff --git a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex index e8b4f43b7..f5ce9e8a1 100644 --- a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex +++ b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex @@ -15,12 +15,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do properties: %{ id: FlakeID, name: %Schema{type: :string, description: "Folder name"}, - emoji: %Schema{type: :string, description: "Folder emoji", nullable: true} + emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}, + emoji_url: %Schema{ + type: :string, + description: "URL of the folder emoji if it's a custom emoji, null otherwise", + nullable: true + } }, example: %{ "id" => "9toJCu5YZW7O7gfvH6", "name" => "Read later", - "emoji" => nil + "emoji" => nil, + "emoji_url" => nil } }) end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index db2b61b3c..26e38aac8 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -81,10 +81,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin]) - # Note: scope not present in Mastodon: read:bookmarks plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks) - # Note: scope not present in Mastodon: write:bookmarks plug( OAuthScopesPlug, %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] diff --git a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex index 6d6e2e940..55f323a16 100644 --- a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex @@ -10,10 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - # Note: scope not present in Mastodon: read:bookmarks plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index) - # Note: scope not present in Mastodon: write:bookmarks plug( OAuthScopesPlug, %{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete] diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex index 29028781f..ddffa25d3 100644 --- a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex +++ b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex @@ -9,11 +9,13 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do alias Pleroma.Emoji def render("show.json", %{folder: %BookmarkFolder{} = folder}) do + {emoji, emoji_url} = get_emoji(folder.emoji) + %{ id: folder.id |> to_string(), name: folder.name, - emoji: folder.emoji, - emoji_url: get_emoji_url(folder.emoji) + emoji: emoji, + emoji_url: emoji_url } end @@ -21,20 +23,15 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders)) end - defp get_emoji_url(nil) do - nil - end + defp get_emoji(nil), do: {nil, nil} - defp get_emoji_url(emoji) do + defp get_emoji(emoji) do if Emoji.unicode?(emoji) do - nil + {emoji, nil} else - emoji = Emoji.get(emoji) - - if emoji != nil do - Emoji.local_url(emoji.file) - else - nil + case Emoji.get(emoji) do + nil -> {nil, nil} + emoji_data -> {emoji, Emoji.local_url(emoji_data.file)} end end end From 1b182b07dcef7f915454043985bb28e030d8d401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 2 Mar 2026 23:10:20 +0100 Subject: [PATCH 185/317] is this what i was meant to do? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../remote_interaction_operation.ex | 68 ++++++++++++++++--- .../controllers/util_controller.ex | 2 +- .../remote_interaction_controller.ex | 2 +- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex index a490bd491..b5bd9d72f 100644 --- a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex @@ -13,16 +13,6 @@ defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do apply(__MODULE__, operation, []) end - def remote_subscribe_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Remote Subscribe", - operationId: "RemoteInteractionController.remote_subscribe", - parameters: [], - responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} - } - end - def remote_interaction_operation do %Operation{ tags: ["Remote interaction"], @@ -49,6 +39,64 @@ defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do } end + def follow_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Display follow form", + operationId: "RemoteInteractionController.follow", + parameters: [], + responses: %{ + 200 => Operation.response("Web Page", "text/html", %Schema{type: :string}), + 302 => Operation.response("Redirect to the status page", nil, nil) + } + } + end + + def do_follow_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Perform follow activity", + operationId: "RemoteInteractionController.do_follow", + parameters: [], + responses: %{ + 200 => Operation.response("Web page", "text/html", %Schema{type: :string}), + 302 => Operation.response("Redirect to the account page", nil, nil) + } + } + end + + def authorize_interaction_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Authorize remote interaction", + operationId: "RemoteInteractionController.authorize_interaction", + parameters: [], + responses: %{ + 302 => Operation.response("Redirect to remote_interaction path", nil, nil) + } + } + end + + def show_subscribe_form_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Show remote subscribe form", + operationId: "RemoteInteractionController.show_subscribe_form", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end + + def remote_subscribe_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Remote Subscribe", + operationId: "RemoteInteractionController.remote_subscribe", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end + def show_subscribe_form_operation do %Operation{ tags: ["Remote interaction"], diff --git a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 9528d5564..70b0b9cf6 100644 --- a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - plug(Pleroma.Web.ApiSpec.CastAndValidate, [replace_params: false]) + plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex index 8592fc990..90f15cdb6 100644 --- a/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex +++ b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionController do plug( Pleroma.Web.Plugs.OAuthScopesPlug, %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} - when action in [:do_follow] + when action == :do_follow ) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.RemoteInteractionOperation From b645643cfb5aa835876001af48e1d237a36f1f9d Mon Sep 17 00:00:00 2001 From: Oneric Date: Tue, 10 Jun 2025 18:33:59 +0000 Subject: [PATCH 186/317] Merge pull request 'Allow fine-grained announce visibilities' (#941) from Oneric/akkoma:announce-visibility into develop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/941 Reviewed-by: floatingghost Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/activity_pub/builder.ex | 26 +++--- lib/pleroma/web/common_api.ex | 14 ++-- lib/pleroma/web/common_api/utils.ex | 84 ++++++++++++------- .../announce_validation_test.exs | 21 +++-- .../web/activity_pub/side_effects_test.exs | 8 +- 5 files changed, 91 insertions(+), 62 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 167c769a9..3b208ab77 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -332,21 +332,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} def announce(actor, object, options \\ []) do - public? = Keyword.get(options, :public, false) + visibility = Keyword.get(options, :visibility, "public") - to = - cond do - actor.ap_id == Relay.ap_id() -> - [actor.follower_address] - - public? and Visibility.local_public?(object) -> - [actor.follower_address, object.data["actor"], Utils.as_local_public()] - - public? -> - [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] - - true -> - [actor.follower_address, object.data["actor"]] + {to, cc} = + if actor.ap_id == Relay.ap_id() do + {[actor.follower_address], []} + else + Pleroma.Web.CommonAPI.Utils.get_to_and_cc_for_visibility( + visibility, + actor.follower_address, + nil, + [object.data["actor"]] + ) end {:ok, @@ -355,6 +352,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do "actor" => actor.ap_id, "object" => object.data["id"], "to" => to, + "cc" => cc, "context" => object.data["context"], "type" => "Announce", "published" => Utils.make_date() diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 04181ad8f..cb9d521b3 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -222,8 +222,8 @@ defmodule Pleroma.Web.CommonAPI do with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), object = %Object{} <- Object.normalize(activity, fetch: false), {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, - public = public_announce?(object, params), - {:ok, announce, _} <- Builder.announce(user, object, public: public), + visibility = announce_visibility(object, params), + {:ok, announce, _} <- Builder.announce(user, object, visibility: visibility), {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do {:ok, activity} else @@ -407,13 +407,11 @@ defmodule Pleroma.Web.CommonAPI do end end - defp public_announce?(_, %{visibility: visibility}) - when visibility in ~w{public unlisted private direct}, - do: visibility in ~w(public unlisted) + def announce_visibility(_, %{visibility: visibility}) + when visibility in ~w{public unlisted private direct local}, + do: visibility - defp public_announce?(object, _) do - Visibility.public?(object) - end + def announce_visibility(object, _), do: Visibility.get_visibility(object) @spec get_visibility(map(), map() | nil, Participation.t() | nil) :: {String.t() | nil, String.t() | nil} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 91bf9c502..32572a721 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -75,48 +75,70 @@ defmodule Pleroma.Web.CommonAPI.Utils do {Enum.map(participation.recipients, & &1.ap_id), []} end - def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do - to = - case visibility do - "public" -> [Pleroma.Constants.as_public() | draft.mentions] - "local" -> [Utils.as_local_public() | draft.mentions] + def get_to_and_cc(%{visibility: visibility} = draft) do + # If the OP is a DM already, add the implicit actor + mentions = + if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do + Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]) + else + draft.mentions end - cc = [draft.user.follower_address] - - if draft.in_reply_to do - {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} - else - {to, cc} - end + get_to_and_cc_for_visibility( + visibility, + draft.user.follower_address, + draft.in_reply_to && draft.in_reply_to.data["actor"], + mentions + ) end - def get_to_and_cc(%{visibility: "unlisted"} = draft) do - to = [draft.user.follower_address | draft.mentions] - cc = [Pleroma.Constants.as_public()] + def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do + scope_addr = Pleroma.Constants.as_public() - if draft.in_reply_to do - {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} - else - {to, cc} - end + to = + if parent_actor, + do: Enum.uniq([parent_actor, scope_addr | mentions]), + else: [scope_addr | mentions] + + {to, [follower_collection]} end - def get_to_and_cc(%{visibility: "private"} = draft) do - {to, cc} = get_to_and_cc(struct(draft, visibility: "direct")) - {[draft.user.follower_address | to], cc} + def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do + recipients = + if parent_actor, + do: Enum.uniq([parent_actor | mentions]), + else: mentions + + to = [ + Utils.as_local_public() + | Enum.filter(recipients, fn addr -> + String.starts_with?(addr, Pleroma.Web.Endpoint.url() <> "/") + end) + ] + + {to, [follower_collection]} end - def get_to_and_cc(%{visibility: "direct"} = draft) do - # If the OP is a DM already, add the implicit actor. - if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do - {Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []} - else - {draft.mentions, []} - end + def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do + to = + if parent_actor, + do: Enum.uniq([parent_actor, follower_collection | mentions]), + else: [follower_collection | mentions] + + {to, [Pleroma.Constants.as_public()]} end - def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []} + def get_to_and_cc_for_visibility("private", follower_collection, _, mentions) do + {[follower_collection | mentions], []} + end + + def get_to_and_cc_for_visibility("direct", _, _, mentions) do + {mentions, []} + end + + def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do + {mentions, []} + end def get_addressed_users(_, to) when is_list(to) do User.get_ap_ids_by_nicknames(to) diff --git a/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs index 5b2fcb26d..1ca4f04f6 100644 --- a/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs @@ -86,23 +86,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do object = Object.normalize(post_activity, fetch: false) # Another user can't announce it - {:ok, announce, []} = Builder.announce(announcer, object, public: false) + {:ok, announce, []} = Builder.announce(announcer, object, visibility: "private") {:error, cng} = ObjectValidator.validate(announce, []) assert {:actor, {"can not announce this object", []}} in cng.errors - # The actor of the object can announce it - {:ok, announce, []} = Builder.announce(user, object, public: false) + # The actor of the object can announce it with a restrictive scope + {:ok, announce, []} = Builder.announce(user, object, visibility: "private") + assert {:ok, _, _} = ObjectValidator.validate(announce, []) + {:ok, announce, []} = Builder.announce(user, object, visibility: "direct") assert {:ok, _, _} = ObjectValidator.validate(announce, []) # The actor of the object can not announce it publicly - {:ok, announce, []} = Builder.announce(user, object, public: true) + {:ok, announce, []} = Builder.announce(user, object, visibility: "public") + {:error, cng1} = ObjectValidator.validate(announce, []) - {:error, cng} = ObjectValidator.validate(announce, []) + {:ok, announce, []} = Builder.announce(user, object, visibility: "unlisted") + {:error, cng2} = ObjectValidator.validate(announce, []) - assert {:actor, {"can not announce this object publicly", []}} in cng.errors + {:ok, announce, []} = Builder.announce(user, object, visibility: "local") + {:error, cng3} = ObjectValidator.validate(announce, []) + + for cng <- [cng1, cng2, cng3] do + assert {:actor, {"can not announce this object publicly", []}} in cng.errors + end end end end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 4a18cab68..6d20c591c 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -784,13 +784,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) - {:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) + {:ok, announce_data, _meta} = Builder.announce(user, post.object, visibility: "public") {:ok, private_announce_data, _meta} = - Builder.announce(user, private_post.object, public: false) + Builder.announce(user, private_post.object, visibility: "private") {:ok, relay_announce_data, _meta} = - Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) + Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, + visibility: "public" + ) {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) From 8921dbfffd882d657ffaa7f4c8cfc8e723b0240d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 18 Feb 2026 13:35:20 +0100 Subject: [PATCH 187/317] changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/boost-visibilities.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/boost-visibilities.add diff --git a/changelog.d/boost-visibilities.add b/changelog.d/boost-visibilities.add new file mode 100644 index 000000000..317d9840d --- /dev/null +++ b/changelog.d/boost-visibilities.add @@ -0,0 +1 @@ +Allow fine-grained announce visibilities From 490cd33bc9e2d58799b9c7702ffdaa2614506c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 18 Feb 2026 11:06:13 +0100 Subject: [PATCH 188/317] Support lists `exclusive` param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/exclusive-lists.add | 1 + lib/pleroma/list.ex | 23 ++++++-- .../web/api_spec/operations/list_operation.ex | 26 +++++++-- lib/pleroma/web/api_spec/schemas/list.ex | 6 +- .../controllers/list_controller.ex | 10 ++-- .../controllers/timeline_controller.ex | 6 +- .../web/mastodon_api/views/list_view.ex | 3 +- .../20260218000000_add_exclusive_to_lists.exs | 9 +++ test/pleroma/list_test.exs | 56 ++++++++++++------- .../controllers/list_controller_test.exs | 16 +++--- .../controllers/timeline_controller_test.exs | 25 +++++++++ 11 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 changelog.d/exclusive-lists.add create mode 100644 priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add new file mode 100644 index 000000000..bbd722f07 --- /dev/null +++ b/changelog.d/exclusive-lists.add @@ -0,0 +1 @@ +Support lists `exclusive` param diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index b446b91a0..cd23bd0a9 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -17,13 +17,14 @@ defmodule Pleroma.List do field(:title, :string) field(:following, {:array, :string}, default: []) field(:ap_id, :string) + field(:exclusive, :boolean, default: false) timestamps() end - def title_changeset(list, attrs \\ %{}) do + def update_changeset(list, attrs \\ %{}) do list - |> cast(attrs, [:title]) + |> cast(attrs, [:title, :exclusive]) |> validate_required([:title]) end @@ -91,14 +92,14 @@ defmodule Pleroma.List do |> Repo.all() end - def rename(%Pleroma.List{} = list, title) do + def update(%Pleroma.List{} = list, params) do list - |> title_changeset(%{title: title}) + |> update_changeset(params) |> Repo.update() end - def create(title, %User{} = creator) do - changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title}) + def create(params, %User{} = creator) do + changeset = update_changeset(%Pleroma.List{user_id: creator.id}, params) if changeset.valid? do Repo.transaction(fn -> @@ -149,4 +150,14 @@ defmodule Pleroma.List do end def member?(_, _), do: false + + def get_exclusive_list_members(%User{id: user_id}) do + Pleroma.List + |> where([l], l.user_id == ^user_id) + |> where([l], l.exclusive == true) + |> select([l], l.following) + |> Repo.all() + |> List.flatten() + |> Enum.uniq() + end end diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index 7d876ae2d..d2e803178 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do summary: "Create a list", description: "Fetch the list with the given ID. Used for verifying the title of a list.", operationId: "ListController.create", - requestBody: create_update_request(), + requestBody: create_request(), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("List", "application/json", List), @@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do description: "Change the title of a list", operationId: "ListController.update", parameters: [id_param()], - requestBody: create_update_request(), + requestBody: update_request(), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("List", "application/json", List), @@ -164,14 +164,15 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end - defp create_update_request do + defp create_request do request_body( "Parameters", %Schema{ - description: "POST body for creating or updating a List", + description: "POST body for creating a List", type: :object, properties: %{ - title: %Schema{type: :string, description: "List title"} + title: %Schema{type: :string, description: "List title"}, + exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} }, required: [:title] }, @@ -179,6 +180,21 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end + defp update_request do + request_body( + "Parameters", + %Schema{ + description: "PUT body for updating a List", + type: :object, + properties: %{ + title: %Schema{type: :string, description: "List title"}, + exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + } + }, + required: true + ) + end + defp add_remove_accounts_request(required) when is_boolean(required) do request_body( "Parameters", diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex index e57de7917..5df674894 100644 --- a/lib/pleroma/web/api_spec/schemas/list.ex +++ b/lib/pleroma/web/api_spec/schemas/list.ex @@ -13,7 +13,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do type: :object, properties: %{ id: %Schema{type: :string, description: "The internal database ID of the list"}, - title: %Schema{type: :string, description: "The user-defined title of the list"} + title: %Schema{type: :string, description: "The user-defined title of the list"}, + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } }, example: %{ "id" => "12249", diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 3bfc365a5..048012ae6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -28,27 +28,27 @@ defmodule Pleroma.Web.MastodonAPI.ListController do # POST /api/v1/lists def create( - %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{title: title}}}} = + %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn, _ ) do - with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do + with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(params, user) do render(conn, "show.json", list: list) end end - # GET /api/v1/lists/:idOB + # GET /api/v1/lists/:id def show(%{assigns: %{list: list}} = conn, _) do render(conn, "show.json", list: list) end # PUT /api/v1/lists/:id def update( - %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: %{title: title}}}} = + %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: params}}} = conn, _ ) do - with {:ok, list} <- Pleroma.List.rename(list, title) do + with {:ok, list} <- Pleroma.List.update(list, params) do render(conn, "show.json", list: list) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 5ee74a80e..99a5b6957 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -45,6 +45,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> User.followed_hashtags() |> Enum.map(& &1.id) + excluded_list_members = + user + |> Pleroma.List.get_exclusive_list_members() + params = params |> Map.put(:type, ["Create", "Announce"]) @@ -58,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> Map.delete(:local) activities = - [user.ap_id | User.following(user)] + [user.ap_id | User.following(user) -- excluded_list_members] |> ActivityPub.fetch_activities(params) |> Enum.reverse() diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex index a7ae7c5f7..740f458d4 100644 --- a/lib/pleroma/web/mastodon_api/views/list_view.ex +++ b/lib/pleroma/web/mastodon_api/views/list_view.ex @@ -13,7 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.ListView do def render("show.json", %{list: list}) do %{ id: to_string(list.id), - title: list.title + title: list.title, + exclusive: list.exclusive } end end diff --git a/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs b/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs new file mode 100644 index 000000000..253a8acb7 --- /dev/null +++ b/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddExclusiveToLists do + use Ecto.Migration + + def change do + alter table(:lists) do + add(:exclusive, :boolean, default: false) + end + end +end diff --git a/test/pleroma/list_test.exs b/test/pleroma/list_test.exs index a68146b0d..d44310689 100644 --- a/test/pleroma/list_test.exs +++ b/test/pleroma/list_test.exs @@ -10,22 +10,23 @@ defmodule Pleroma.ListTest do test "creating a list" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - %Pleroma.List{title: title} = Pleroma.List.get(list.id, user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) + %Pleroma.List{title: title, exclusive: exclusive} = Pleroma.List.get(list.id, user) assert title == "title" + assert exclusive == false end test "validates title" do user = insert(:user) - assert {:error, changeset} = Pleroma.List.create("", user) + assert {:error, changeset} = Pleroma.List.create(%{title: ""}, user) assert changeset.errors == [title: {"can't be blank", [validation: :required]}] end test "getting a list not belonging to the user" do user = insert(:user) other_user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) ret = Pleroma.List.get(list.id, other_user) assert is_nil(ret) end @@ -33,7 +34,7 @@ defmodule Pleroma.ListTest do test "adding an user to a list" do user = insert(:user) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, %{following: following}} = Pleroma.List.follow(list, other_user) assert [other_user.follower_address] == following end @@ -41,7 +42,7 @@ defmodule Pleroma.ListTest do test "removing an user from a list" do user = insert(:user) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) {:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) assert [] == following @@ -49,14 +50,27 @@ defmodule Pleroma.ListTest do test "renaming a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{title: title}} = Pleroma.List.rename(list, "new") + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) + {:ok, %{title: title}} = Pleroma.List.update(list, %{title: "new"}) assert "new" == title end + test "updating a list exclusivity" do + user = insert(:user) + + {:ok, %{exclusive: exclusive} = list} = + Pleroma.List.create(%{title: "title", exclusive: true}, user) + + assert exclusive == true + {:ok, %{exclusive: exclusive} = list} = Pleroma.List.update(list, %{exclusive: false}) + assert exclusive == false + {:ok, %{exclusive: exclusive}} = Pleroma.List.update(list, %{exclusive: true}) + assert exclusive == true + end + test "deleting a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, list} = Pleroma.List.delete(list) assert is_nil(Repo.get(Pleroma.List, list.id)) end @@ -65,7 +79,7 @@ defmodule Pleroma.ListTest do user = insert(:user) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) {:ok, following} = Pleroma.List.get_following(list) @@ -76,9 +90,9 @@ defmodule Pleroma.ListTest do test "getting all lists by an user" do user = insert(:user) other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.create(%{title: "title"}, user) + {:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user) + {:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user) lists = Pleroma.List.for_user(user, %{}) assert list_one in lists assert list_two in lists @@ -88,9 +102,9 @@ defmodule Pleroma.ListTest do test "getting all lists the user is a member of" do user = insert(:user) other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.create(%{title: "title"}, user) + {:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user) + {:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user) {:ok, list_one} = Pleroma.List.follow(list_one, other_user) {:ok, list_two} = Pleroma.List.follow(list_two, other_user) {:ok, list_three} = Pleroma.List.follow(list_three, user) @@ -106,8 +120,8 @@ defmodule Pleroma.ListTest do not_owner = insert(:user) member_1 = insert(:user) member_2 = insert(:user) - {:ok, owned_list} = Pleroma.List.create("owned", owner) - {:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner) + {:ok, owned_list} = Pleroma.List.create(%{title: "owned"}, owner) + {:ok, not_owned_list} = Pleroma.List.create(%{title: "not owned"}, not_owner) {:ok, owned_list} = Pleroma.List.follow(owned_list, member_1) {:ok, owned_list} = Pleroma.List.follow(owned_list, member_2) {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1) @@ -123,14 +137,14 @@ defmodule Pleroma.ListTest do test "get by ap_id" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) assert Pleroma.List.get_by_ap_id(list.ap_id) == list end test "memberships" do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) assert Pleroma.List.memberships(member) == [list.ap_id] @@ -140,7 +154,7 @@ defmodule Pleroma.ListTest do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) assert Pleroma.List.member?(list, member) diff --git a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs index 430b8b89d..bfda48be4 100644 --- a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs @@ -56,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) assert %{} == conn @@ -77,7 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do other_user = insert(:user) third_user = insert(:user) fourth_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) {:ok, list} = Pleroma.List.follow(list, fourth_user) @@ -98,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) @@ -115,7 +115,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "listing users in a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = @@ -129,7 +129,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "retrieving a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = conn @@ -150,7 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) assert %{"title" => "newname"} = conn @@ -161,7 +161,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "validates title when renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = conn @@ -175,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "deleting a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = delete(conn, "/api/v1/lists/#{list.id}") diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 4d646509c..9808652d2 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -149,6 +149,31 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do |> get("/api/v1/timelines/home?remote=true&local=true") |> json_response_and_validate_schema(200) == [] end + + test "the home timeline excludes posts from users in exclusive lists", %{ + user: user, + conn: conn + } do + other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, user, other_user1} = User.follow(user, other_user1) + {:ok, user, other_user2} = User.follow(user, other_user2) + + {:ok, list} = Pleroma.List.create(%{title: "foo", exclusive: true}, user) + {:ok, _list} = Pleroma.List.follow(list, other_user1) + + {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi"}) + {:ok, %{id: activity2_id}} = CommonAPI.post(other_user2, %{status: "hi too"}) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^activity2_id}] = response + end end describe "public" do From 19025563e2107e5aa70b5d85aa9c1f9b20cba5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 3 Mar 2026 00:29:46 +0100 Subject: [PATCH 189/317] fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../operations/remote_interaction_operation.ex | 10 ---------- lib/pleroma/web/router.ex | 3 ++- .../web/templates/static_fe/static_fe/profile.html.eex | 2 +- .../remote_interaction_controller_test.exs | 6 +++--- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex index b5bd9d72f..54edbcf32 100644 --- a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex @@ -96,14 +96,4 @@ defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} } end - - def show_subscribe_form_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Show remote subscribe form", - operationId: "RemoteInteractionController.show_subscribe_form", - parameters: [], - responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} - } - end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 22e82568a..eea0a0912 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -234,10 +234,11 @@ defmodule Pleroma.Web.Router do end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do + pipe_through(:pleroma_api) + get("/emoji", UtilController, :emoji) get("/captcha", UtilController, :captcha) get("/healthcheck", UtilController, :healthcheck) - get("/federation_status", InstancesController, :show) end diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index a14ca305e..85bfd7b3a 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -2,7 +2,7 @@

<%= link instance_name(), to: "/" %>

-
+ diff --git a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs index 116609013..9df86c0a2 100644 --- a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs +++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs @@ -428,7 +428,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user) assert avatar_url == "https://remote.org/avatar.png" end @@ -445,7 +445,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user) url = Pleroma.Web.Endpoint.url() assert String.starts_with?(avatar_url, url) @@ -460,7 +460,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]} }) - avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user) assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png" end From ca38217898060775500ac9ebac3852e5a4c91e8f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 3 Mar 2026 23:10:15 +0100 Subject: [PATCH 190/317] Fix AccountController Plug warning the URI path used in plug tests must start with "/", got: "api/v1/blocks" (plug 1.19.1) lib/plug/adapters/test/conn.ex:14: Plug.Adapters.Test.Conn.conn/4 (phoenix 1.7.14) lib/phoenix/test/conn_test.ex:236: Phoenix.ConnTest.dispatch_endpoint/5 (phoenix 1.7.14) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5 test/pleroma/web/mastodon_api/controllers/account_controller_test.exs:2099: Pleroma.Web.MastodonAPI.AccountControllerTest."test getting a list of blocks"/1 (ex_unit 1.19.5) lib/ex_unit/runner.ex:528: ExUnit.Runner.exec_test/2 (ex_unit 1.19.5) lib/ex_unit/capture_log.ex:121: ExUnit.CaptureLog.with_log/2 (ex_unit 1.19.5) lib/ex_unit/runner.ex:477: anonymous fn/3 in ExUnit.Runner.maybe_capture_log/3 (stdlib 7.2) timer.erl:599: :timer.tc/2 (ex_unit 1.19.5) lib/ex_unit/runner.ex:450: anonymous fn/6 in ExUnit.Runner.spawn_test_monitor/4 --- changelog.d/plug-test-typo.skip | 0 .../web/mastodon_api/controllers/account_controller_test.exs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/plug-test-typo.skip diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip new file mode 100644 index 000000000..e69de29bb diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index ea98b53a8..14a8eb9a0 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -2096,7 +2096,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do result = conn |> assign(:user, user) - |> get("api/v1/blocks") + |> get("/api/v1/blocks") |> json_response_and_validate_schema(200) assert [ From 848b3f5d5b6e004c51e18a4adb04d721fb89ea5c Mon Sep 17 00:00:00 2001 From: Yonle Date: Mon, 23 Feb 2026 20:57:11 +0700 Subject: [PATCH 191/317] reverse_proxy,endpoint,uploaded_media: add immutable cache-control flag --- lib/pleroma/reverse_proxy.ex | 2 +- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/uploaded_media.ex | 2 +- test/pleroma/reverse_proxy_test.exs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index bb55a4984..c7ee47c6e 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -12,7 +12,7 @@ defmodule Pleroma.ReverseProxy do @keep_resp_headers @resp_cache_headers ++ ~w(content-length content-type content-disposition content-encoding) ++ ~w(content-range accept-ranges vary) - @default_cache_control_header "public, max-age=1209600" + @default_cache_control_header "public, max-age=1209600, immutable" @valid_resp_codes [200, 206, 304] @max_read_duration :timer.seconds(30) @max_body_length :infinity diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index bab3c9fd0..13c655ee2 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Web.Plugs.HTTPSecurityPlug) plug(Pleroma.Web.Plugs.UploadedMedia) - @static_cache_control "public, max-age=1209600" + @static_cache_control "public, max-age=1209600, immutable" @static_cache_disabled "public, no-cache" # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index abacf965b..5a3ea12aa 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do # no slashes @path "media" - @default_cache_control_header "public, max-age=1209600" + @default_cache_control_header "public, max-age=1209600, immutable" def init(_opts) do static_plug_opts = diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs index 8dbe9c6bf..ec4470379 100644 --- a/test/pleroma/reverse_proxy_test.exs +++ b/test/pleroma/reverse_proxy_test.exs @@ -294,7 +294,7 @@ defmodule Pleroma.ReverseProxyTest do |> expect(:stream_body, fn _ -> :done end) conn = ReverseProxy.call(conn, "/cache") - assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers + assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers end end From 970e0f90441ff092fd6ee11ed0a87a06f4269373 Mon Sep 17 00:00:00 2001 From: Yonle Date: Mon, 2 Mar 2026 23:48:59 +0700 Subject: [PATCH 192/317] endpoint: set cache control for favicon.png Signed-off-by: Yonle --- lib/pleroma/web/endpoint.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 13c655ee2..f7c58f459 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -56,7 +56,7 @@ defmodule Pleroma.Web.Endpoint do Pleroma.Web.Plugs.InstanceStatic, at: "/", from: :pleroma, - only: ["emoji", "images"], + only: ["emoji", "images", "favicon.png"], gzip: true, cache_control_for_etags: @static_cache_control, headers: %{ From 8abd25950a483efcfa42ba113086dd6cc5730ebf Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 02:54:47 +0700 Subject: [PATCH 193/317] endpoint: use favicon plug --- lib/pleroma/web/endpoint.ex | 12 +++++- lib/pleroma/web/plugs/favicon.ex | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/plugs/favicon.ex diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index f7c58f459..e769b11b1 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.Endpoint do @static_cache_control "public, max-age=1209600, immutable" @static_cache_disabled "public, no-cache" + @favicon_cache_control "public, max=age=86400, immutable" # cache for a day # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well @@ -56,7 +57,7 @@ defmodule Pleroma.Web.Endpoint do Pleroma.Web.Plugs.InstanceStatic, at: "/", from: :pleroma, - only: ["emoji", "images", "favicon.png"], + only: ["emoji", "images"], gzip: true, cache_control_for_etags: @static_cache_control, headers: %{ @@ -73,6 +74,15 @@ defmodule Pleroma.Web.Endpoint do } ) + plug(Pleroma.Web.Plugs.Favicon, + at: "/", + only: ["favicon.png"], + cache_control_for_etags: @favicon_cache_control, + headers: %{ + "cache-control" => @favicon_cache_control + } + ) + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/", frontend_type: :primary, diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon.ex new file mode 100644 index 000000000..ad105c59a --- /dev/null +++ b/lib/pleroma/web/plugs/favicon.ex @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.Favicon do + @behaviour Plug + + @moduledoc """ + Serves favicon.png directly from the instance static directory, + bypassing the frontend-specific logic. + """ + + alias Pleroma.Web.Plugs.FrontendStatic + import Plug.Conn, only: [put_resp_header: 3] + + def init(opts) do + opts + |> Keyword.put(:from, "__unconfigured_favicon_static_plug") + |> Plug.Static.init() + end + + def call(conn, opts) do + if favicon_route?(conn.path_info) do + case find_favicon_dir(conn) do + {:ok, dir} -> + call_static(conn, opts, dir) + |> Plug.Conn.halt() + + :error -> + conn # Let the request keep going to a 404 + end + else + conn + end + end + + defp find_favicon_dir(conn) do + instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static") + instance_path = Path.join(instance_dir, "favicon.png") + + frontend_type = FrontendStatic.preferred_or_fallback(conn, :primary) + frontend_dir = FrontendStatic.file_path("", frontend_type) + frontend_path = if frontend_dir, do: Path.join(frontend_dir, "favicon.png"), else: nil + + priv_dir = Application.app_dir(:pleroma, "priv/static") + priv_path = Path.join(priv_dir, "favicon.png") + + cond do + File.exists?(instance_path) -> {:ok, instance_dir} + frontend_path && File.exists?(frontend_path) -> {:ok, frontend_dir} + File.exists?(priv_path) -> {:ok, priv_dir} + true -> :error + end + end + + defp favicon_route?(["favicon.png"]), do: true + defp favicon_route?(_), do: false + + defp call_static(conn, opts, from) do + opts = + opts + |> Map.put(:from, from) + |> Map.put(:content_types, false) + + conn = set_content_type(conn) + + Plug.Static.call(conn, opts) + end + + defp set_content_type(conn) do + put_resp_header(conn, "content-type", "image/png") + end +end From 4bc0b26abed50ca04041297f1f66467a8d812359 Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 02:57:53 +0700 Subject: [PATCH 194/317] changelog.d: add cache-control-immutable --- changelog.d/cache-control-immutable.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/cache-control-immutable.add diff --git a/changelog.d/cache-control-immutable.add b/changelog.d/cache-control-immutable.add new file mode 100644 index 000000000..516db67bf --- /dev/null +++ b/changelog.d/cache-control-immutable.add @@ -0,0 +1 @@ +Add immutable tag on cache-control header for several endpoints that's serving the same exact things. \ No newline at end of file From 0879dd39505bb9f577bd371a04f49b9beb2123ae Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 03:13:55 +0700 Subject: [PATCH 195/317] endpoint: reorder: handle favicon plug first --- lib/pleroma/web/endpoint.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index e769b11b1..2bf64a1fd 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -65,15 +65,6 @@ defmodule Pleroma.Web.Endpoint do } ) - plug(Pleroma.Web.Plugs.InstanceStatic, - at: "/", - gzip: true, - cache_control_for_etags: @static_cache_disabled, - headers: %{ - "cache-control" => @static_cache_disabled - } - ) - plug(Pleroma.Web.Plugs.Favicon, at: "/", only: ["favicon.png"], @@ -83,6 +74,15 @@ defmodule Pleroma.Web.Endpoint do } ) + plug(Pleroma.Web.Plugs.InstanceStatic, + at: "/", + gzip: true, + cache_control_for_etags: @static_cache_disabled, + headers: %{ + "cache-control" => @static_cache_disabled + } + ) + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/", frontend_type: :primary, From 96f252023e31549d934b42c098d6e62192f9f8d0 Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 22:49:54 +0700 Subject: [PATCH 196/317] constants: remove favicon.png from static_only_files --- lib/pleroma/constants.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index f78b84099..178dd6094 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Constants do const(static_only_files, do: - ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) + ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js schemas doc embed.js embed.css) ) const(status_updatable_fields, From 89751296803ba60354c16335f3422421710ddb00 Mon Sep 17 00:00:00 2001 From: Yonle Date: Thu, 5 Mar 2026 02:07:39 +0700 Subject: [PATCH 197/317] webplug(favicon): remove check on url path. --- lib/pleroma/web/plugs/favicon.ex | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon.ex index ad105c59a..32ef86cf8 100644 --- a/lib/pleroma/web/plugs/favicon.ex +++ b/lib/pleroma/web/plugs/favicon.ex @@ -20,17 +20,12 @@ defmodule Pleroma.Web.Plugs.Favicon do end def call(conn, opts) do - if favicon_route?(conn.path_info) do - case find_favicon_dir(conn) do - {:ok, dir} -> - call_static(conn, opts, dir) - |> Plug.Conn.halt() + case find_favicon_dir(conn) do + {:ok, dir} -> + call_static(conn, opts, dir) - :error -> - conn # Let the request keep going to a 404 - end - else - conn + :error -> + conn # Let the request keep going to a 404 end end @@ -53,9 +48,6 @@ defmodule Pleroma.Web.Plugs.Favicon do end end - defp favicon_route?(["favicon.png"]), do: true - defp favicon_route?(_), do: false - defp call_static(conn, opts, from) do opts = opts From d03ae43ee07a8d6467fe8b608ba69f2166a740e3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 10:28:09 +0100 Subject: [PATCH 198/317] Favicon Plug: Simplify and pass when not requesting favicon --- lib/pleroma/web/plugs/favicon.ex | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon.ex index 32ef86cf8..6564d2e00 100644 --- a/lib/pleroma/web/plugs/favicon.ex +++ b/lib/pleroma/web/plugs/favicon.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.Plugs.Favicon do bypassing the frontend-specific logic. """ - alias Pleroma.Web.Plugs.FrontendStatic import Plug.Conn, only: [put_resp_header: 3] def init(opts) do @@ -19,8 +18,8 @@ defmodule Pleroma.Web.Plugs.Favicon do |> Plug.Static.init() end - def call(conn, opts) do - case find_favicon_dir(conn) do + def call(%{request_path: "/favicon.png"} = conn, opts) do + case find_favicon_dir() do {:ok, dir} -> call_static(conn, opts, dir) @@ -29,20 +28,19 @@ defmodule Pleroma.Web.Plugs.Favicon do end end - defp find_favicon_dir(conn) do + def call(conn, _) do + conn + end + + defp find_favicon_dir() do instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static") instance_path = Path.join(instance_dir, "favicon.png") - frontend_type = FrontendStatic.preferred_or_fallback(conn, :primary) - frontend_dir = FrontendStatic.file_path("", frontend_type) - frontend_path = if frontend_dir, do: Path.join(frontend_dir, "favicon.png"), else: nil - priv_dir = Application.app_dir(:pleroma, "priv/static") priv_path = Path.join(priv_dir, "favicon.png") cond do File.exists?(instance_path) -> {:ok, instance_dir} - frontend_path && File.exists?(frontend_path) -> {:ok, frontend_dir} File.exists?(priv_path) -> {:ok, priv_dir} true -> :error end From 2388964b141bb963d1ab37ccdd00985140a2c6aa Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:37:02 +0100 Subject: [PATCH 199/317] Favicon Plug: Add tests --- test/pleroma/web/plugs/favicon_plug_test.exs | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/pleroma/web/plugs/favicon_plug_test.exs diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs new file mode 100644 index 000000000..36650e7bf --- /dev/null +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FaviconPlugTest do + use Pleroma.Web.ConnCase + + # import Mox + + # alias Pleroma.UnstubbedConfigMock, as: ConfigMock + + @dir "test/tmp/favicon_static" + + describe "default favicon" do + test "returns favicon", %{conn: conn} do + conn = get(conn, "/favicon.png") + etag = get_resp_header(conn, "etag") + + # etag changes when serving a different file + assert etag == ["\"72487CE\""] + assert response_content_type(conn, :png) + end + + test "returns correct cache-control", %{conn: conn} do + cache = + conn + |> get("/favicon.png") + |> get_resp_header("cache-control") + + assert cache == ["public, max=age=86400, immutable"] + end + end + + describe "custom favicon" do + setup do + Pleroma.Backports.mkdir_p!(@dir) + favicon_path = Path.join(@dir, "favicon.png") + + # Favicon plug should always return image/png + donor_image = "test/fixtures/image.gif" + image_stat = File.stat!(donor_image) + + # Preserve stat since that's what etag is based on + File.cp!(donor_image, favicon_path) + File.write_stat!(favicon_path, image_stat) + + clear_config([:instance, :static_dir], @dir) + + on_exit( fn -> File.rm_rf!(@dir) end) + end + + test "returns favicon", %{conn: conn} do + conn = get(conn, "/favicon.png") + etag = get_resp_header(conn, "etag") + + assert etag == ["\"215A3A4\""] + assert response_content_type(conn, :png) + end + + test "returns correct cache-control", %{conn: conn} do + cache = + conn + |> get("/favicon.png") + |> get_resp_header("cache-control") + + assert cache == ["public, max=age=86400, immutable"] + end + end +end From 662c9f36ac597c7f0bfbcdec8c12f2dfb87b8df0 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:43:55 +0100 Subject: [PATCH 200/317] Favicon Plug: Update moduledoc and rename to adhere to convention --- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/{favicon.ex => favicon_plug.ex} | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) rename lib/pleroma/web/plugs/{favicon.ex => favicon_plug.ex} (86%) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 2bf64a1fd..81a9d3a09 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -65,7 +65,7 @@ defmodule Pleroma.Web.Endpoint do } ) - plug(Pleroma.Web.Plugs.Favicon, + plug(Pleroma.Web.Plugs.FaviconPlug, at: "/", only: ["favicon.png"], cache_control_for_etags: @favicon_cache_control, diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon_plug.ex similarity index 86% rename from lib/pleroma/web/plugs/favicon.ex rename to lib/pleroma/web/plugs/favicon_plug.ex index 6564d2e00..d50a03d83 100644 --- a/lib/pleroma/web/plugs/favicon.ex +++ b/lib/pleroma/web/plugs/favicon_plug.ex @@ -2,12 +2,13 @@ # Copyright © 2017-2026 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.Plugs.Favicon do +defmodule Pleroma.Web.Plugs.FaviconPlug do @behaviour Plug @moduledoc """ - Serves favicon.png directly from the instance static directory, - bypassing the frontend-specific logic. + This is a shim to call `Plug.Static` but with runtime `from` configuration for instance favicon. + + Serves default or custom favicon.png with cacheable cache-control. """ import Plug.Conn, only: [put_resp_header: 3] From d0db1f00c3d52cab72de6db300bb99d58b1f289a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:55:02 +0100 Subject: [PATCH 201/317] Favicon Plug: assert HTTP 200 status in tests --- test/pleroma/web/plugs/favicon_plug_test.exs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs index 36650e7bf..87d955ccc 100644 --- a/test/pleroma/web/plugs/favicon_plug_test.exs +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -17,16 +17,16 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do etag = get_resp_header(conn, "etag") # etag changes when serving a different file + assert conn.status == 200 assert etag == ["\"72487CE\""] assert response_content_type(conn, :png) end test "returns correct cache-control", %{conn: conn} do - cache = - conn - |> get("/favicon.png") - |> get_resp_header("cache-control") + conn = get(conn, "/favicon.png") + cache = get_resp_header(conn, "cache-control") + assert conn.status == 200 assert cache == ["public, max=age=86400, immutable"] end end @@ -53,16 +53,16 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do conn = get(conn, "/favicon.png") etag = get_resp_header(conn, "etag") + assert conn.status == 200 assert etag == ["\"215A3A4\""] assert response_content_type(conn, :png) end test "returns correct cache-control", %{conn: conn} do - cache = - conn - |> get("/favicon.png") - |> get_resp_header("cache-control") + conn = get(conn ,"/favicon.png") + cache = get_resp_header(conn, "cache-control") + assert conn.status == 200 assert cache == ["public, max=age=86400, immutable"] end end From 5f321b0b5b10d0a3324b588b8f9af01878ecac04 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 12:49:20 +0100 Subject: [PATCH 202/317] Favicon Plug: Halt Plug pipeline when favicon not found --- lib/pleroma/web/plugs/favicon_plug.ex | 12 ++++++-- test/pleroma/web/plugs/favicon_plug_test.exs | 30 ++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon_plug.ex b/lib/pleroma/web/plugs/favicon_plug.ex index d50a03d83..1f8b891a1 100644 --- a/lib/pleroma/web/plugs/favicon_plug.ex +++ b/lib/pleroma/web/plugs/favicon_plug.ex @@ -11,7 +11,9 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do Serves default or custom favicon.png with cacheable cache-control. """ - import Plug.Conn, only: [put_resp_header: 3] + import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1] + + require Logger def init(opts) do opts @@ -25,7 +27,12 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do call_static(conn, opts, dir) :error -> - conn # Let the request keep going to a 404 + # Favicon should always be available and this should never occur. + # If it does, halt the pipeline before having unintended side-effects. + Logger.error("No favicon.png found! Is the default favicon deleted?") + conn + |> send_resp(404, "Not found") + |> halt() end end @@ -54,7 +61,6 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do |> Map.put(:content_types, false) conn = set_content_type(conn) - Plug.Static.call(conn, opts) end diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs index 87d955ccc..62926e041 100644 --- a/test/pleroma/web/plugs/favicon_plug_test.exs +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -5,20 +5,21 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do use Pleroma.Web.ConnCase - # import Mox - - # alias Pleroma.UnstubbedConfigMock, as: ConfigMock - @dir "test/tmp/favicon_static" + setup do + Pleroma.Backports.mkdir_p!(@dir) + + on_exit(fn -> File.rm_rf!(@dir) end) + end + describe "default favicon" do test "returns favicon", %{conn: conn} do conn = get(conn, "/favicon.png") - etag = get_resp_header(conn, "etag") + body_size = byte_size(conn.resp_body) - # etag changes when serving a different file assert conn.status == 200 - assert etag == ["\"72487CE\""] + assert body_size == 1583 assert response_content_type(conn, :png) end @@ -33,28 +34,21 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do describe "custom favicon" do setup do - Pleroma.Backports.mkdir_p!(@dir) favicon_path = Path.join(@dir, "favicon.png") + donor_image = "test/fixtures/image.png" - # Favicon plug should always return image/png - donor_image = "test/fixtures/image.gif" - image_stat = File.stat!(donor_image) - - # Preserve stat since that's what etag is based on File.cp!(donor_image, favicon_path) - File.write_stat!(favicon_path, image_stat) - clear_config([:instance, :static_dir], @dir) - on_exit( fn -> File.rm_rf!(@dir) end) + on_exit(fn -> File.rm!(favicon_path) end) end test "returns favicon", %{conn: conn} do conn = get(conn, "/favicon.png") - etag = get_resp_header(conn, "etag") + body_size = byte_size(conn.resp_body) assert conn.status == 200 - assert etag == ["\"215A3A4\""] + assert body_size == 104426 assert response_content_type(conn, :png) end From 3760480813745c0f2d0b2fe1a8642400308e2be2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:48:37 +0100 Subject: [PATCH 203/317] lint --- lib/pleroma/web/plugs/favicon_plug.ex | 3 ++- test/pleroma/web/plugs/favicon_plug_test.exs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon_plug.ex b/lib/pleroma/web/plugs/favicon_plug.ex index 1f8b891a1..e2e1f1adb 100644 --- a/lib/pleroma/web/plugs/favicon_plug.ex +++ b/lib/pleroma/web/plugs/favicon_plug.ex @@ -30,6 +30,7 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do # Favicon should always be available and this should never occur. # If it does, halt the pipeline before having unintended side-effects. Logger.error("No favicon.png found! Is the default favicon deleted?") + conn |> send_resp(404, "Not found") |> halt() @@ -40,7 +41,7 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do conn end - defp find_favicon_dir() do + defp find_favicon_dir do instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static") instance_path = Path.join(instance_dir, "favicon.png") diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs index 62926e041..520501250 100644 --- a/test/pleroma/web/plugs/favicon_plug_test.exs +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -8,9 +8,9 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do @dir "test/tmp/favicon_static" setup do - Pleroma.Backports.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf!(@dir) end) + on_exit(fn -> File.rm_rf!(@dir) end) end describe "default favicon" do @@ -48,12 +48,12 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do body_size = byte_size(conn.resp_body) assert conn.status == 200 - assert body_size == 104426 + assert body_size == 104_426 assert response_content_type(conn, :png) end test "returns correct cache-control", %{conn: conn} do - conn = get(conn ,"/favicon.png") + conn = get(conn, "/favicon.png") cache = get_resp_header(conn, "cache-control") assert conn.status == 200 From 499b2ed11810c418d5d42334f1e98aec28f93f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 17:23:54 +0100 Subject: [PATCH 204/317] remove unused alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/pleroma_api/controllers/util_controller.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 70b0b9cf6..882e9476e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do require Logger - alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Emoji alias Pleroma.Healthcheck From 87b4e3f3ff96e0860421562c0b956dbd3d716426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 14:27:27 +0100 Subject: [PATCH 205/317] Avoid code duplication in UserView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/user-view.ignore | 1 + .../web/activity_pub/views/user_view.ex | 74 ++++++++----------- 2 files changed, 32 insertions(+), 43 deletions(-) create mode 100644 changelog.d/user-view.ignore diff --git a/changelog.d/user-view.ignore b/changelog.d/user-view.ignore new file mode 100644 index 000000000..37e9a7e09 --- /dev/null +++ b/changelog.d/user-view.ignore @@ -0,0 +1 @@ +Avoid code duplication in UserView diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4362db324..9eb8bb849 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -35,32 +35,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) - - endpoints = render("endpoints.json", %{user: user}) - - %{ - "id" => user.ap_id, + Map.merge(common_actor_fields(user), %{ "type" => "Application", - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", - "inbox" => "#{user.ap_id}/inbox", - "outbox" => "#{user.ap_id}/outbox", "name" => "Pleroma", "summary" => "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", - "url" => user.ap_id, "manuallyApprovesFollowers" => false, - "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", - "owner" => user.ap_id, - "publicKeyPem" => public_key - }, - "endpoints" => endpoints, "invisible" => User.invisible?(user) - } + }) |> Map.merge(Utils.make_json_ld_header()) end @@ -77,13 +59,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do end def render("user.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) user = User.sanitize_html(user) - endpoints = render("endpoints.json", %{user: user}) - emoji_tags = Transmogrifier.take_emoji_tags(user) fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) @@ -102,25 +79,9 @@ defmodule Pleroma.Web.ActivityPub.UserView do do: Date.to_iso8601(user.birthday), else: nil - %{ - "id" => user.ap_id, - "type" => user.actor_type, - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", - "inbox" => "#{user.ap_id}/inbox", - "outbox" => "#{user.ap_id}/outbox", + Map.merge(common_actor_fields(user), %{ "featured" => "#{user.ap_id}/collections/featured", "preferredUsername" => user.nickname, - "name" => user.name, - "summary" => user.bio, - "url" => user.ap_id, - "manuallyApprovesFollowers" => user.is_locked, - "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", - "owner" => user.ap_id, - "publicKeyPem" => public_key - }, - "endpoints" => endpoints, "attachment" => fields, "tag" => emoji_tags, # Note: key name is indeed "discoverable" (not an error) @@ -130,7 +91,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "vcard:bday" => birthday, "webfinger" => "acct:#{User.full_nickname(user)}", "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at) - } + }) |> Map.merge( maybe_make_image( &User.avatar_url/2, @@ -309,6 +270,33 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end + defp common_actor_fields(%User{} = user) do + endpoints = render("endpoints.json", %{user: user}) + + {:ok, _, public_key} = Keys.keys_from_pem(user.keys) + public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) + public_key = :public_key.pem_encode([public_key]) + + %{ + "id" => user.ap_id, + "type" => user.actor_type, + "following" => "#{user.ap_id}/following", + "followers" => "#{user.ap_id}/followers", + "inbox" => "#{user.ap_id}/inbox", + "outbox" => "#{user.ap_id}/outbox", + "name" => user.name, + "summary" => user.bio, + "url" => user.ap_id, + "manuallyApprovesFollowers" => user.is_locked, + "endpoints" => endpoints, + "publicKey" => %{ + "id" => "#{user.ap_id}#main-key", + "owner" => user.ap_id, + "publicKeyPem" => public_key + } + } + end + defp maybe_put_total_items(map, false, _total), do: map defp maybe_put_total_items(map, true, total) do From 0592f111f6bc337256786a999252fe07bb80d4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 17:28:47 +0100 Subject: [PATCH 206/317] update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../integration/mastodon_websocket_test.exs | 2 +- .../pleroma/web/activity_pub/activity_pub_test.exs | 2 +- test/pleroma/web/activity_pub/publisher_test.exs | 2 +- .../web/activity_pub/transmogrifier_test.exs | 2 +- test/pleroma/web/activity_pub/visibility_test.exs | 2 +- test/pleroma/web/common_api/utils_test.exs | 4 ++-- test/pleroma/web/common_api_test.exs | 2 +- .../controllers/account_controller_test.exs | 2 +- .../controllers/timeline_controller_test.exs | 12 ++++++------ .../web/mastodon_api/views/list_view_test.exs | 14 +++++++++----- .../web/mastodon_api/views/status_view_test.exs | 2 +- test/pleroma/web/streamer_test.exs | 10 +++++----- 12 files changed, 30 insertions(+), 26 deletions(-) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 88f32762d..47f6f5f76 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -363,7 +363,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do test "accepts the 'list' stream", %{token: token, user: user} do posting_user = insert(:user) - {:ok, list} = Pleroma.List.create("test", user) + {:ok, list} = Pleroma.List.create(%{title: "test"}, user) Pleroma.List.follow(list, posting_user) assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}") diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 73f53db56..a390cdfa8 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1754,7 +1754,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do test "fetch_activities/2 returns activities addressed to a list " do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 1f9e0bfe5..c0908655a 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -334,7 +334,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do test "activity with BCC is published to a list member." do actor = insert(:user) - {:ok, list} = Pleroma.List.create("list", actor) + {:ok, list} = Pleroma.List.create(%{title: "list"}, actor) list_member = insert(:user, %{local: false}) Pleroma.List.follow(list, list_member) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index b54196a3c..1e9be54ea 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -581,7 +581,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do test "it strips BCC field" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/activity_pub/visibility_test.exs b/test/pleroma/web/activity_pub/visibility_test.exs index fd3dc83a1..f92f2df16 100644 --- a/test/pleroma/web/activity_pub/visibility_test.exs +++ b/test/pleroma/web/activity_pub/visibility_test.exs @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do following = insert(:user) unrelated = insert(:user) {:ok, following, user} = Pleroma.User.follow(following, user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) Pleroma.List.follow(list, unrelated) diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index 27b1da1e3..d0cbc3111 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -647,7 +647,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do describe "maybe_add_list_data/3" do test "adds list params when found user list" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == %{ @@ -658,7 +658,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do test "returns original params when list not found" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, insert(:user)) assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == %{additional: %{}, object: %{}} diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 4eb057712..017fac696 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -759,7 +759,7 @@ defmodule Pleroma.Web.CommonAPITest do test "it allows to address a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index ea98b53a8..fb84569bc 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1815,7 +1815,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do test "returns lists to which the account belongs" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create("Test List", user) + assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create(%{title: "Test List"}, user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) assert [%{"id" => _list_id, "title" => "Test List"}] = diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 9808652d2..b685f4ff8 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -631,7 +631,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."}) {:ok, _} = CommonAPI.repeat(activity_one.id, other_user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = get(conn, "/api/v1/timelines/list/#{list.id}") @@ -643,7 +643,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do test "works with pagination", %{user: user, conn: conn} do other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) Enum.each(1..30, fn i -> @@ -669,7 +669,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do other_user = insert(:user) {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = get(conn, "/api/v1/timelines/list/#{list.id}") @@ -692,7 +692,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do visibility: "private" }) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = get(conn, "/api/v1/timelines/list/#{list.id}") @@ -710,7 +710,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do {:ok, _} = CommonAPI.react_with_emoji(activity.id, user3, "🎅") User.mute(user, user3) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, user2) result = @@ -741,7 +741,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do end test "filtering", %{user: user, conn: conn} do - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) local_user = insert(:user) {:ok, local_activity} = CommonAPI.post(local_user, %{status: "Marisa is stupid."}) diff --git a/test/pleroma/web/mastodon_api/views/list_view_test.exs b/test/pleroma/web/mastodon_api/views/list_view_test.exs index bbf87bab2..ae0593b6b 100644 --- a/test/pleroma/web/mastodon_api/views/list_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/list_view_test.exs @@ -10,11 +10,12 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do test "show" do user = insert(:user) title = "mortal enemies" - {:ok, list} = Pleroma.List.create(title, user) + {:ok, list} = Pleroma.List.create(%{title: title}, user) expected = %{ id: to_string(list.id), - title: title + title: title, + exclusive: false } assert expected == ListView.render("show.json", %{list: list}) @@ -23,10 +24,13 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do test "index" do user = insert(:user) - {:ok, list} = Pleroma.List.create("my list", user) - {:ok, list2} = Pleroma.List.create("cofe", user) + {:ok, list} = Pleroma.List.create(%{title: "my list", exclusive: false}, user) + {:ok, list2} = Pleroma.List.create(%{title: "cofe", exclusive: true}, user) - assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = + assert [ + %{id: _, title: "my list", exclusive: false}, + %{id: _, title: "cofe", exclusive: true} + ] = ListView.render("index.json", lists: [list, list2]) end 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 73cab817b..76123cd0f 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -909,7 +909,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do test "visibility/list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index f5008f6b9..b26bd1847 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -220,7 +220,7 @@ defmodule Pleroma.Web.StreamerTest do } do %{token: read_lists_token} = oauth_access(["read:lists"], user: user) %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) - {:ok, list} = List.create("Test", user) + {:ok, list} = List.create(%{title: "Test"}, user) assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) @@ -233,7 +233,7 @@ defmodule Pleroma.Web.StreamerTest do test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do another_user = insert(:user) - {:ok, list} = List.create("Test", another_user) + {:ok, list} = List.create(%{title: "Test"}, another_user) assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) @@ -803,7 +803,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, user_a, user_b} = User.follow(user_a, user_b) - {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.create(%{title: "Test"}, user_a) {:ok, list} = List.follow(list, user_b) Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) @@ -820,7 +820,7 @@ defmodule Pleroma.Web.StreamerTest do test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do user_b = insert(:user) - {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.create(%{title: "Test"}, user_a) {:ok, list} = List.follow(list, user_b) Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) @@ -839,7 +839,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, user_a, user_b} = User.follow(user_a, user_b) - {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.create(%{title: "Test"}, user_a) {:ok, list} = List.follow(list, user_b) Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) From 4e1ba489ec087b980e61ad705ab966953946ee90 Mon Sep 17 00:00:00 2001 From: shibao Date: Sun, 8 Mar 2026 11:16:33 +0000 Subject: [PATCH 207/317] fix 404s for missing static files --- lib/pleroma/web/plugs/instance_static.ex | 35 +++++++++++++-- .../web/plugs/instance_static_test.exs | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index d2a674d39..948188287 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do require Pleroma.Constants - import Plug.Conn, only: [put_resp_header: 3] + import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1] @moduledoc """ This is a shim to call `Plug.Static` but with runtime `from` configuration. @@ -51,10 +51,37 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do |> Map.put(:from, from) |> Map.put(:content_types, false) - conn = set_content_type(conn, conn.request_path) + conn = + conn + |> set_content_type(conn.request_path) + |> Plug.Static.call(opts) - # Call Plug.Static with our sanitized content-type - Plug.Static.call(conn, opts) + if conn.halted do + conn + else + path = String.trim_leading(conn.request_path, "/") + + if not File.exists?(file_path(path)) do + conn + |> put_status(:not_found) + |> send_404() + |> halt() + else + conn + end + end + end + + defp send_404(conn) do + if String.ends_with?(String.downcase(conn.request_path), ".json") do + conn + |> put_resp_header("content-type", "application/json") + |> send_resp(404, Jason.encode!(%{error: "not found"})) + else + conn + |> put_resp_header("content-type", "text/plain") + |> send_resp(404, "Not found") + end end defp set_content_type(conn, "/emoji/" <> filepath) do diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs index b5a5a3334..017c49d1e 100644 --- a/test/pleroma/web/plugs/instance_static_test.exs +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -137,4 +137,47 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do # It should be preserved because "image" is in the allowed_mime_types list assert content_type == "image/jpeg" end + + describe "404s for missing files in static-only paths" do + test "returns 404 for non-existent static-only JSON files" do + conn = get(build_conn(), "/static/non-existent.json") + + assert conn.status == 404 + assert ["application/json"] = get_resp_header(conn, "content-type") + assert Jason.decode!(conn.resp_body) == %{"error" => "not found"} + end + + test "returns 404 for non-existent static-only non-JSON files" do + conn = get(build_conn(), "/static/non-existent.txt") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 404 for non-existent .css files" do + conn = get(build_conn(), "/static/non-existent.css") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + # Verifies that we forced text/plain for the error body, even though the path was .css + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 404 for non-existent files without an extension" do + conn = get(build_conn(), "/static/non-existent") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 200 (falls through to SPA) for non-static-only paths" do + # /some-route is NOT in static_only_files, so it should still fall through to the SPA. + conn = get(build_conn(), "/some-route") + + assert conn.status == 200 + assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type") + end + end end From bceb28b9416c34e066c31d5bbf4dac4366aa98ff Mon Sep 17 00:00:00 2001 From: shibao Date: Sun, 8 Mar 2026 11:19:14 +0000 Subject: [PATCH 208/317] add changelog note for missing static files fix --- changelog.d/missing-static-file.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/missing-static-file.fix diff --git a/changelog.d/missing-static-file.fix b/changelog.d/missing-static-file.fix new file mode 100644 index 000000000..c7ef805aa --- /dev/null +++ b/changelog.d/missing-static-file.fix @@ -0,0 +1 @@ +Fix 404 error codes for missing static files From a0131ff7338b34103e63a635f3778e5b2c58d8e9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 8 Mar 2026 21:19:49 +0100 Subject: [PATCH 209/317] credo: fix ordering of aliases missed in pleroma/pleroma!7841 --- changelog.d/credo-aliases-sort-fixes.skip | 0 lib/pleroma/web/o_auth/token_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/token_controller.ex | 2 +- lib/pleroma/web/preload/providers/instance.ex | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog.d/credo-aliases-sort-fixes.skip diff --git a/changelog.d/credo-aliases-sort-fixes.skip b/changelog.d/credo-aliases-sort-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/o_auth/token_controller.ex b/lib/pleroma/web/o_auth/token_controller.ex index 37d08eea3..d2313a9cc 100644 --- a/lib/pleroma/web/o_auth/token_controller.ex +++ b/lib/pleroma/web/o_auth/token_controller.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.OAuth.TokenController do alias Pleroma.User alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.OAuth.TokenView + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex index 581dd569a..10eeb907e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.PleromaAPI.TokenController do alias Pleroma.User alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.PleromaAPI.TokenView + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex index d5417af30..f2a60393f 100644 --- a/lib/pleroma/web/preload/providers/instance.ex +++ b/lib/pleroma/web/preload/providers/instance.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo + alias Pleroma.Web.PleromaAPI.UtilView alias Pleroma.Web.Plugs.InstanceStatic alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.PleromaAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" From 23cc812366cc6ec765bcad008a5165bdd8101a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 19 Mar 2026 15:19:51 +0100 Subject: [PATCH 210/317] Restore embed route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk Assisted-by: your mother Signed-off-by: nicole mikołajczyk --- changelog.d/restore-embeds.fix | 1 + lib/pleroma/web/embed_controller.ex | 1 + lib/pleroma/web/router.ex | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 changelog.d/restore-embeds.fix diff --git a/changelog.d/restore-embeds.fix b/changelog.d/restore-embeds.fix new file mode 100644 index 000000000..5a2a1c4fe --- /dev/null +++ b/changelog.d/restore-embeds.fix @@ -0,0 +1 @@ +Restore embeds route diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex index 2ca4501a6..8420f17a5 100644 --- a/lib/pleroma/web/embed_controller.ex +++ b/lib/pleroma/web/embed_controller.ex @@ -20,6 +20,7 @@ defmodule Pleroma.Web.EmbedController do conn |> delete_resp_header("x-frame-options") |> delete_resp_header("content-security-policy") + |> put_layout({Pleroma.Web.LayoutView, :embed}) |> render("show.html", activity: activity, author: User.sanitize_html(author), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index eea0a0912..40c6f6d68 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1032,6 +1032,8 @@ defmodule Pleroma.Web.Router do pipe_through(:pleroma_html) post("/auth/password", OAuth.PasswordController, :request) + + get("/embed/:id", EmbedController, :show) end scope "/proxy/", Pleroma.Web do From dfaabb48ef162a07302580ee60f014fcf5cffd59 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:03:54 +0200 Subject: [PATCH 211/317] Elixir 1.19: Fix typing violation on struct updates in Pleroma.Marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Marker is expected on struct update: %Pleroma.Marker{marker | user: user} but got type: dynamic() where "marker" was given the type: # type: dynamic() # from: lib/pleroma/marker.ex {:ok, marker} when defining the variable "marker", you must also pattern match on "%Pleroma.Marker{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 81 │ {:ok, marker} -> %__MODULE__{marker | user: user} │ ~ │ └─ lib/pleroma/marker.ex:81:24: Pleroma.Marker.get_marker/2 --- lib/pleroma/marker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 68b054e4d..4f645240e 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Marker do defp get_marker(user, timeline) do case Repo.find_resource(get_query(user, timeline)) do - {:ok, marker} -> %__MODULE__{marker | user: user} + {:ok, %__MODULE__{} = marker} -> %__MODULE__{marker | user: user} _ -> %__MODULE__{timeline: timeline, user_id: user.id} end end From 1b9cd83d88776f4bc04f15628406d1fdd2850d9c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:06:02 +0200 Subject: [PATCH 212/317] Elixir 1.19: Fix typing violation on struct updates in MFA.Changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.MFA.Settings is expected on struct update: %Pleroma.MFA.Settings{settings | enabled: false} but got type: dynamic() where "settings" was given the type: # type: dynamic() # from: lib/pleroma/mfa/changeset.ex:11:14 settings = Pleroma.MFA.fetch_settings(Ecto.Changeset.apply_changes(changeset)) when defining the variable "settings", you must also pattern match on "%Pleroma.MFA.Settings{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 17 │ put_change(changeset, %Settings{settings | enabled: false}) │ ~ │ └─ lib/pleroma/mfa/changeset.ex:17:29: Pleroma.MFA.Changeset.disable/2 --- warning: a struct for Pleroma.MFA.Settings is expected on struct update: %Pleroma.MFA.Settings{ settings | totp: %Pleroma.MFA.Settings.TOTP{confirmed: false, delivery_type: "app", secret: nil} } but got type: dynamic() where "settings" was given the type: # type: dynamic() # from: lib/pleroma/mfa/changeset.ex:23:74 %Pleroma.User{multi_factor_authentication_settings: settings} = user when defining the variable "settings", you must also pattern match on "%Pleroma.MFA.Settings{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 25 │ |> put_change(%Settings{settings | totp: %Settings.TOTP{}}) │ ~ │ └─ lib/pleroma/mfa/changeset.ex:25:19: Pleroma.MFA.Changeset.disable_totp/1 --- lib/pleroma/mfa/changeset.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 3ec3cfe91..890cb2193 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -8,7 +8,7 @@ defmodule Pleroma.MFA.Changeset do alias Pleroma.User def disable(%Ecto.Changeset{} = changeset, force \\ false) do - settings = + %Settings{} = settings = changeset |> Ecto.Changeset.apply_changes() |> MFA.fetch_settings() @@ -20,20 +20,20 @@ defmodule Pleroma.MFA.Changeset do end end - def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do + def disable_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do user |> put_change(%Settings{settings | totp: %Settings.TOTP{}}) end - def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do - totp_settings = %Settings.TOTP{settings.totp | confirmed: true} + def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do + totp_settings = %Settings.TOTP{%Settings.TOTP{} = settings.totp | confirmed: true} user |> put_change(%Settings{settings | totp: totp_settings, enabled: true}) end def setup_totp(%User{} = user, attrs) do - mfa_settings = MFA.fetch_settings(user) + %Settings{} = mfa_settings = MFA.fetch_settings(user) totp_settings = %Settings.TOTP{} @@ -46,7 +46,7 @@ defmodule Pleroma.MFA.Changeset do def cast_backup_codes(%User{} = user, codes) do user |> put_change(%Settings{ - user.multi_factor_authentication_settings + %Settings{} = user.multi_factor_authentication_settings | backup_codes: codes }) end From 5b6af83e8658905420bc9d4f3b25a34667c39d39 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:09:03 +0200 Subject: [PATCH 213/317] Elixir 1.19: Fix typing violation on struct updates in Pleroma.Upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Upload is expected on struct update: %Pleroma.Upload{ upload | path: case upload.path do x when x === false or x === nil -> <> x -> x end } but got type: dynamic() where "upload" was given the type: # type: dynamic() # from: lib/pleroma/upload.ex:95:24 {:ok, upload} <- prepare_upload(upload, opts) when defining the variable "upload", you must also pattern match on "%Pleroma.Upload{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 96 │ upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, │ ~ │ └─ lib/pleroma/upload.ex:96:19: Pleroma.Upload.store/2 --- lib/pleroma/upload.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 06d8005bc..350ff6cb3 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -93,7 +93,7 @@ defmodule Pleroma.Upload do def store(upload, opts \\ []) do opts = get_opts(opts) - with {:ok, upload} <- prepare_upload(upload, opts), + with {:ok, %__MODULE__{} = upload} <- prepare_upload(upload, opts), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), description = get_description(upload), From 19e05b4a7bc55447598e11bf140785ed5e3435f8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:09:51 +0200 Subject: [PATCH 214/317] Elixir 1.19: Fix typing violation on struct updates in Web.ApiSpec.Cast* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Plug.Conn is expected on struct update: %Plug.Conn{conn | query_params: query_params} but got type: dynamic() where "conn" was given the type: # type: dynamic() # from: lib/pleroma/web/api_spec/cast_and_validate.ex:109:43 conn when defining the variable "conn", you must also pattern match on "%Plug.Conn{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 133 │ conn = %Conn{conn | query_params: query_params} │ ~ │ └─ lib/pleroma/web/api_spec/cast_and_validate.ex:133:16: Pleroma.Web.ApiSpec.CastAndValidate.cast_and_validate/6 --- lib/pleroma/web/api_spec/cast_and_validate.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index 672d1c4a1..57ea8e9b3 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) end - defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do + defp cast_and_validate(spec, operation, %Conn{} = conn, content_type, false = _strict, cast_opts) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do {:ok, conn} -> {:ok, conn} From 958d250fe52711e3d69bba0eba77b06075ee8a80 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:11:33 +0200 Subject: [PATCH 215/317] Elixir 1.19: Fix typing violation on struct updates in Web.ApiSpec.Rend* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for OpenApiSpex.Cast.Error is expected on struct update: %OpenApiSpex.Cast.Error{err | name: err.value} but got type: dynamic(%{..., name: nil, reason: :invalid_enum}) where "err" was given the type: # type: dynamic(%{..., name: nil, reason: :invalid_enum}) # from: lib/pleroma/web/api_spec/render_error.ex:20:45 %{name: nil, reason: :invalid_enum} = err when defining the variable "err", you must also pattern match on "%OpenApiSpex.Cast.Error{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 21 │ %OpenApiSpex.Cast.Error{err | name: err.value} │ ~ │ └─ lib/pleroma/web/api_spec/render_error.ex:21:11: Pleroma.Web.ApiSpec.RenderError.call/2 --- lib/pleroma/web/api_spec/render_error.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index 3539af6e4..acf510774 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -17,10 +17,10 @@ defmodule Pleroma.Web.ApiSpec.RenderError do def call(conn, errors) do errors = Enum.map(errors, fn - %{name: nil, reason: :invalid_enum} = err -> + %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err -> %OpenApiSpex.Cast.Error{err | name: err.value} - %{name: nil} = err -> + %OpenApiSpex.Cast.Error{name: nil} = err -> %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} err -> From 8417629b4b51a378e2dd6788e7f43512e97734bc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:12:41 +0200 Subject: [PATCH 216/317] Elixir 1.19: Fix typing violation on struct updates in CommonAPI.Activity* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Web.CommonAPI.ActivityDraft is expected on struct update: %Pleroma.Web.CommonAPI.ActivityDraft{draft | object: object} but got type: dynamic() where "draft" was given the type: # type: dynamic() # from: lib/pleroma/web/common_api/activity_draft.ex:91:22 draft when defining the variable "draft", you must also pattern match on "%Pleroma.Web.CommonAPI.ActivityDraft{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 102 │ %__MODULE__{draft | object: object} │ ~ │ └─ lib/pleroma/web/common_api/activity_draft.ex:102:5: Pleroma.Web.CommonAPI.ActivityDraft.listen_object/1 --- lib/pleroma/web/common_api/activity_draft.ex | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c0b98508c..c5959276a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> validate() end - defp listen_object(draft) do + defp listen_object(%__MODULE__{} = draft) do object = draft.params |> Map.take([:album, :artist, :title, :length]) @@ -102,20 +102,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | object: object} end - defp put_params(draft, params) do + defp put_params(%__MODULE__{} = draft, params) do params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) %__MODULE__{draft | params: params} end - defp status(%{params: %{status: status}} = draft) do + defp status(%__MODULE__{params: %{status: status}} = draft) do %__MODULE__{draft | status: String.trim(status)} end - defp summary(%{params: params} = draft) do + defp summary(%__MODULE__{params: params} = draft) do %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} end - defp full_payload(%{status: status, summary: summary} = draft) do + defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do full_payload = String.trim(status <> summary) case Utils.validate_character_limit(full_payload, draft.attachments) do @@ -124,7 +124,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp attachments(%{params: params} = draft) do + defp attachments(%__MODULE__{params: params} = draft) do attachments = Utils.attachments_from_ids(params, draft.user) draft = %__MODULE__{draft | attachments: attachments} @@ -134,9 +134,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do # If a post was deleted all its activities (except the newly added Delete) are purged too, # thus lookup by Create db ID will yield nil just as if it never existed in the first place. # @@ -166,13 +166,13 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do %__MODULE__{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft - defp quote_post(%{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} @@ -188,12 +188,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quote_post(draft), do: draft - defp in_reply_to_conversation(draft) do + defp in_reply_to_conversation(%__MODULE__{} = 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} end - defp visibility(%{params: params} = draft) do + defp visibility(%__MODULE__{params: params} = draft) do case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do {visibility, "direct"} when visibility != "direct" -> add_error(draft, dgettext("errors", "The message visibility must be direct")) @@ -226,14 +226,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quoting_visibility(draft), do: draft - defp expires_at(draft) do + defp expires_at(%__MODULE__{} = draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end end - defp poll(draft) do + defp poll(%__MODULE__{} = draft) do case Utils.make_poll_data(draft.params) do {:ok, {poll, poll_emoji}} -> %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} @@ -243,7 +243,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp content(%{mentions: mentions} = draft) do + defp content(%__MODULE__{mentions: mentions} = draft) do {content_html, mentioned_users, tags} = Utils.make_content_html(draft) mentioned_ap_ids = @@ -257,22 +257,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} end - defp to_and_cc(draft) do + defp to_and_cc(%__MODULE__{} = draft) do {to, cc} = Utils.get_to_and_cc(draft) %__MODULE__{draft | to: to, cc: cc} end - defp context(draft) do + defp context(%__MODULE__{} = draft) do context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) %__MODULE__{draft | context: context} end - defp sensitive(draft) do + defp sensitive(%__MODULE__{} = draft) do sensitive = draft.params[:sensitive] %__MODULE__{draft | sensitive: sensitive} end - defp language(draft) do + defp language(%__MODULE__{} = draft) do language = with language <- draft.params[:language], true <- good_locale_code?(language) do @@ -284,7 +284,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | language: language} end - defp object(draft) do + defp object(%__MODULE__{} = draft) do emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji) # Sometimes people create posts with subject containing emoji, @@ -328,12 +328,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | object: object} end - defp preview?(draft) do + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %__MODULE__{draft | preview?: preview?} end - defp changes(draft) do + defp changes(%__MODULE__{} = draft) do direct? = draft.visibility == "direct" additional = %{"cc" => draft.cc, "directMessage" => direct?} @@ -359,7 +359,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(draft, _func), do: draft - defp add_error(draft, message) do + defp add_error(%__MODULE__{} = draft, message) do %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} end From 93e8f9d7d1dcc4fca58538616d79538fe30d48f1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:40:19 +0100 Subject: [PATCH 217/317] Elixir 1.19: Fix typing violations in ActivityPubTest --- test/pleroma/web/activity_pub/activity_pub_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 37ec87a9b..13146619a 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1524,8 +1524,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do %{test_file: test_file} end - test "strips / from filename", %{test_file: file} do - file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"} + test "strips / from filename", %{test_file: %Plug.Upload{} = file} do + file = %{file | filename: "../../../../../nested/bad.jpg"} {:ok, %Object{} = object} = ActivityPub.upload(file) [%{"href" => href}] = object.data["url"] assert Regex.match?(~r"/bad.jpg$", href) @@ -1786,10 +1786,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + {:ok, %Activity{} = activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) activity = Repo.preload(activity, :bookmark) - activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} + activity = %{activity | thread_muted?: !!activity.thread_muted?} assert ActivityPub.fetch_activities([], %{user: user}) == [activity] end @@ -1989,7 +1989,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert User.following?(follower, old_user) assert User.following?(follower_move_opted_out, old_user) - assert {:ok, activity} = ActivityPub.move(old_user, new_user) + assert {:ok, %Activity{} = activity} = ActivityPub.move(old_user, new_user) assert %Activity{ actor: ^old_ap_id, @@ -2021,7 +2021,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert User.following?(follower_move_opted_out, old_user) refute User.following?(follower_move_opted_out, new_user) - activity = %Activity{activity | object: nil} + activity = %{activity | object: nil} assert [%Notification{activity: ^activity}] = Notification.for_user(follower) From b8a66c22b369fe52ed2305ee37828cd4f4060b79 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:42:26 +0100 Subject: [PATCH 218/317] Elixir 1.19: Fix typing violation in MediaControllerTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Plug.Upload is expected on struct update: %Plug.Upload{image | filename: "../../../../../nested/file.jpg"} but got type: dynamic() where "image" was given the type: # type: dynamic() # from: test/pleroma/web/mastodon_api/controllers/media_controller_test.exs:132:42 %{conn: conn, image: image} when defining the variable "image", you must also pattern match on "%Plug.Upload{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 133 │ image = %Plug.Upload{ │ ~ │ └─ test/pleroma/web/mastodon_api/controllers/media_controller_test.exs:133:15: Pleroma.Web.MastodonAPI.MediaControllerTest."test Upload media Do not allow nested filename"/1 --- .../web/mastodon_api/controllers/media_controller_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs index ae86078d7..fc083fd0e 100644 --- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -129,8 +129,8 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) end - test "Do not allow nested filename", %{conn: conn, image: image} do - image = %Plug.Upload{ + test "Do not allow nested filename", %{conn: conn, image: %Plug.Upload{} = image} do + image = %{ image | filename: "../../../../../nested/file.jpg" } From ec294b30c1beaab4ef88f3195de36bbbc88b9c24 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:44:37 +0100 Subject: [PATCH 219/317] Elixir 1.19: Fix typing violation in RepoTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Web.OAuth.Token is expected on struct update: %Pleroma.Web.OAuth.Token{Pleroma.Factory.insert(:oauth_token) | user: user} but got type: dynamic() you must assign "Pleroma.Factory.insert(:oauth_token)" to variable and pattern match on "%Pleroma.Web.OAuth.Token{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 27 │ token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} │ ~ │ └─ test/pleroma/repo_test.exs:27:15: Pleroma.RepoTest."test get_assoc/2 get assoc from preloaded data"/1 --- test/pleroma/repo_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/repo_test.exs b/test/pleroma/repo_test.exs index 9c0f5d028..721175bda 100644 --- a/test/pleroma/repo_test.exs +++ b/test/pleroma/repo_test.exs @@ -24,7 +24,8 @@ defmodule Pleroma.RepoTest do describe "get_assoc/2" do test "get assoc from preloaded data" do user = %User{name: "Agent Smith"} - token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + %Pleroma.Web.OAuth.Token{} = token = insert(:oauth_token) + token = %{token | user: user} assert Repo.get_assoc(token, :user) == {:ok, user} end From f4c28392e1841dd3789a53f553a1f3d47510ddd3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:48:55 +0100 Subject: [PATCH 220/317] Elixir 1.19: Fix typing violation in MarkerTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Marker is expected on struct update: %Pleroma.Marker{refresh_record(marker) | unread_count: 2} but got type: dynamic() where "marker" was given the type: # type: dynamic() # from: test/pleroma/marker_test.exs:35:14 marker = Pleroma.Factory.insert(:marker, user: user) you must assign "refresh_record(marker)" to variable and pattern match on "%Pleroma.Marker{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 43 │ ) == [%Marker{refresh_record(marker) | unread_count: 2}] │ ~ │ └─ test/pleroma/marker_test.exs:43:20: Pleroma.MarkerTest."test get_markers/2 returns user markers"/1 --- test/pleroma/marker_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/marker_test.exs b/test/pleroma/marker_test.exs index 819dde9be..7f573ac7a 100644 --- a/test/pleroma/marker_test.exs +++ b/test/pleroma/marker_test.exs @@ -36,11 +36,12 @@ defmodule Pleroma.MarkerTest do insert(:notification, user: user, activity: insert(:note_activity)) insert(:notification, user: user, activity: insert(:note_activity)) insert(:marker, timeline: "home", user: user) + %Marker{} = refreshed_marker = refresh_record(marker) assert Marker.get_markers( user, ["notifications"] - ) == [%Marker{refresh_record(marker) | unread_count: 2}] + ) == [%{refreshed_marker | unread_count: 2}] end end From f60a317c2faee2bda15b1d1fc4c6cc9ff7ff8fb3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 14:58:32 +0100 Subject: [PATCH 221/317] Elixir 1.19: Only match once on structs Second match is not needed and a simple Map update is recommended by the compiler --- lib/pleroma/marker.ex | 2 +- lib/pleroma/web/api_spec/render_error.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 4f645240e..fab24d183 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Marker do defp get_marker(user, timeline) do case Repo.find_resource(get_query(user, timeline)) do - {:ok, %__MODULE__{} = marker} -> %__MODULE__{marker | user: user} + {:ok, %__MODULE__{} = marker} -> %{marker | user: user} _ -> %__MODULE__{timeline: timeline, user_id: user.id} end end diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index acf510774..2ba76f250 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -18,10 +18,10 @@ defmodule Pleroma.Web.ApiSpec.RenderError do errors = Enum.map(errors, fn %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err -> - %OpenApiSpex.Cast.Error{err | name: err.value} + %{err | name: err.value} %OpenApiSpex.Cast.Error{name: nil} = err -> - %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} + %{err | name: List.last(err.path)} err -> err From 531041041a9137151022a6877e0bf1c24c15eff2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:01:36 +0100 Subject: [PATCH 222/317] Elixir 1.19: Fix deprecation warning when invoking ParallelCompiler warning: you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions (elixir 1.19.5) lib/kernel/parallel_compiler.ex:324: Kernel.ParallelCompiler.spawn_workers/3 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/html.ex:13: Pleroma.HTML.compile_scrubbers/0 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:48: Pleroma.Application.start/2 (kernel 10.5) application_master.erl:299: :application_master.start_it_old/4 warning: you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions (elixir 1.19.5) lib/kernel/parallel_compiler.ex:324: Kernel.ParallelCompiler.spawn_workers/3 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:121: Pleroma.Application.load_custom_modules/0 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:60: Pleroma.Application.start/2 (kernel 10.5) application_master.erl:299: :application_master.start_it_old/4 --- lib/pleroma/utils.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index 73001c987..61d122a47 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Utils do dir |> File.ls!() |> Enum.map(&Path.join(dir, &1)) - |> Kernel.ParallelCompiler.compile() + |> Kernel.ParallelCompiler.compile(return_diagnostics: true) end @doc """ From bf86768e889b626ed4c8c8132c15fa189562bf30 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:05:31 +0100 Subject: [PATCH 223/317] Elixir 1.19: Fix ConfigDBTest regex tests It is not possible match regexes anymore as this worked by accident previously. Instead, at least check that the sources of the regex (the regex itself) match. Notice the +1 difference in the regex Reference below. 1) test to_elixir_types/1 complex keyword with sigil (Pleroma.ConfigDBTest) test/pleroma/config_db_test.exs:460 Assertion with == failed code: assert ConfigDB.to_elixir_types([ %{"tuple" => [":federated_timeline_removal", []]}, %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, %{"tuple" => [":replace", []]} ]) == [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []] left: [federated_timeline_removal: [], reject: [%Regex{opts: [], re_pattern: {:re_pattern, 0, 0, 0, #Reference<0.230935836.591265794.259515>}, source: "comp[lL][aA][iI][nN]er"}], replace: []] right: [federated_timeline_removal: [], reject: [%Regex{opts: [], re_pattern: {:re_pattern, 0, 0, 0, #Reference<0.230935836.591265794.259516>}, source: "comp[lL][aA][iI][nN]er"}], replace: []] stacktrace: test/pleroma/config_db_test.exs:461: (test) --- test/pleroma/config_db_test.exs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index d68e4e6fa..59beba48e 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -273,24 +273,24 @@ defmodule Pleroma.ConfigDBTest do end test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/ + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == ~r/comp[lL][aA][iI][nN]er/.source end test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/ + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == ~r/https:\/\/example.com/.source end test "link sigil with um modifiers" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") == - ~r/https:\/\/example.com/um + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um").source == + ~r/https:\/\/example.com/um.source end test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == ~r/https:\/\/example.com/i.source end test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == ~r/https:\/\/example.com/s.source end test "raise if valid delimiter not found" do @@ -460,11 +460,11 @@ defmodule Pleroma.ConfigDBTest do test "complex keyword with sigil" do assert ConfigDB.to_elixir_types([ %{"tuple" => [":federated_timeline_removal", []]}, - %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, + %{"tuple" => [":reject", [~r/comp[lL][aA][iI][nN]er/.source]]}, %{"tuple" => [":replace", []]} ]) == [ federated_timeline_removal: [], - reject: [~r/comp[lL][aA][iI][nN]er/], + reject: [~r/comp[lL][aA][iI][nN]er/.source], replace: [] ] end From 6a3b5b3218f2b28395d3e949b091dd0af14bdca7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:13:10 +0100 Subject: [PATCH 224/317] Elixir 1.19: Fix MRFTest regex tests It is no longer possible to match regexes. Instead at least match that the sources of the regexes (regexes themselves) are the same. Notice the +1 Reference number below. 2) test subdomain_match/2 wildcard domains with one subdomain (Pleroma.Web.ActivityPub.MRFTest) test/pleroma/web/activity_pub/mrf_test.exs:36 Assertion with == failed code: assert regexes == [~r/^(.*\.)*unsafe.tld$/i] left: [%Regex{opts: [:caseless], re_pattern: {:re_pattern, 1, 0, 0, #Reference<0.378940835.3277193222.129648>}, source: "^(.*\\.)*unsafe.tld$"}] right: [%Regex{opts: [:caseless], re_pattern: {:re_pattern, 1, 0, 0, #Reference<0.378940835.3277193222.129649>}, source: "^(.*\\.)*unsafe.tld$"}] stacktrace: test/pleroma/web/activity_pub/mrf_test.exs:39: (test) --- test/pleroma/web/activity_pub/mrf_test.exs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 25548e3da..6656a01b8 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -11,17 +11,21 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF test "subdomains_regex/1" do - assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ - ~r/^unsafe.tld$/i, - ~r/^(.*\.)*unsafe.tld$/i + regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) + + assert matchable_regexes == [ + ~r/^unsafe.tld$/i.source, + ~r/^(.*\.)*unsafe.tld$/i.source ] end describe "subdomain_match/2" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] + assert matchable_regexes == [~r/^unsafe.tld$/i.source, ~r/^unsafe2.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -31,8 +35,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -42,8 +47,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") @@ -53,8 +59,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "matches are case-insensitive" do regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] + assert matchable_regexes == [~r/^UnSafe.TLD$/i.source, ~r/^UnSAFE2.Tld$/i.source] assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") From a9ad6297b7a8444bad161256183b494d45ce92a7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:28:06 +0100 Subject: [PATCH 225/317] Elixir 1.19: Fix Mastodon StatusControllerTest DateTime difference --- .../web/mastodon_api/controllers/status_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 298e92366..8477ae03f 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3068,7 +3068,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(expires_at, a_expires_at) == 0 + assert DateTime.diff(DateTime.truncate(expires_at, :second), DateTime.truncate(a_expires_at, :second)) == 0 %{conn: conn} = oauth_access(["read:statuses"]) From ee55764501077864e8112108a4df4c74c5f2d234 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 20:16:30 +0100 Subject: [PATCH 226/317] lint --- lib/pleroma/mfa/changeset.ex | 7 ++++--- lib/pleroma/web/api_spec/cast_and_validate.ex | 9 ++++++++- lib/pleroma/web/common_api/activity_draft.ex | 10 +++++++--- test/pleroma/config_db_test.exs | 12 ++++++++---- test/pleroma/web/activity_pub/activity_pub_test.exs | 3 ++- .../controllers/status_controller_test.exs | 6 +++++- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 890cb2193..2045c3a7c 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -8,7 +8,8 @@ defmodule Pleroma.MFA.Changeset do alias Pleroma.User def disable(%Ecto.Changeset{} = changeset, force \\ false) do - %Settings{} = settings = + %Settings{} = + settings = changeset |> Ecto.Changeset.apply_changes() |> MFA.fetch_settings() @@ -26,7 +27,7 @@ defmodule Pleroma.MFA.Changeset do end def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do - totp_settings = %Settings.TOTP{%Settings.TOTP{} = settings.totp | confirmed: true} + totp_settings = %Settings.TOTP{(%Settings.TOTP{} = settings.totp) | confirmed: true} user |> put_change(%Settings{settings | totp: totp_settings, enabled: true}) @@ -46,7 +47,7 @@ defmodule Pleroma.MFA.Changeset do def cast_backup_codes(%User{} = user, codes) do user |> put_change(%Settings{ - %Settings{} = user.multi_factor_authentication_settings + (%Settings{} = user.multi_factor_authentication_settings) | backup_codes: codes }) end diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index 57ea8e9b3..95bd4d9cf 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -106,7 +106,14 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) end - defp cast_and_validate(spec, operation, %Conn{} = conn, content_type, false = _strict, cast_opts) do + defp cast_and_validate( + spec, + operation, + %Conn{} = conn, + content_type, + false = _strict, + cast_opts + ) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do {:ok, conn} -> {:ok, conn} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c5959276a..6c84ef656 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -136,7 +136,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) + when is_binary(id) do # If a post was deleted all its activities (except the newly added Delete) are purged too, # thus lookup by Create db ID will yield nil just as if it never existed in the first place. # @@ -166,13 +167,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do + defp in_reply_to( + %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft + ) do %__MODULE__{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft - defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) + when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index 59beba48e..98b56a146 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -273,11 +273,13 @@ defmodule Pleroma.ConfigDBTest do end test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == ~r/comp[lL][aA][iI][nN]er/.source + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == + ~r/comp[lL][aA][iI][nN]er/.source end test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == ~r/https:\/\/example.com/.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == + ~r/https:\/\/example.com/.source end test "link sigil with um modifiers" do @@ -286,11 +288,13 @@ defmodule Pleroma.ConfigDBTest do end test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == ~r/https:\/\/example.com/i.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == + ~r/https:\/\/example.com/i.source end test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == ~r/https:\/\/example.com/s.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == + ~r/https:\/\/example.com/s.source end test "raise if valid delimiter not found" do diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 13146619a..9aafc41a5 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1786,7 +1786,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) - {:ok, %Activity{} = activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + {:ok, %Activity{} = activity} = + CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) activity = Repo.preload(activity, :bookmark) activity = %{activity | thread_muted?: !!activity.thread_muted?} 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 8477ae03f..11e96a6ac 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3068,7 +3068,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(DateTime.truncate(expires_at, :second), DateTime.truncate(a_expires_at, :second)) == 0 + + assert DateTime.diff( + DateTime.truncate(expires_at, :second), + DateTime.truncate(a_expires_at, :second) + ) == 0 %{conn: conn} = oauth_access(["read:statuses"]) From 645211812e41a05cd1de3801030e0ddc3c756600 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 15 Jan 2026 16:03:44 +0100 Subject: [PATCH 227/317] Elixir 1.19 MRFTest: Replace matchable_regexes with regexes_match! func --- test/pleroma/web/activity_pub/mrf_test.exs | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 6656a01b8..401d4ebb3 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -10,22 +10,25 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF + defp regexes_match!([],[]), do: true + + defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do + authority.source == checked.source and regexes_match!(authority_rest, checked_rest) + end + + defp regexes_match!(_, _), do: false + test "subdomains_regex/1" do regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [ - ~r/^unsafe.tld$/i.source, - ~r/^(.*\.)*unsafe.tld$/i.source - ] + assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^(.*\.)*unsafe.tld$/i]) end describe "subdomain_match/2" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^unsafe.tld$/i.source, ~r/^unsafe2.tld$/i.source] + assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -35,9 +38,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] + assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -47,9 +49,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] + assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") @@ -59,9 +60,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "matches are case-insensitive" do regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^UnSafe.TLD$/i.source, ~r/^UnSAFE2.Tld$/i.source] + assert regexes_match!(regexes, [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i]) assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") From 750266f2e3d60ad2b83ad8c79b93f914063726c2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 22:27:48 +0100 Subject: [PATCH 228/317] ActivityDraft: Add missing __MODULE__ matches and drop unneeded ones --- lib/pleroma/web/common_api/activity_draft.ex | 48 ++++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 6c84ef656..16489663a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -99,34 +99,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("cc", draft.cc) |> Map.put("actor", draft.user.ap_id) - %__MODULE__{draft | object: object} + %{draft | object: object} end defp put_params(%__MODULE__{} = draft, params) do params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) - %__MODULE__{draft | params: params} + %{draft | params: params} end defp status(%__MODULE__{params: %{status: status}} = draft) do - %__MODULE__{draft | status: String.trim(status)} + %{draft | status: String.trim(status)} end defp summary(%__MODULE__{params: params} = draft) do - %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} + %{draft | summary: Map.get(params, :spoiler_text, "")} end defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do full_payload = String.trim(status <> summary) case Utils.validate_character_limit(full_payload, draft.attachments) do - :ok -> %__MODULE__{draft | full_payload: full_payload} + :ok -> %{draft | full_payload: full_payload} {:error, message} -> add_error(draft, message) end end defp attachments(%__MODULE__{params: params} = draft) do attachments = Utils.attachments_from_ids(params, draft.user) - draft = %__MODULE__{draft | attachments: attachments} + draft = %{draft | attachments: attachments} case Utils.validate_attachments_count(attachments) do :ok -> draft @@ -149,7 +149,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do with %Activity{} = activity <- Activity.get_by_id(id), true <- Visibility.visible_for_user?(activity, draft.user), {_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do - %__MODULE__{draft | in_reply_to: activity} + %{draft | in_reply_to: activity} else nil -> add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) @@ -170,7 +170,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to( %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft ) do - %__MODULE__{draft | in_reply_to: in_reply_to} + %{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft @@ -179,14 +179,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> - %__MODULE__{draft | quote_post: activity} + %{draft | quote_post: activity} _ -> draft end end - defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quote_id: id}} = draft) when not_empty_string(id) do quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)}) end @@ -194,7 +194,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to_conversation(%__MODULE__{} = 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} + %{draft | in_reply_to_conversation: in_reply_to_conversation} end defp visibility(%__MODULE__{params: params} = draft) do @@ -203,7 +203,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do add_error(draft, dgettext("errors", "The message visibility must be direct")) {visibility, _} -> - %__MODULE__{draft | visibility: visibility} + %{draft | visibility: visibility} end end @@ -219,7 +219,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do false end - defp quoting_visibility(%{quote_post: %Activity{}} = draft) do + defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), true <- can_quote?(draft, object, Visibility.get_visibility(object)) do draft @@ -232,7 +232,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp expires_at(%__MODULE__{} = draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do - {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} + {:ok, expires_at} -> %{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end end @@ -240,7 +240,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp poll(%__MODULE__{} = draft) do case Utils.make_poll_data(draft.params) do {:ok, {poll, poll_emoji}} -> - %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} + %{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} {:error, message} -> add_error(draft, message) @@ -258,22 +258,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Kernel.++(mentioned_ap_ids) |> Utils.get_addressed_users(draft.params[:to]) - %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} + %{draft | content_html: content_html, mentions: mentions, tags: tags} end defp to_and_cc(%__MODULE__{} = draft) do {to, cc} = Utils.get_to_and_cc(draft) - %__MODULE__{draft | to: to, cc: cc} + %{draft | to: to, cc: cc} end defp context(%__MODULE__{} = draft) do context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) - %__MODULE__{draft | context: context} + %{draft | context: context} end defp sensitive(%__MODULE__{} = draft) do sensitive = draft.params[:sensitive] - %__MODULE__{draft | sensitive: sensitive} + %{draft | sensitive: sensitive} end defp language(%__MODULE__{} = draft) do @@ -285,7 +285,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary) end - %__MODULE__{draft | language: language} + %{draft | language: language} end defp object(%__MODULE__{} = draft) do @@ -329,12 +329,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) - %__MODULE__{draft | object: object} + %{draft | object: object} end defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) - %__MODULE__{draft | preview?: preview?} + %{draft | preview?: preview?} end defp changes(%__MODULE__{} = draft) do @@ -357,14 +357,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do } |> Utils.maybe_add_list_data(draft.user, draft.visibility) - %__MODULE__{draft | changes: changes} + %{draft | changes: changes} end defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(draft, _func), do: draft defp add_error(%__MODULE__{} = draft, message) do - %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} + %{draft | valid?: false, errors: [message | draft.errors]} end defp validate(%{valid?: true} = draft), do: {:ok, draft} From 2937bb68b1d0d0b9c70fa40782f9cd772a56217d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:09:18 -0700 Subject: [PATCH 229/317] Fix MoveTokensExpirationIntoOban migration Pleroma.Workers.PurgeExpiredToken.enqueue/1 no longer exists, so this migration would fail. The enqueue/1 function is only used for this migration, so we can just include it in the migration module directly. --- ...0907092050_move_tokens_expiration_into_oban.exs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs index c140bc66a..0a55e4f71 100644 --- a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs +++ b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs @@ -21,7 +21,7 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do from(t in Pleroma.Web.OAuth.Token, where: t.valid_until > ^NaiveDateTime.utc_now()) |> Pleroma.Repo.stream() |> Stream.each(fn token -> - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + enqueue(%{ token_id: token.id, valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), mod: Pleroma.Web.OAuth.Token @@ -33,7 +33,7 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do from(t in Pleroma.MFA.Token, where: t.valid_until > ^NaiveDateTime.utc_now()) |> Pleroma.Repo.stream() |> Stream.each(fn token -> - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + enqueue(%{ token_id: token.id, valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), mod: Pleroma.MFA.Token @@ -41,4 +41,14 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do end) |> Stream.run() end + + @spec enqueue(%{token_id: integer(), valid_until: DateTime.t()}) :: + {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} + defp enqueue(args) do + {scheduled_at, args} = Map.pop(args, :valid_until) + + args + |> Pleroma.Workers.PurgeExpiredToken.new(scheduled_at: scheduled_at) + |> Oban.insert() + end end From f3f72048ac2a7c379cf31b2478eefc057de198f9 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:19:33 -0700 Subject: [PATCH 230/317] Fix MoveActivityExpirationsToOban migration This never would have worked correctly. warning: Pleroma.Workers.PurgeExpiredActivity.enqueue/1 is undefined or private. Did you mean: * enqueue/2 --- ...0200825061316_move_activity_expirations_to_oban.exs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs index f15876180..b512313c7 100644 --- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -23,10 +23,12 @@ defmodule Pleroma.Repo.Migrations.MoveActivityExpirationsToOban do |> Pleroma.Repo.stream() |> Stream.each(fn expiration -> with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: FlakeId.to_string(expiration.activity_id), - expires_at: expires_at - }) + Pleroma.Workers.PurgeExpiredActivity.enqueue( + %{ + activity_id: FlakeId.to_string(expiration.activity_id) + }, + scheduled_at: expires_at + ) end end) |> Stream.run() From e1a1e5c726fe0aef9accca30641160934d02143a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:22:58 -0700 Subject: [PATCH 231/317] Correct old migrations for expiring activities and user access tokens. --- changelog.d/old-migrations.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/old-migrations.fix diff --git a/changelog.d/old-migrations.fix b/changelog.d/old-migrations.fix new file mode 100644 index 000000000..49566c896 --- /dev/null +++ b/changelog.d/old-migrations.fix @@ -0,0 +1 @@ +Correct old migrations for expiring activities and user access tokens. From cbb715b978efe6f65c30b555341e0fa3191013cb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:36:16 -0700 Subject: [PATCH 232/317] No-op code correctness improvements detected by Elixir 1.19 compiler --- changelog.d/elixir-1.19-cherrypicks.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/elixir-1.19-cherrypicks.change diff --git a/changelog.d/elixir-1.19-cherrypicks.change b/changelog.d/elixir-1.19-cherrypicks.change new file mode 100644 index 000000000..7e56be008 --- /dev/null +++ b/changelog.d/elixir-1.19-cherrypicks.change @@ -0,0 +1 @@ +No-op code correctness improvements detected by Elixir 1.19 compiler From 711b33d81cd8ecbf83aa7eea9c4a76ab531713fe Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 13:32:25 -0700 Subject: [PATCH 233/317] Fix CommonAPI.favorite/2 arg order --- test/pleroma/search_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/search_test.exs b/test/pleroma/search_test.exs index cdd2a72bd..d777bcda2 100644 --- a/test/pleroma/search_test.exs +++ b/test/pleroma/search_test.exs @@ -54,7 +54,7 @@ defmodule Pleroma.SearchTest do assert_enqueued(worker: SearchIndexingWorker, args: args) - {:ok, fav_activity} = CommonAPI.favorite(user, activity.id) + {:ok, fav_activity} = CommonAPI.favorite(activity.id, user) args = %{"op" => "add_to_index", "activity" => fav_activity.id} From ea78e7683795dcda995e3755f0275fe6398f815f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 14:46:38 -0700 Subject: [PATCH 234/317] Fix add_to_index/1 to adhere to the typespec --- lib/pleroma/search.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index 8477873f9..f78495c8d 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Search do def add_to_index(%Activity{id: activity_id}) do case Activity.get_by_id_with_object(activity_id) do %Activity{} = preloaded -> add_to_index(preloaded) - _ -> :ok + _ -> {:ok, :noop} end end From f06a0eab50b9ed3e6d4ed4db714d5632225ff9f0 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 14:47:39 -0700 Subject: [PATCH 235/317] Move object_to_search_data/1 to Pleroma.Search This standardizes this functionality within the Search module so it doesn't need to be imported by other search backends from Meilisearch Also integrate its filtering rules into Search.indexable?/1 for consistency --- changelog.d/search-indexing.skip | 0 lib/mix/tasks/pleroma/search/meilisearch.ex | 2 +- lib/pleroma/search.ex | 39 +++++++++++++++++++- lib/pleroma/search/meilisearch.ex | 40 ++------------------- lib/pleroma/search/qdrant_search.ex | 4 +-- 5 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 changelog.d/search-indexing.skip diff --git a/changelog.d/search-indexing.skip b/changelog.d/search-indexing.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index edce9e871..facc38815 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -72,7 +72,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do query, timeout: :infinity ) - |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1) + |> Stream.map(&Pleroma.Search.object_to_search_data/1) |> Stream.filter(fn o -> not is_nil(o) end) |> Stream.chunk_every(chunk_size) |> Stream.transform(0, fn objects, acc -> diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index f78495c8d..9cd2768c4 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -38,6 +38,43 @@ defmodule Pleroma.Search do search_module.healthcheck_endpoints() end - defp indexable?(%Activity{data: %{"type" => "Create"}}), do: true + def object_to_search_data(%Object{} = object) do + data = object.data + + content_str = + case data["content"] do + [nil | rest] -> to_string(rest) + str -> str + end + + content = + with {:ok, scrubbed} <- + FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), + trimmed <- String.trim(scrubbed) do + trimmed + end + + # Make sure we have a non-empty string + if content != "" do + {:ok, published, _} = DateTime.from_iso8601(data["published"]) + + %{ + id: object.id, + content: content, + ap: data["id"], + published: published |> DateTime.to_unix() + } + end + end + + defp indexable?(%Activity{ + data: %{"type" => "Create"}, + object: %Object{ + data: %{"content" => content, "published" => published, "type" => "Note"} + } + }) + when not is_nil(content) and content not in ["", "."] and not is_nil(published), + do: true + defp indexable?(_), do: false end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 4541ef14a..dc10076e1 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Search.Meilisearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config alias Pleroma.Object + alias Pleroma.Search import Pleroma.Search.DatabaseSearch import Ecto.Query @@ -119,46 +120,9 @@ defmodule Pleroma.Search.Meilisearch do end end - def object_to_search_data(object) do - # Only index public or unlisted Notes - if not is_nil(object) and object.data["type"] == "Note" and - not is_nil(object.data["content"]) and - not is_nil(object.data["published"]) and - (Pleroma.Constants.as_public() in object.data["to"] or - Pleroma.Constants.as_public() in object.data["cc"]) and - object.data["content"] not in ["", "."] do - data = object.data - - content_str = - case data["content"] do - [nil | rest] -> to_string(rest) - str -> str - end - - content = - with {:ok, scrubbed} <- - FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), - trimmed <- String.trim(scrubbed) do - trimmed - end - - # Make sure we have a non-empty string - if content != "" do - {:ok, published, _} = DateTime.from_iso8601(data["published"]) - - %{ - id: object.id, - content: content, - ap: data["id"], - published: published |> DateTime.to_unix() - } - end - end - end - @impl true def add_to_index(%Activity{object: %Object{} = object} = activity) do - search_data = object_to_search_data(object) + search_data = Search.object_to_search_data(object) result = meili_put( diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex index 06f5b1983..4d57cfa88 100644 --- a/lib/pleroma/search/qdrant_search.ex +++ b/lib/pleroma/search/qdrant_search.ex @@ -5,11 +5,11 @@ defmodule Pleroma.Search.QdrantSearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config alias Pleroma.Object + alias Pleroma.Search alias __MODULE__.OpenAIClient alias __MODULE__.QdrantClient - import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1] import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3] @impl true @@ -84,7 +84,7 @@ defmodule Pleroma.Search.QdrantSearch do @impl true def add_to_index(%Activity{object: %Object{} = object} = activity) do - search_data = object_to_search_data(object) + search_data = Search.object_to_search_data(object) with {:ok, embedding} <- get_embedding(search_data.content), {:ok, %{status: 200}} <- From 5aa3c8a06e9b62f5fa6a90ae319c7a75cbbac4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Mar 2026 11:50:31 +0100 Subject: [PATCH 236/317] Federate `votersCount` correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk Assisted-by: your mother Signed-off-by: nicole mikołajczyk --- changelog.d/poll-voters-count.fix | 1 + lib/pleroma/object.ex | 8 ++++++++ .../object_validators/question_validator.ex | 1 + .../web/activity_pub/transmogrifier.ex | 7 +++++++ .../web/mastodon_api/views/poll_view.ex | 2 ++ .../transmogrifier/question_handling_test.exs | 19 +++++++++++++++++++ .../web/mastodon_api/views/poll_view_test.exs | 9 ++++++++- 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 changelog.d/poll-voters-count.fix diff --git a/changelog.d/poll-voters-count.fix b/changelog.d/poll-voters-count.fix new file mode 100644 index 000000000..2dbc81b5d --- /dev/null +++ b/changelog.d/poll-voters-count.fix @@ -0,0 +1 @@ +Federate `votersCount` correctly diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index d0cb16b79..f1e07a257 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -374,10 +374,18 @@ defmodule Pleroma.Object do voters = [actor | object.data["voters"] || []] |> Enum.uniq() + voters_count = + if Map.has_key?(object.data, "votersCount") do + object.data["votersCount"] + 1 + else + length(voters) + end + data = object.data |> Map.put(key, options) |> Map.put("voters", voters) + |> Map.put("votersCount", voters_count) object |> Object.change(%{data: data}) 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 21940f4f1..065c75910 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -28,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do end field(:closed, ObjectValidators.DateTime) + field(:votersCount, :integer) field(:voters, {:array, ObjectValidators.ObjectID}, default: []) field(:nonAnonymous, :boolean) embeds_many(:anyOf, QuestionOptionsValidator) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6af5ee89d..4421da26c 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -783,6 +783,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_replies(obj_data), do: obj_data + defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do + Map.merge(obj, %{"votersCount" => length(voters)}) + end + + defp set_voters_count(obj), do: obj + # Prepares and sanitizes the object for federation. def prepare_object(object) do object @@ -795,6 +801,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> set_reply_to_uri |> set_quote_url |> set_replies + |> set_voters_count |> CommonFixes.maybe_add_content_map() |> strip_internal_fields |> strip_internal_tags diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 1e3c9f36d..3b4271227 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -75,6 +75,8 @@ defmodule Pleroma.Web.MastodonAPI.PollView do length(voters) end + defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(_), do: 0 defp voted_and_own_votes(%{object: object} = params, options) do diff --git a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs index d31070546..2a4e78cf0 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs @@ -170,4 +170,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) end + + test "it displays voters count for a poll" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "???", + poll: %{expires_in: 10, options: ["yes", "no"]} + }) + + object = Object.normalize(activity, fetch: false) + {:ok, _, _} = CommonAPI.vote(object, other_user, [1]) + + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) + + refute Map.has_key?(modified["object"], "voters") + assert modified["object"]["votersCount"] == 1 + end end diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index 6de001421..16281393d 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -167,7 +167,14 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do } = PollView.render("show.json", %{object: object}) end - test "that poll is non anonymous" do + test "displays correct voters count" do + object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true) + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 14 + end + + test "detects that poll is non anonymous" do object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true) result = PollView.render("show.json", %{object: object}) From 799199f6b504d918bf55787149ec0e7240693164 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 20:50:39 +0100 Subject: [PATCH 237/317] DigestEmailsWorker: Change Oban queue to "background" The mailer queue has been long gone and that left Oban jobs always stuck in the "available" state that would never execute. --- lib/pleroma/workers/cron/digest_emails_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex index b50b52a7b..5cb13f5f9 100644 --- a/lib/pleroma/workers/cron/digest_emails_worker.ex +++ b/lib/pleroma/workers/cron/digest_emails_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do The worker to send digest emails. """ - use Oban.Worker, queue: "mailer" + use Oban.Worker, queue: :background alias Pleroma.Config alias Pleroma.Emails From c8baad165ba418f0aac9edcdc548381c9b5eb4fc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 31 Mar 2026 16:04:55 +0200 Subject: [PATCH 238/317] lint: fix warnings throughout codebase --- changelog.d/lint-warnings.skip | 0 .../web/api_spec/operations/list_operation.ex | 10 ++++++++-- lib/pleroma/web/endpoint.ex | 3 ++- test/pleroma/web/activity_pub/mrf_test.exs | 2 +- .../controllers/account_controller_test.exs | 5 ++++- .../remote_interaction_controller_test.exs | 16 ++++++++++++---- 6 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 changelog.d/lint-warnings.skip diff --git a/changelog.d/lint-warnings.skip b/changelog.d/lint-warnings.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index d2e803178..87189edc2 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -172,7 +172,10 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do type: :object, properties: %{ title: %Schema{type: :string, description: "List title"}, - exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } }, required: [:title] }, @@ -188,7 +191,10 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do type: :object, properties: %{ title: %Schema{type: :string, description: "List title"}, - exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } } }, required: true diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 81a9d3a09..a5c04a0c4 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -48,7 +48,8 @@ defmodule Pleroma.Web.Endpoint do @static_cache_control "public, max-age=1209600, immutable" @static_cache_disabled "public, no-cache" - @favicon_cache_control "public, max=age=86400, immutable" # cache for a day + # cache for a day + @favicon_cache_control "public, max=age=86400, immutable" # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 401d4ebb3..2d0f3b317 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF - defp regexes_match!([],[]), do: true + defp regexes_match!([], []), do: true defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do authority.source == checked.source and regexes_match!(authority_rest, checked_rest) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 67f292506..01e195842 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1815,7 +1815,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do test "returns lists to which the account belongs" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create(%{title: "Test List"}, user) + + assert {:ok, %Pleroma.List{id: _list_id} = list} = + Pleroma.List.create(%{title: "Test List"}, user) + {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) assert [%{"id" => _list_id, "title" => "Test List"}] = diff --git a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs index 9df86c0a2..9236d1bf8 100644 --- a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs +++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs @@ -83,7 +83,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do response = conn - |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get( + remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + ) |> html_response(200) assert response =~ "Log in to follow" @@ -114,7 +116,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do response = conn |> assign(:user, user) - |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get( + remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + ) |> html_response(200) assert response =~ "Remote follow" @@ -157,7 +161,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do conn |> assign(:user, user) |> assign(:token, read_token) - |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{ + "user" => %{"id" => user2.id} + }) |> response(200) assert response =~ "Error following account" @@ -496,7 +502,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do ) assert redirected_to(conn) == - remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + remote_interaction_path(conn, :follow, %{ + acct: "https://mastodon.social/users/emelie" + }) end end From eb69576154a29c8556a1e37b3138f359cd7e0e57 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 31 Mar 2026 16:21:04 +0200 Subject: [PATCH 239/317] fix test after embed route got added back --- test/pleroma/web/plugs/frontend_static_plug_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index e1e331c06..b7c06eacd 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -105,6 +105,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do "nodeinfo", "manifest.json", "auth", + "embed", "proxy", "test", "user_exists", From a9fe2fe4d8fe976a2a96cd7d2e9f38fd4b8a857b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 15:22:34 +0100 Subject: [PATCH 240/317] Move main Woodpecker file to own directory --- .woodpecker.yml => .woodpecker/test.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .woodpecker.yml => .woodpecker/test.yaml (100%) diff --git a/.woodpecker.yml b/.woodpecker/test.yaml similarity index 100% rename from .woodpecker.yml rename to .woodpecker/test.yaml From 88a349f3ab30b9661daed66b529a094bcabb2737 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 16:29:59 +0100 Subject: [PATCH 241/317] Woodpecker CI: Retry failed tests using pleroma.test_runner I didn't add the --cover option, but it would be useless right now anyway --- .woodpecker/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml index 4ceb1cab5..10ca016c4 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -17,7 +17,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.hex --force" - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - - su testuser -c "HOME=/home/testuser mix test" + - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules" services: postgres: From 1a0af1c0c074ee207fa3b377d534c6a028c9fb6a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 17:09:04 +0100 Subject: [PATCH 242/317] Woodpecker CI: Add check-changelog workflow --- .woodpecker/changelog.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .woodpecker/changelog.yaml diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml new file mode 100644 index 000000000..4f38ce618 --- /dev/null +++ b/.woodpecker/changelog.yaml @@ -0,0 +1,9 @@ +when: + - event: pull_request + +steps: + check-changelog: + image: docker.io/alpine:3.23 + commands: + - apk add --no-cache git + - sh ./tools/check-changelog From 4493d0d187e668f422aa2c7b29392a92ff338edc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 17:19:24 +0100 Subject: [PATCH 243/317] Woodpecker CI: Update check-changelog script for Woodpecker --- tools/check-changelog | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/check-changelog b/tools/check-changelog index 5952aefcc..d09a68895 100644 --- a/tools/check-changelog +++ b/tools/check-changelog @@ -1,14 +1,14 @@ #!/bin/sh echo "adding ownership exception" -git config --global --add safe.directory $(pwd) +git config --global --add safe.directory "$(pwd)" echo "looking for change log" git remote add upstream https://git.pleroma.social/pleroma/pleroma.git -git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME +git fetch upstream ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/upstream/${CI_COMMIT_TARGET_BRANCH} -git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \ +git diff --raw --no-renames upstream/${CI_COMMIT_TARGET_BRANCH} HEAD -- changelog.d | \ grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\|change\)$' ret=$? From 2880aac61794bfa25601a0c2cbf0e8c949f6d4bf Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 18:27:45 +0100 Subject: [PATCH 244/317] Woodpecker CI: Unit test using Elixir 1.15 and 1.18 --- ...est.yaml => unit-testing-elixir-1.15.yaml} | 10 ++++-- .woodpecker/unit-testing-elixir-1.18.yaml | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) rename .woodpecker/{test.yaml => unit-testing-elixir-1.15.yaml} (87%) create mode 100644 .woodpecker/unit-testing-elixir-1.18.yaml diff --git a/.woodpecker/test.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml similarity index 87% rename from .woodpecker/test.yaml rename to .woodpecker/unit-testing-elixir-1.15.yaml index 10ca016c4..cf1e638eb 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,9 +1,13 @@ when: - - event: - - pull_request + - event: pull_request + - event: push + branch: develop + +depends_on: + - changelog steps: - test: + unit-testing-elixir-1.15: image: elixir:1.15-alpine environment: MIX_ENV: test diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml new file mode 100644 index 000000000..0e382f527 --- /dev/null +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -0,0 +1,32 @@ +when: + - event: pull_request + - event: push + branch: develop + +depends_on: + - changelog + +steps: + unit-testing-elixir-1.18: + image: elixir:1.18-alpine + environment: + MIX_ENV: test + DB_HOST: postgres + DB_PORT: 5432 + commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix deps.get" + - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules" + +services: + postgres: + image: postgres:13-alpine + environment: + POSTGRES_DB: pleroma_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres From 6f8233d780d16f9db0479849dd685b866fa096b2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 19:24:42 +0100 Subject: [PATCH 245/317] Woodpecker CI: Add linting pipeline --- .woodpecker/lint.yaml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .woodpecker/lint.yaml diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml new file mode 100644 index 000000000..02b814223 --- /dev/null +++ b/.woodpecker/lint.yaml @@ -0,0 +1,36 @@ +when: + - event: pull_request + +depends_on: + - changelog + +steps: + mix-format: + image: &elixir-image + docker.io/elixir:1.15-alpine + commands: + - | + if ! mix format --check-formatted; then + touch fail.stamp + fi + + credo: + image: *elixir-image + environment: + MIX_ENV: test + commands: + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix deps.get" + - | + if ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp; then + touch fail.stamp + fi + + cycles: + image: *elixir-image + commands: + - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' From b67d7c110623115bb7baa63ecb78300e6c3fe24a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 19:38:51 +0100 Subject: [PATCH 246/317] changelog --- changelog.d/woodpecker-pr-pipeline.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/woodpecker-pr-pipeline.skip diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip new file mode 100644 index 000000000..e69de29bb From 0fd544722f7686cd2705fb4d48032d5e4d5111e5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 19:43:50 +0100 Subject: [PATCH 247/317] Woodpecker: Ensure correct workflow status in lint pipeline --- .woodpecker/lint.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 02b814223..910e335df 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -26,7 +26,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - | - if ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp; then + if [ ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp ]; then touch fail.stamp fi @@ -34,3 +34,14 @@ steps: image: *elixir-image commands: - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' + + ensure-status: + image: *elixir-image + commands: | + if [ -f fail.stamp ]; then + echo "One or more previous steps fails. Failing workflow... + exit 1 + else + echo "All steps passed" + exit 0 + fi From 8640fcef22c8d773ac7b0d13669545f66b1e98d3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:10:13 +0100 Subject: [PATCH 248/317] Woodpecker CI: Fix compile error on Elixir 1.18 due to wrong OTP --- .woodpecker/unit-testing-elixir-1.18.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 0e382f527..91bcf1824 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,7 +8,7 @@ depends_on: steps: unit-testing-elixir-1.18: - image: elixir:1.18-alpine + image: elixir:1.18-otp-27-alpine environment: MIX_ENV: test DB_HOST: postgres From b224a2dacc793302242fc0738739415fa5484318 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:10:49 +0100 Subject: [PATCH 249/317] Woodpecker CI: Don't immediately fail whole lint workflow with one error --- .woodpecker/lint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 910e335df..766a992eb 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -8,6 +8,7 @@ steps: mix-format: image: &elixir-image docker.io/elixir:1.15-alpine + failure: ignore commands: - | if ! mix format --check-formatted; then @@ -16,6 +17,7 @@ steps: credo: image: *elixir-image + failure: ignore environment: MIX_ENV: test commands: @@ -32,6 +34,7 @@ steps: cycles: image: *elixir-image + failure: ignore commands: - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' From 265d3eeebc15e6e63c6239ae9184e5626705c2d4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:22:49 +0100 Subject: [PATCH 250/317] Woodpecker CI: Fix syntax error in lint workflow --- .woodpecker/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 766a992eb..792540f71 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -42,7 +42,7 @@ steps: image: *elixir-image commands: | if [ -f fail.stamp ]; then - echo "One or more previous steps fails. Failing workflow... + echo "One or more previous steps fails. Failing workflow..." exit 1 else echo "All steps passed" From 56a25202b97c911e78a06a0bc20d585620582e49 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:33:43 +0100 Subject: [PATCH 251/317] Woodpecker CI: Fix credo --- .woodpecker/lint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 792540f71..78f4a5164 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -21,6 +21,7 @@ steps: environment: MIX_ENV: test commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl - adduser -D -h /home/testuser testuser - mkdir -p /home/testuser/.mix /home/testuser/.hex - chown -R testuser:testuser . /home/testuser From b0de9bd3cdf9731bff858df640276ec70612a3e4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:36:27 +0100 Subject: [PATCH 252/317] Woodpecker CI: Make xref use fail stamp --- .woodpecker/lint.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 78f4a5164..77f4ff589 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -37,7 +37,10 @@ steps: image: *elixir-image failure: ignore commands: - - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' + - | + if [ ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && -f fail.stamp ]; then + touch fail.stamp + fi ensure-status: image: *elixir-image From cdcc432f31e0245f6d849aeec4f7e438f1f8df91 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:44:28 +0100 Subject: [PATCH 253/317] Woodpecker CI: Lint workflow, don't use brackets in shell tests --- .woodpecker/lint.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 77f4ff589..5fa440f2f 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -29,7 +29,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - | - if [ ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp ]; then + if ! su testuser -c "HOME=/home/testuser mix analyze" && ! test -f fail.stamp; then touch fail.stamp fi @@ -38,14 +38,14 @@ steps: failure: ignore commands: - | - if [ ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && -f fail.stamp ]; then - touch fail.stamp + if ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && test -f fail.stamp; then + touch fail.stamp fi ensure-status: image: *elixir-image commands: | - if [ -f fail.stamp ]; then + if test -f fail.stamp; then echo "One or more previous steps fails. Failing workflow..." exit 1 else From 08bf6c8fedbf2776bf87ad81731df9ab6ad85eb3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 21:02:04 +0100 Subject: [PATCH 254/317] Woodpecker CI: Explicitely exit with non-zero exit code on fail --- .woodpecker/lint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 5fa440f2f..6d4042890 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -13,6 +13,7 @@ steps: - | if ! mix format --check-formatted; then touch fail.stamp + exit 1 fi credo: @@ -31,6 +32,7 @@ steps: - | if ! su testuser -c "HOME=/home/testuser mix analyze" && ! test -f fail.stamp; then touch fail.stamp + exit 1 fi cycles: @@ -40,6 +42,7 @@ steps: - | if ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && test -f fail.stamp; then touch fail.stamp + exit 1 fi ensure-status: From 1fe0970b6446444910acdc3340dc9065c45f24c6 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 21:21:11 +0100 Subject: [PATCH 255/317] woodpecker CI: Fix cycles in lint workflow --- .woodpecker/lint.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 6d4042890..2594b903a 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -30,7 +30,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - | - if ! su testuser -c "HOME=/home/testuser mix analyze" && ! test -f fail.stamp; then + if ! su testuser -c "HOME=/home/testuser mix analyze"; then touch fail.stamp exit 1 fi @@ -39,8 +39,15 @@ steps: image: *elixir-image failure: ignore commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix compile" - | - if ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && test -f fail.stamp; then + if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then touch fail.stamp exit 1 fi From 7bba485397fa40c748647ddec9201b6612c47b93 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 16:17:36 +0100 Subject: [PATCH 256/317] Woodpecker CI: Disable cycles lint step for now since it always fails --- .woodpecker/lint.yaml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 2594b903a..5e9d9ff63 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -35,22 +35,22 @@ steps: exit 1 fi - cycles: - image: *elixir-image - failure: ignore - commands: - - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl - - adduser -D -h /home/testuser testuser - - mkdir -p /home/testuser/.mix /home/testuser/.hex - - chown -R testuser:testuser . /home/testuser - - su testuser -c "HOME=/home/testuser mix local.hex --force" - - su testuser -c "HOME=/home/testuser mix local.rebar --force" - - su testuser -c "HOME=/home/testuser mix compile" - - | - if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then - touch fail.stamp - exit 1 - fi + # cycles: + # image: *elixir-image + # failure: ignore + # commands: + # - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + # - adduser -D -h /home/testuser testuser + # - mkdir -p /home/testuser/.mix /home/testuser/.hex + # - chown -R testuser:testuser . /home/testuser + # - su testuser -c "HOME=/home/testuser mix local.hex --force" + # - su testuser -c "HOME=/home/testuser mix local.rebar --force" + # - su testuser -c "HOME=/home/testuser mix compile" + # - | + # if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then + # touch fail.stamp + # exit 1 + # fi ensure-status: image: *elixir-image From 072dc39d831bd01d36e0ec9f15c43ac900983506 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 16:18:15 +0100 Subject: [PATCH 257/317] Woodpecker CI: Don't depend on changelog in lint workflow --- .woodpecker/lint.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 5e9d9ff63..db815ff93 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,9 +1,6 @@ when: - event: pull_request -depends_on: - - changelog - steps: mix-format: image: &elixir-image From 096c4ea9801f780c1a1455ff0fff4086ea7c1478 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 16:20:24 +0100 Subject: [PATCH 258/317] Woodpecker CI: Run lint and unit tests also on push to default branch --- .woodpecker/lint.yaml | 2 ++ .woodpecker/unit-testing-elixir-1.15.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.18.yaml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index db815ff93..d222de483 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,5 +1,7 @@ when: - event: pull_request + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} steps: mix-format: diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index cf1e638eb..75ca3b6a0 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,10 +1,10 @@ when: - event: pull_request - event: push - branch: develop + branch: ${CI_REPO_DEFAULT_BRANCH} depends_on: - - changelog + - lint steps: unit-testing-elixir-1.15: diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 91bcf1824..01687cfb8 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,10 +1,10 @@ when: - event: pull_request - event: push - branch: develop + branch: ${CI_REPO_DEFAULT_BRANCH} depends_on: - - changelog + - lint steps: unit-testing-elixir-1.18: From fd7b809c5430aa557cb6711629ddf14a5d005fe5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 18:12:38 +0100 Subject: [PATCH 259/317] Woodpecker CI: Only run lint and unit tests when relevant files changed --- .woodpecker/lint.yaml | 2 ++ .woodpecker/unit-testing-elixir-1.15.yaml | 2 ++ .woodpecker/unit-testing-elixir-1.18.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index d222de483..b96d584ee 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,7 +1,9 @@ when: - event: pull_request + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] steps: mix-format: diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 75ca3b6a0..84046be41 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,7 +1,9 @@ when: - event: pull_request + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 01687cfb8..8dcec62fb 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,7 +1,9 @@ when: - event: pull_request + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint From 01ced6bea2db989287c53795d7fb66c5c9138600 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 1 Apr 2026 11:59:23 -0700 Subject: [PATCH 260/317] Fix the daily email digest job which was not executing --- changelog.d/email_digest.fix | 1 + .../20260401185429_cleanup_stale_digest_email_jobs.exs | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog.d/email_digest.fix create mode 100644 priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs diff --git a/changelog.d/email_digest.fix b/changelog.d/email_digest.fix new file mode 100644 index 000000000..cd15874a2 --- /dev/null +++ b/changelog.d/email_digest.fix @@ -0,0 +1 @@ +Fix the daily email digest job which was not executing diff --git a/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs b/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs new file mode 100644 index 000000000..882c7ec66 --- /dev/null +++ b/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.CleanupStaleDigestEmailJobs do + use Ecto.Migration + + def up do + execute( + "DELETE from oban_jobs WHERE queue = 'mailer' AND worker = 'Pleroma.Workers.Cron.DigestEmailsWorker'" + ) + end +end From 00265751cc7242016e6ae6d8ff0408f30c90f263 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 3 Apr 2026 13:22:12 -0700 Subject: [PATCH 261/317] Update Bandit --- changelog.d/bandit.change | 1 + mix.exs | 2 +- mix.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/bandit.change diff --git a/changelog.d/bandit.change b/changelog.d/bandit.change new file mode 100644 index 000000000..3831a02c2 --- /dev/null +++ b/changelog.d/bandit.change @@ -0,0 +1 @@ +Update Bandit to 1.10.4 diff --git a/mix.exs b/mix.exs index dc9fa31f5..ca365e9eb 100644 --- a/mix.exs +++ b/mix.exs @@ -197,7 +197,7 @@ defmodule Pleroma.Mixfile do {:elixir_make, "~> 0.7.8", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, {:exile, "~> 0.10.0"}, - {:bandit, "~> 1.5.7"}, + {:bandit, "~> 1.10"}, {:websock_adapter, "~> 0.5.8"}, {:oban_live_dashboard, "~> 0.1.1"}, {:multipart, "~> 0.4.0", optional: true}, diff --git a/mix.lock b/mix.lock index 8e1f684dc..a940d4011 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"}, - "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, + "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, @@ -144,7 +144,7 @@ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"}, - "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, From 7582b71f4699dfedaac9ccdae163537034a86466 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 24 Mar 2026 11:56:28 -0700 Subject: [PATCH 262/317] Downgrade Hackney to 1.20.1, before connection performance regressions It appears the implementation of Happy Eyeballs in 1.22.0 is the origin of some pretty serious performance regressions that remain even in the latest Hackney 3.0 branch. Connection tests: === 1.22.0 === First call: 9434ms Second call: 14ms === 1.21.0 === First call: 228ms Second call: 16ms We went back further to 1.20.1 though because of reported problems with the mail client and ssl_options. That bug was not reproduced by a dev, though, but we'll trust it for now. --- changelog.d/hackney-downgrade.change | 1 + mix.exs | 2 +- mix.lock | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/hackney-downgrade.change diff --git a/changelog.d/hackney-downgrade.change b/changelog.d/hackney-downgrade.change new file mode 100644 index 000000000..a98710692 --- /dev/null +++ b/changelog.d/hackney-downgrade.change @@ -0,0 +1 @@ +Downgrade Hackney to 1.20.1 diff --git a/mix.exs b/mix.exs index dc9fa31f5..f445ca7aa 100644 --- a/mix.exs +++ b/mix.exs @@ -211,7 +211,7 @@ defmodule Pleroma.Mixfile do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.9", only: :test}, {:covertool, "~> 2.0", only: :test}, - {:hackney, "~> 1.25.0", override: true}, + {:hackney, "~> 1.20.1", override: true}, {:mox, "~> 1.2", only: :test}, {:websockex, "~> 0.4.3", only: :test}, {:benchee, "~> 1.4", only: :benchmark}, diff --git a/mix.lock b/mix.lock index 8e1f684dc..6e12e9aa3 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, - "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"}, @@ -59,7 +59,7 @@ "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, - "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"}, @@ -80,7 +80,7 @@ "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, - "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, @@ -128,6 +128,7 @@ "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, + "quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]}, From c2fb145c5f33ea1b4c3ad7dd4ce21588bedc5e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 10 Apr 2026 21:33:20 +0200 Subject: [PATCH 263/317] litepub-0.1.jsonld cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/context-cleanup.skip | 1 + priv/static/schemas/litepub-0.1.jsonld | 59 +++++++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 changelog.d/context-cleanup.skip diff --git a/changelog.d/context-cleanup.skip b/changelog.d/context-cleanup.skip new file mode 100644 index 000000000..ae609602d --- /dev/null +++ b/changelog.d/context-cleanup.skip @@ -0,0 +1 @@ +litepub-0.1.jsonld cleanup diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3569165a4..4e82d4b01 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -4,46 +4,55 @@ "https://w3id.org/security/v1", "https://purl.archive.org/socialweb/webfinger", { - "Emoji": "toot:Emoji", + "as": "https://www.w3.org/ns/activitystreams#", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + + "fedibird": "http://fedibird.com/ns#", + "litepub": "http://litepub.social/ns#", + "sm": "http://smithereen.software/ns#", + "toot": "http://joinmastodon.org/ns#", + + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, "Hashtag": "as:Hashtag", - "PropertyValue": "schema:PropertyValue", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "quoteUrl": "as:quoteUrl", + "sensitive": "as:sensitive", + "atomUri": "ostatus:atomUri", "conversation": { "@id": "ostatus:conversation", "@type": "@id" }, - "discoverable": "toot:discoverable", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "capabilities": "litepub:capabilities", - "ostatus": "http://ostatus.org#", - "schema": "http://schema.org#", - "toot": "http://joinmastodon.org/ns#", - "fedibird": "http://fedibird.com/ns#", + + "PropertyValue": "schema:PropertyValue", "value": "schema:value", - "sensitive": "as:sensitive", - "litepub": "http://litepub.social/ns#", - "invisible": "litepub:invisible", + + "quoteUri": "fedibird:quoteUri", + + "capabilities": "litepub:capabilities", + "ChatMessage": "litepub:ChatMessage", "directMessage": "litepub:directMessage", + "EmojiReact": "litepub:EmojiReact", + "formerRepresentations": "litepub:formerRepresentations", + "invisible": "litepub:invisible", "listMessage": { "@id": "litepub:listMessage", "@type": "@id" }, - "quoteUrl": "as:quoteUrl", - "quoteUri": "fedibird:quoteUri", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" }, - "EmojiReact": "litepub:EmojiReact", - "ChatMessage": "litepub:ChatMessage", - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" - }, - "vcard": "http://www.w3.org/2006/vcard/ns#", - "formerRepresentations": "litepub:formerRepresentations", - "sm": "http://smithereen.software/ns#", - "nonAnonymous": "sm:nonAnonymous" + + "nonAnonymous": "sm:nonAnonymous", + + "discoverable": "toot:discoverable", + "Emoji": "toot:Emoji" } ] -} +} \ No newline at end of file From fc5aea73ff094a4b44599d20c0498688f039d3f4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 7 Apr 2026 14:25:48 +0200 Subject: [PATCH 264/317] Woodpecker CI: Add develop Docker image build pipeline --- .woodpecker/docker-develop.yaml | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .woodpecker/docker-develop.yaml diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml new file mode 100644 index 000000000..c50fd7eb1 --- /dev/null +++ b/.woodpecker/docker-develop.yaml @@ -0,0 +1,45 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + docker-develop-amd64: + image: woodpeckerci/plugin-docker-buildx:6.0.4 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + settings: + repo: git.pleroma.social/pleroma/pleroma + tags: [latest-amd64, develop-amd64] + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-develop-arm64: + image: woodpeckerci/plugin-docker-buildx:6.0.4 + when: + - evaluate: platform == "linux/arm64" + settings: + repo: git.pleroma.social/pleroma/pleroma + tags: [latest-arm64, develop-arm64] + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password From 5351cd4ce98511113a7bb132f43b2b3c46c00041 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 7 Apr 2026 19:01:26 +0200 Subject: [PATCH 265/317] Woodpecker CI: Add OTP develop pipeline musl and glibc builds need to be split due to workspace polution. Workflow's steps share the same workspace, so two separate build steps can't be in the same workflow since they share the buld artifacts, deps. --- .woodpecker/otp-develop-musl.yaml | 85 ++++++++++++++++++++++++++++++ .woodpecker/otp-develop.yaml | 87 +++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .woodpecker/otp-develop-musl.yaml create mode 100644 .woodpecker/otp-develop.yaml diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml new file mode 100644 index 000000000..74703e759 --- /dev/null +++ b/.woodpecker/otp-develop-musl.yaml @@ -0,0 +1,85 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + otp-develop-amd64-musl: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + + upload-artifacts-amd64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + + otp-develop-arm64-musl: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + + upload-artifacts-arm64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml new file mode 100644 index 000000000..20a81569d --- /dev/null +++ b/.woodpecker/otp-develop.yaml @@ -0,0 +1,87 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + otp-develop-amd64: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + + upload-artifacts-amd64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + + otp-develop-arm64: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + + upload-artifacts-arm64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From d2f7c9252f5d4e92144ca434f9643728a836ed86 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 9 Apr 2026 21:47:51 +0200 Subject: [PATCH 266/317] Woodpecker CI Docker develop: Switch to kaniko --- .woodpecker/docker-develop.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index c50fd7eb1..92299ca7c 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -15,7 +15,7 @@ labels: steps: docker-develop-amd64: - image: woodpeckerci/plugin-docker-buildx:6.0.4 + image: woodpeckerci/plugin-kaniko:2.3.1 # when: # - platform: linux/amd64 # does not work even though it should according to docs @@ -23,7 +23,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - repo: git.pleroma.social/pleroma/pleroma + repo: pleroma/pleroma tags: [latest-amd64, develop-amd64] registry: git.pleroma.social username: @@ -32,11 +32,11 @@ steps: from_secret: pleroma-ci-password docker-develop-arm64: - image: woodpeckerci/plugin-docker-buildx:6.0.4 + image: woodpeckerci/plugin-kaniko:2.3.1 when: - evaluate: platform == "linux/arm64" settings: - repo: git.pleroma.social/pleroma/pleroma + repo: pleroma/pleroma tags: [latest-arm64, develop-arm64] registry: git.pleroma.social username: From e2adc796c40d23881640533ed1c3250ae451d089 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 9 Apr 2026 23:34:49 +0200 Subject: [PATCH 267/317] Woodpecker CI: Multiplatform Docker image manifests --- .woodpecker/docker-develop-combine.yaml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .woodpecker/docker-develop-combine.yaml diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml new file mode 100644 index 000000000..f79e6b59b --- /dev/null +++ b/.woodpecker/docker-develop-combine.yaml @@ -0,0 +1,34 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +depends_on: + - docker-develop + +skip_clone: true + +steps: + docker-develop-combine: + image: docker.io/docker:cli + environment: + BUILD_ARCHES: "amd64 arm64" + REGISTRY: "git.fluffytail.org" + IMAGE_PATH: "git.fluffytail.org/pleroma-test/pleroma" + REGISTRY_USER: + from_secret: pleroma-ci-user + REGISTRY_PASSWORD: + from_secret: pleroma-ci-password + commands: + - set +x + - mkdir -p ~/.docker + - echo "{\"auths\":{\"$REGISTRY\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json + - set -x + - IMAGES_DEVELOP=; for arch in $BUILD_ARCHES; do IMAGES_DEVELOP="$IMAGES_DEVELOP $IMAGE_PATH:develop-$arch"; done + - IMAGES_LATEST=; for arch in $BUILD_ARCHES; do IMAGES_LATEST="$IMAGES_LATEST $IMAGE_PATH:latest-$arch"; done + - echo $IMAGES_DEVELOP + - echo $IMAGES_LATEST + - docker manifest create $IMAGE_PATH:develop $IMAGES_DEVELOP + - docker manifest push $IMAGE_PATH:develop + - docker manifest create $IMAGE_PATH:latest $IMAGES_LATEST + - docker manifest push $IMAGE_PATH:latest From 67e7f788c9b6688cda039f9ee4473522367ced72 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 00:00:10 +0200 Subject: [PATCH 268/317] Woodpecker CI Docker Develop combine: Switch to plugin Replaces manual tagging handling with a plugin, mostly to avoid dealing with echoed out secrets in the job log, which should be censored automatically, but who knows when that breaks... --- .woodpecker/docker-develop-combine.yaml | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml index f79e6b59b..67da6ca12 100644 --- a/.woodpecker/docker-develop-combine.yaml +++ b/.woodpecker/docker-develop-combine.yaml @@ -10,25 +10,13 @@ skip_clone: true steps: docker-develop-combine: - image: docker.io/docker:cli - environment: - BUILD_ARCHES: "amd64 arm64" - REGISTRY: "git.fluffytail.org" - IMAGE_PATH: "git.fluffytail.org/pleroma-test/pleroma" - REGISTRY_USER: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + settings: + registry: "git.fluffytail.org" + image: "pleroma-test/pleroma" + architectures: [amd64, arm64] + tags: [latest, develop] + username: from_secret: pleroma-ci-user - REGISTRY_PASSWORD: + password: from_secret: pleroma-ci-password - commands: - - set +x - - mkdir -p ~/.docker - - echo "{\"auths\":{\"$REGISTRY\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json - - set -x - - IMAGES_DEVELOP=; for arch in $BUILD_ARCHES; do IMAGES_DEVELOP="$IMAGES_DEVELOP $IMAGE_PATH:develop-$arch"; done - - IMAGES_LATEST=; for arch in $BUILD_ARCHES; do IMAGES_LATEST="$IMAGES_LATEST $IMAGE_PATH:latest-$arch"; done - - echo $IMAGES_DEVELOP - - echo $IMAGES_LATEST - - docker manifest create $IMAGE_PATH:develop $IMAGES_DEVELOP - - docker manifest push $IMAGE_PATH:develop - - docker manifest create $IMAGE_PATH:latest $IMAGES_LATEST - - docker manifest push $IMAGE_PATH:latest From 13d6246ed9e77e5cd5f812c6b22859d8a310ee0e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 22:19:56 +0200 Subject: [PATCH 269/317] Woodpecker CI: Cleanup develop releases CI code duplication --- .woodpecker/docker-develop.yaml | 23 ++++++++-------- .woodpecker/otp-develop-musl.yaml | 44 ++++++++++++++----------------- .woodpecker/otp-develop.yaml | 44 ++++++++++++++----------------- 3 files changed, 51 insertions(+), 60 deletions(-) diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index 92299ca7c..1a61b1626 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -13,6 +13,15 @@ matrix: labels: platform: ${platform} +variables: + docker_variables: &docker_variables + repo: pleroma/pleroma + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + steps: docker-develop-amd64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -23,23 +32,13 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - repo: pleroma/pleroma + <<: *docker_variables tags: [latest-amd64, develop-amd64] - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 when: - evaluate: platform == "linux/arm64" settings: - repo: pleroma/pleroma + <<: *docker_variables tags: [latest-arm64, develop-arm64] - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml index 74703e759..7ed567949 100644 --- a/.woodpecker/otp-develop-musl.yaml +++ b/.woodpecker/otp-develop-musl.yaml @@ -13,6 +13,22 @@ matrix: labels: platform: ${platform} +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + steps: otp-develop-amd64-musl: image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -27,13 +43,7 @@ steps: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS commands: - apk add git build-base cmake file-dev openssl vips-dev zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release upload-artifacts-amd64-musl: @@ -41,11 +51,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip @@ -60,13 +66,7 @@ steps: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS commands: - apk add git build-base cmake file-dev openssl vips-dev zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release upload-artifacts-arm64-musl: @@ -74,11 +74,7 @@ steps: when: - evaluate: platform == "linux/arm64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml index 20a81569d..6cd76d450 100644 --- a/.woodpecker/otp-develop.yaml +++ b/.woodpecker/otp-develop.yaml @@ -13,6 +13,22 @@ matrix: labels: platform: ${platform} +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + steps: otp-develop-amd64: image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -28,13 +44,7 @@ steps: DEBIAN_FRONTEND: noninteractive commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release upload-artifacts-amd64: @@ -42,11 +52,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip @@ -62,13 +68,7 @@ steps: DEBIAN_FRONTEND: noninteractive commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release upload-artifacts-arm64: @@ -76,11 +76,7 @@ steps: when: - evaluate: platform == "linux/arm64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From f00c13602d734f4215e2ac6c54a6cbbff12e5d40 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 22:44:40 +0200 Subject: [PATCH 270/317] Woodpecker CI Develop: Also tag images using commit sha With the commit sha being present, `tags` now has to be a list instead of an array, otherwise Woodpecker raises a yaml compiler warning: yaml: line 17: did not find expected ',' or ']' --- .woodpecker/docker-develop-combine.yaml | 5 ++++- .woodpecker/docker-develop.yaml | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml index 67da6ca12..ebe5a502c 100644 --- a/.woodpecker/docker-develop-combine.yaml +++ b/.woodpecker/docker-develop-combine.yaml @@ -15,7 +15,10 @@ steps: registry: "git.fluffytail.org" image: "pleroma-test/pleroma" architectures: [amd64, arm64] - tags: [latest, develop] + tags: + - latest + - develop + - ${CI_COMMIT_SHA} username: from_secret: pleroma-ci-user password: diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index 1a61b1626..4f6bbdf12 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -33,7 +33,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *docker_variables - tags: [latest-amd64, develop-amd64] + tags: + - latest-amd64 + - develop-amd64 + - ${CI_COMMIT_SHA}-amd64 docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -41,4 +44,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *docker_variables - tags: [latest-arm64, develop-arm64] + tags: + - latest-arm64 + - develop-arm64 + - ${CI_COMMIT_SHA}-arm64 From e002650e23f4e1afe7d79585f8e6c6177a34f97d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 13 Apr 2026 15:15:45 +0200 Subject: [PATCH 271/317] Woodpecker CI: Add Docker stable releases --- .woodpecker/docker-stable-combine.yaml | 27 +++++++++++++ .woodpecker/docker-stable.yaml | 52 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 .woodpecker/docker-stable-combine.yaml create mode 100644 .woodpecker/docker-stable.yaml diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml new file mode 100644 index 000000000..e011750f1 --- /dev/null +++ b/.woodpecker/docker-stable-combine.yaml @@ -0,0 +1,27 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +depends_on: + - docker-stable + +skip_clone: true + +steps: + docker-stable-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + settings: + registry: "git.fluffytail.org" + image: "pleroma-test/pleroma" + architectures: [amd64, arm64] + tags: + - latest + - stable + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml new file mode 100644 index 000000000..6c5e31091 --- /dev/null +++ b/.woodpecker/docker-stable.yaml @@ -0,0 +1,52 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + docker_variables: &docker_variables + repo: pleroma-test/pleroma + registry: git.fluffytail.org + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + +steps: + docker-stable-amd64: + image: woodpeckerci/plugin-kaniko:2.3.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *docker_variables + tags: + - latest-amd64 + - stable-amd64 + - ${CI_COMMIT_SHA}-amd64 + + docker-stable-arm64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *docker_variables + tags: + - latest-arm64 + - stable-arm64 + - ${CI_COMMIT_SHA}-arm64 From 97a2e8c7647bbf40e554fb1ee40ef8160525dfd9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 13 Apr 2026 17:17:28 +0200 Subject: [PATCH 272/317] Woodpecker CI: Tag stable docker release with version tag --- .woodpecker/docker-stable-combine.yaml | 16 +++++++++++-- .woodpecker/docker-stable.yaml | 33 +++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml index e011750f1..e07e7a83c 100644 --- a/.woodpecker/docker-stable-combine.yaml +++ b/.woodpecker/docker-stable-combine.yaml @@ -13,11 +13,13 @@ skip_clone: true steps: docker-stable-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - settings: + when: + - event: push + settings: &docker_settings registry: "git.fluffytail.org" image: "pleroma-test/pleroma" architectures: [amd64, arm64] - tags: + tags: &docker_tags - latest - stable - ${CI_COMMIT_SHA} @@ -25,3 +27,13 @@ steps: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password + + docker-stable-tag-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: tag + settings: + <<: *docker_settings + tags: + - <<: *docker_tags + - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml index 6c5e31091..dcf98e31f 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker-stable.yaml @@ -23,30 +23,51 @@ variables: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password + kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 steps: docker-stable-amd64: - image: woodpeckerci/plugin-kaniko:2.3.1 + image: *kaniko_image # when: # - platform: linux/amd64 # does not work even though it should according to docs # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' settings: <<: *docker_variables - tags: + tags: &amd64_tags - latest-amd64 - stable-amd64 - ${CI_COMMIT_SHA}-amd64 - docker-stable-arm64: - image: woodpeckerci/plugin-kaniko:2.3.1 + docker-stable-tag-amd64: + image: *kaniko_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' settings: <<: *docker_variables tags: + - <<: *amd64_tags + - ${CI_COMMIT_TAG}-amd64 + + docker-stable-arm64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' + settings: + <<: *docker_variables + tags: &arm64_tags - latest-arm64 - stable-arm64 - ${CI_COMMIT_SHA}-arm64 + + docker-stable-tag-arm64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *docker_variables + tags: + - <<: *arm64_tags + - ${CI_COMMIT_TAG}-arm64 From 42eb9706a5cb2a76d9093852735459371d647fb1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 17 Apr 2026 19:39:53 +0200 Subject: [PATCH 273/317] Woodpecker CI: Build stable OTP releases --- .woodpecker/otp-stable-musl.yaml | 83 +++++++++++++++++++++++++++++++ .woodpecker/otp-stable.yaml | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 .woodpecker/otp-stable-musl.yaml create mode 100644 .woodpecker/otp-stable.yaml diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml new file mode 100644 index 000000000..f97bde4ef --- /dev/null +++ b/.woodpecker/otp-stable-musl.yaml @@ -0,0 +1,83 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma-test' + +steps: + otp-stable-amd64-musl: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip release + + upload-artifacts-amd64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: stable-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + + otp-stable-arm64-musl: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip release + + upload-artifacts-arm64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: stable-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml new file mode 100644 index 000000000..6219ff403 --- /dev/null +++ b/.woodpecker/otp-stable.yaml @@ -0,0 +1,85 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma-test' + +steps: + otp-stable-amd64: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64.zip release + + upload-artifacts-amd64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: stable-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + + otp-stable-arm64: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64.zip release + + upload-artifacts-arm64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: stable-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip From eea01b54b7852628142dae3af9a280ee92620248 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 18 Apr 2026 22:28:40 +0200 Subject: [PATCH 274/317] Woodpecker CI: Allow running stable release jobs manually Also allows Docker images to be tagged with a version in manual jobs when CI_COMMIT_TAG is manually specified --- .woodpecker/docker-stable-combine.yaml | 4 ++++ .woodpecker/docker-stable.yaml | 6 ++++++ .woodpecker/otp-stable-musl.yaml | 2 ++ .woodpecker/otp-stable.yaml | 2 ++ 4 files changed, 14 insertions(+) diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml index e07e7a83c..9f144d3f6 100644 --- a/.woodpecker/docker-stable-combine.yaml +++ b/.woodpecker/docker-stable-combine.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable depends_on: - docker-stable @@ -15,6 +17,7 @@ steps: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - event: push + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: &docker_settings registry: "git.fluffytail.org" image: "pleroma-test/pleroma" @@ -32,6 +35,7 @@ steps: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_settings tags: diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml index dcf98e31f..bd32b94d1 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker-stable.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: @@ -34,6 +36,7 @@ steps: # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &amd64_tags @@ -45,6 +48,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: @@ -55,6 +59,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &arm64_tags @@ -66,6 +71,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index f97bde4ef..4663a4401 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index 6219ff403..afb23eada 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: From dd29b9c11bdef53a81071f530b7416970baa8e70 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 18 Apr 2026 23:25:53 +0200 Subject: [PATCH 275/317] Woodpecker CI OTP: use CI_COMMIT_BRANCH variable instead of stable --- .woodpecker/otp-stable-musl.yaml | 20 ++++++++++---------- .woodpecker/otp-stable.yaml | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index 4663a4401..96be0bf8e 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -48,7 +48,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -56,10 +56,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-amd64-musl - package_version: stable-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip otp-stable-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -71,7 +71,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -79,7 +79,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-arm64-musl - package_version: stable-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index afb23eada..ae2670a79 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -49,7 +49,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -57,10 +57,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-amd64 - package_version: stable-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip otp-stable-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -73,7 +73,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -81,7 +81,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-arm64 - package_version: stable-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From 89a78d765cc231934608149f394389a9b90e840c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 16:09:15 +0200 Subject: [PATCH 276/317] Woodpecker CI: Unify Docker image workflows --- .woodpecker/docker-combine.yaml | 65 +++++++++++++++++++ .woodpecker/docker-develop-combine.yaml | 25 ------- .woodpecker/docker-develop.yaml | 50 -------------- .woodpecker/docker-stable-combine.yaml | 43 ------------ .../{docker-stable.yaml => docker.yaml} | 50 +++++++++----- 5 files changed, 100 insertions(+), 133 deletions(-) create mode 100644 .woodpecker/docker-combine.yaml delete mode 100644 .woodpecker/docker-develop-combine.yaml delete mode 100644 .woodpecker/docker-develop.yaml delete mode 100644 .woodpecker/docker-stable-combine.yaml rename .woodpecker/{docker-stable.yaml => docker.yaml} (58%) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml new file mode 100644 index 000000000..46401b0cb --- /dev/null +++ b/.woodpecker/docker-combine.yaml @@ -0,0 +1,65 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + - event: manual + branch: stable + +depends_on: + - docker + +skip_clone: true + +steps: + docker-develop-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + settings: + registry: "git.pleroma.social" + image: "pleroma/pleroma" + architectures: [amd64, arm64] + tags: + - latest + - develop + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-stable-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: push + branch: stable + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' + settings: &docker_settings + registry: "git.pleroma.social" + image: "pleroma/pleroma" + architectures: [amd64, arm64] + tags: &docker_tags + - latest + - stable + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-stable-tag-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' + settings: + <<: *docker_settings + tags: + - <<: *docker_tags + - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml deleted file mode 100644 index ebe5a502c..000000000 --- a/.woodpecker/docker-develop-combine.yaml +++ /dev/null @@ -1,25 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -depends_on: - - docker-develop - -skip_clone: true - -steps: - docker-develop-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - settings: - registry: "git.fluffytail.org" - image: "pleroma-test/pleroma" - architectures: [amd64, arm64] - tags: - - latest - - develop - - ${CI_COMMIT_SHA} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml deleted file mode 100644 index 4f6bbdf12..000000000 --- a/.woodpecker/docker-develop.yaml +++ /dev/null @@ -1,50 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - docker_variables: &docker_variables - repo: pleroma/pleroma - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - -steps: - docker-develop-amd64: - image: woodpeckerci/plugin-kaniko:2.3.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *docker_variables - tags: - - latest-amd64 - - develop-amd64 - - ${CI_COMMIT_SHA}-amd64 - - docker-develop-arm64: - image: woodpeckerci/plugin-kaniko:2.3.1 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *docker_variables - tags: - - latest-arm64 - - develop-arm64 - - ${CI_COMMIT_SHA}-arm64 diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml deleted file mode 100644 index 9f144d3f6..000000000 --- a/.woodpecker/docker-stable-combine.yaml +++ /dev/null @@ -1,43 +0,0 @@ -when: - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: tag - branch: stable - - event: manual - branch: stable - -depends_on: - - docker-stable - -skip_clone: true - -steps: - docker-stable-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - when: - - event: push - - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' - settings: &docker_settings - registry: "git.fluffytail.org" - image: "pleroma-test/pleroma" - architectures: [amd64, arm64] - tags: &docker_tags - - latest - - stable - - ${CI_COMMIT_SHA} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - - docker-stable-tag-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - when: - - event: tag - - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' - settings: - <<: *docker_settings - tags: - - <<: *docker_tags - - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker.yaml similarity index 58% rename from .woodpecker/docker-stable.yaml rename to .woodpecker/docker.yaml index bd32b94d1..c1299b4fe 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -13,14 +16,13 @@ matrix: - linux/arm64 # This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore labels: platform: ${platform} variables: docker_variables: &docker_variables - repo: pleroma-test/pleroma - registry: git.fluffytail.org + repo: pleroma/pleroma + registry: git.pleroma.social username: from_secret: pleroma-ci-user password: @@ -28,15 +30,33 @@ variables: kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 steps: + docker-develop-amd64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + tags: + - latest-amd64 + - develop-amd64 + - ${CI_COMMIT_SHA}-amd64 + + docker-develop-arm64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + tags: + - latest-arm64 + - develop-arm64 + - ${CI_COMMIT_SHA}-arm64 + docker-stable-amd64: image: *kaniko_image - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &amd64_tags @@ -47,8 +67,8 @@ steps: docker-stable-tag-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: @@ -58,8 +78,8 @@ steps: docker-stable-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &arm64_tags @@ -70,8 +90,8 @@ steps: docker-stable-tag-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: From d8b8cbbb8d74938159bdf50c190bf54eba276557 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 18:33:39 +0200 Subject: [PATCH 277/317] Woodpecker CI: Shorten commit sha to eight chars This will hopefully help with avoiding: https://github.com/woodpecker-ci/woodpecker/issues/5450 --- .woodpecker/docker-combine.yaml | 4 ++-- .woodpecker/docker.yaml | 8 ++++---- .woodpecker/otp-develop-musl.yaml | 16 ++++++++-------- .woodpecker/otp-develop.yaml | 16 ++++++++-------- .woodpecker/otp-stable-musl.yaml | 16 ++++++++-------- .woodpecker/otp-stable.yaml | 16 ++++++++-------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 46401b0cb..c69bcf1cf 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -28,7 +28,7 @@ steps: tags: - latest - develop - - ${CI_COMMIT_SHA} + - ${CI_COMMIT_SHA:0:8} username: from_secret: pleroma-ci-user password: @@ -47,7 +47,7 @@ steps: tags: &docker_tags - latest - stable - - ${CI_COMMIT_SHA} + - ${CI_COMMIT_SHA:0:8} username: from_secret: pleroma-ci-user password: diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index c1299b4fe..66efe6903 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -39,7 +39,7 @@ steps: tags: - latest-amd64 - develop-amd64 - - ${CI_COMMIT_SHA}-amd64 + - ${CI_COMMIT_SHA:0:8}-amd64 docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -50,7 +50,7 @@ steps: tags: - latest-arm64 - develop-arm64 - - ${CI_COMMIT_SHA}-arm64 + - ${CI_COMMIT_SHA:0:8}-arm64 docker-stable-amd64: image: *kaniko_image @@ -62,7 +62,7 @@ steps: tags: &amd64_tags - latest-amd64 - stable-amd64 - - ${CI_COMMIT_SHA}-amd64 + - ${CI_COMMIT_SHA:0:8}-amd64 docker-stable-tag-amd64: image: *kaniko_image @@ -85,7 +85,7 @@ steps: tags: &arm64_tags - latest-arm64 - stable-arm64 - - ${CI_COMMIT_SHA}-arm64 + - ${CI_COMMIT_SHA:0:8}-arm64 docker-stable-tag-arm64: image: *kaniko_image diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml index 7ed567949..aff52ee30 100644 --- a/.woodpecker/otp-develop-musl.yaml +++ b/.woodpecker/otp-develop-musl.yaml @@ -44,7 +44,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -53,9 +53,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip otp-develop-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -67,7 +67,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -76,6 +76,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml index 6cd76d450..623d1d846 100644 --- a/.woodpecker/otp-develop.yaml +++ b/.woodpecker/otp-develop.yaml @@ -45,7 +45,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -54,9 +54,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip otp-develop-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -69,7 +69,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -78,6 +78,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index 96be0bf8e..3a3d8bb53 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -48,7 +48,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -57,9 +57,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip otp-stable-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -71,7 +71,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -80,6 +80,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index ae2670a79..80bfc9f7e 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -49,7 +49,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -58,9 +58,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip otp-stable-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -73,7 +73,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -82,6 +82,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip From 5229e8ae6531d4b78828ad43ce17e2e075080c04 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 21:40:56 +0200 Subject: [PATCH 278/317] Woodpecker CI: Unify OTP builds into a single worfklow --- .woodpecker/otp-develop-musl.yaml | 81 ------------------ .woodpecker/otp-develop.yaml | 83 ------------------- .../{otp-stable-musl.yaml => otp-musl.yaml} | 72 +++++++++------- .woodpecker/{otp-stable.yaml => otp.yaml} | 75 ++++++++++------- 4 files changed, 87 insertions(+), 224 deletions(-) delete mode 100644 .woodpecker/otp-develop-musl.yaml delete mode 100644 .woodpecker/otp-develop.yaml rename .woodpecker/{otp-stable-musl.yaml => otp-musl.yaml} (56%) rename .woodpecker/{otp-stable.yaml => otp.yaml} (54%) diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml deleted file mode 100644 index aff52ee30..000000000 --- a/.woodpecker/otp-develop-musl.yaml +++ /dev/null @@ -1,81 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - pleroma_build_cmds: &pleroma_build_cmds - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - -steps: - otp-develop-amd64-musl: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release - - upload-artifacts-amd64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - - otp-develop-arm64-musl: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release - - upload-artifacts-arm64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml deleted file mode 100644 index 623d1d846..000000000 --- a/.woodpecker/otp-develop.yaml +++ /dev/null @@ -1,83 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - pleroma_build_cmds: &pleroma_build_cmds - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - -steps: - otp-develop-amd64: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release - - upload-artifacts-amd64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - - otp-develop-arm64: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release - - upload-artifacts-arm64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-musl.yaml similarity index 56% rename from .woodpecker/otp-stable-musl.yaml rename to .woodpecker/otp-musl.yaml index 3a3d8bb53..87ab0b93a 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -18,7 +21,8 @@ labels: platform: ${platform} variables: - pleroma_build_cmds: &pleroma_build_cmds + build_cmds: &build_cmds + - apk add git build-base cmake file-dev openssl vips-dev zip - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -26,32 +30,54 @@ variables: - mkdir release - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release + build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings user: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password - owner: 'pleroma-test' + owner: 'pleroma' + env: &env + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS steps: - otp-stable-amd64-musl: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + otp-develop-amd64-musl: + image: *build_image_amd64 when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &amd64_build + - <<: *build_cmds - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-stable-amd64-musl: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *amd64_build + + otp-develop-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm64_build + - <<: *build_cmds + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + + otp-stable-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm64_build + upload-artifacts-amd64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/amd64" settings: @@ -61,20 +87,8 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - otp-stable-arm64-musl: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release - upload-artifacts-arm64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/arm64" settings: diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp.yaml similarity index 54% rename from .woodpecker/otp-stable.yaml rename to .woodpecker/otp.yaml index 80bfc9f7e..533143769 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -18,7 +21,8 @@ labels: platform: ${platform} variables: - pleroma_build_cmds: &pleroma_build_cmds + build_cmds: &build_cmds + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -26,33 +30,55 @@ variables: - mkdir release - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release + build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings user: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password - owner: 'pleroma-test' + owner: 'pleroma' + env: &env + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive steps: - otp-stable-amd64: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + otp-develop-amd64: + image: *build_image_amd64 when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &amd64_build + - <<: *build_cmds - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-stable-amd64: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *amd64_build + + otp-develop-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm64_build + - <<: *build_cmds + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release + + otp-stable-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm64_build + upload-artifacts-amd64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/amd64" settings: @@ -62,21 +88,8 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - otp-stable-arm64: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release - upload-artifacts-arm64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/arm64" settings: From 2e968890de91bcc7df982c7ad5bac7ee8621505f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 11:35:34 +0200 Subject: [PATCH 279/317] Woodpecker CI: Remove branch requirement for tag Tag events don't have CI_COMMIT_BRANCH set, and neither can they be restricted to specific branches. The branch condition is ignored on tags. --- .woodpecker/docker-combine.yaml | 1 - .woodpecker/docker.yaml | 5 ++- .woodpecker/otp-musl.yaml | 59 ++++++++++++++++++++++++++++++--- .woodpecker/otp.yaml | 59 ++++++++++++++++++++++++++++++--- 4 files changed, 110 insertions(+), 14 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index c69bcf1cf..e615ca7c2 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 66efe6903..0c4b18fbc 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -67,7 +66,7 @@ steps: docker-stable-tag-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables @@ -90,7 +89,7 @@ steps: docker-stable-tag-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 87ab0b93a..d611f9f13 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -56,10 +55,21 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *amd64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-amd64-musl: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-develop-arm64-musl: image: *build_image_arm64 when: @@ -72,14 +82,27 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *arm64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + upload-artifacts-amd64-musl: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl @@ -87,13 +110,39 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + upload-artifacts-arm64-musl: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 533143769..0d0b08bec 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -57,10 +56,21 @@ steps: otp-stable-amd64: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *amd64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-amd64: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-develop-arm64: image: *build_image_arm64 when: @@ -73,14 +83,27 @@ steps: otp-stable-arm64: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *arm64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip release + upload-artifacts-amd64: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 @@ -88,13 +111,39 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + upload-artifacts-arm64: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip From 16b7a95c481233e2b4e02e90128fedce817852f8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 15:31:46 +0200 Subject: [PATCH 280/317] Woodpecker CI: Run Docker image workflows also on Dockerfile changes --- .woodpecker/docker-combine.yaml | 4 ++-- .woodpecker/docker.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index e615ca7c2..147cb1d55 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -1,10 +1,10 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: push branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 0c4b18fbc..f178fb840 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,10 +1,10 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: push branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable From 209b9c0a1e9bd7c822213f1a4cad745605ae31b2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 21:04:31 +0200 Subject: [PATCH 281/317] Woodpecker CI: Shorten zip archive names further Hopefully this will also help with the workflows randomly failing to create the zip archive due to the Woodpecker bug. --- .woodpecker/otp-musl.yaml | 16 ++++++++-------- .woodpecker/otp.yaml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index d611f9f13..44d44a662 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -50,7 +50,7 @@ steps: environment: *env commands: &amd64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release otp-stable-amd64-musl: image: *build_image_amd64 @@ -68,7 +68,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release otp-develop-arm64-musl: image: *build_image_arm64 @@ -77,7 +77,7 @@ steps: environment: *env commands: &arm64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release otp-stable-arm64-musl: image: *build_image_arm64 @@ -95,7 +95,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-amd64-musl: image: *artifacts_uploader_image @@ -107,7 +107,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -119,7 +119,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-amd64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip upload-artifacts-arm64-musl: @@ -132,7 +132,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -144,5 +144,5 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-arm64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 0d0b08bec..008c77f95 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -51,7 +51,7 @@ steps: environment: *env commands: &amd64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release otp-stable-amd64: image: *build_image_amd64 @@ -69,7 +69,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release otp-develop-arm64: image: *build_image_arm64 @@ -78,7 +78,7 @@ steps: environment: *env commands: &arm64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release otp-stable-arm64: image: *build_image_arm64 @@ -96,7 +96,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-amd64: image: *artifacts_uploader_image @@ -108,7 +108,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -120,7 +120,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-amd64 package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip upload-artifacts-arm64: @@ -133,7 +133,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -145,5 +145,5 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-arm64 package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip From 7f97e21910ea9435febda3c36a67a467af8e2993 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 15:04:12 +0200 Subject: [PATCH 282/317] pleroma_ctl: Properly handle user arguments with whitespace When user supplied arguments to pleroma_ctl include whitespace that has been properly quoted, all arguments were sent to ReleaseTasks in one string, which then String.split/1 the input on any whitespace. This broke Mix tasks that accept certain user input like instance gen and user management. To fix this, pleroma_ctl now sends the arguments in list form. Additionally pleroma_ctl arguments now need to be pre-processed. Fixes pleroma/pleroma#7874 --- lib/pleroma/release_tasks.ex | 19 ++++++++++++++++++- rel/files/bin/pleroma_ctl | 10 +++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex index af2d35c8f..49400940f 100644 --- a/lib/pleroma/release_tasks.ex +++ b/lib/pleroma/release_tasks.ex @@ -5,7 +5,10 @@ defmodule Pleroma.ReleaseTasks do @repo Pleroma.Repo - def run(args) do + # TODO: Kept for some backwards compatibility with buggy pleroma_ctl, + # if a mismatch between pleroma_ctl and Pleroma accidentaly happens. + # Remove in the future. + def run(args) when is_binary(args) do [task | args] = String.split(args) case task do @@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do end end + # HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with + # whitespace will be broken. Previously the broken string form above was used, + # escaping in the shell does not help. + def run(args) when is_list(args) do + [task | args] = args + + case task do + "migrate" -> migrate(args) + "create" -> create() + "rollback" -> rollback(args) + task -> mix_task(task, args) + end + end + def find_module(task) do module_name = task diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 6f0dba3a8..56dc13a06 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -137,7 +137,11 @@ else SCRIPT=$(realpath "$0") SCRIPTPATH=$(dirname "$SCRIPT") - FULL_ARGS="$*" + # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace. + # Previously it was sent as string, which would get split on whitespace on the task side. + # Escaping does not help including non-POSIX printf %q + PREPARED_ARGS="" + for arg in "$@"; do PREPARED_ARGS="$PREPARED_ARGS \"$arg\","; done ACTION="$1" if [ $# -gt 0 ]; then @@ -154,8 +158,8 @@ else if [ "$ACTION" = "update" ]; then update "$@" elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then - "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' + "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])' else - "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' + "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])' fi fi From 95a33855d1b0363057112d3470dfb5d980c58eb5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 22:02:01 +0200 Subject: [PATCH 283/317] pleroma_ctl: Update update logic to Gitea API --- rel/files/bin/pleroma_ctl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 56dc13a06..2e114ff50 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -78,12 +78,17 @@ update() { RELEASE_ROOT=$(dirname "$SCRIPTPATH") uri="https://git.pleroma.social" - project_id="2" + project_name="pleroma" + api_base="${uri}/api/v1/packages/${project_name}/generic" + package_base="${uri}/api/packages/${project_name}/generic" project_branch="${BRANCH:-$(detect_branch)}" flavour="${FLAVOUR:-$(detect_flavour)}" + # API responds in JSON, optimistically try to make it one object per line + ver=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/-/latest | tr ',' '\n' | grep '"version":' | cut -d':' -f2 | tr -d '"') + file=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/files | tr ',' '\n' | grep '"name":' | cut -d':' -f2 | tr -d '"') + full_uri=${FULL_URI:-"${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/"${file}"} tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" - full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}" echo "Downloading the artifact from ${full_uri} to ${artifact}" curl "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" From cafd75b072ef408cc30ddaecdd003827274cdcf8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 23:15:00 +0200 Subject: [PATCH 284/317] Woodpecker CI docker-combine: Hoist docker_settings anchor --- .woodpecker/docker-combine.yaml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 147cb1d55..6fa9583ba 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -20,7 +20,7 @@ steps: when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - settings: + settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" architectures: [amd64, arm64] @@ -39,18 +39,12 @@ steps: - event: push branch: stable - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' - settings: &docker_settings - registry: "git.pleroma.social" - image: "pleroma/pleroma" - architectures: [amd64, arm64] - tags: &docker_tags + settings: + <<: *docker_settings + tags: &stable_docker_tags - latest - stable - ${CI_COMMIT_SHA:0:8} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password docker-stable-tag-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest @@ -60,5 +54,5 @@ steps: settings: <<: *docker_settings tags: - - <<: *docker_tags + - <<: *stable_docker_tags - ${CI_COMMIT_TAG} From 25e543d44d7846125b773ed81b59dcc27137afd5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 23:38:29 +0200 Subject: [PATCH 285/317] changelog --- changelog.d/woodpecker-release-pipeline.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/woodpecker-release-pipeline.skip diff --git a/changelog.d/woodpecker-release-pipeline.skip b/changelog.d/woodpecker-release-pipeline.skip new file mode 100644 index 000000000..e69de29bb From a996d25b8459b5a8f369430106950a2ee9bdf881 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 25 Apr 2026 11:08:28 +0200 Subject: [PATCH 286/317] Woodpecker CI Docker: label workflow as high memory --- .woodpecker/docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index f178fb840..703f3bbfc 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -17,6 +17,7 @@ matrix: # This is needed for the when clauses below. labels: platform: ${platform} + memory: 'high' variables: docker_variables: &docker_variables From e4632eced36c368ec62f38bb4e05bc4cc6602559 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 25 Apr 2026 13:33:58 +0200 Subject: [PATCH 287/317] Woodpecker CI: Only run stable release pipelines on tag events Removes possible races when uploading images/bundles and purposeful pipeline failures when both a push and tag happened (OTP bundles do not allow overwriting). --- .woodpecker/docker-combine.yaml | 5 ----- .woodpecker/docker.yaml | 5 ----- .woodpecker/otp-musl.yaml | 11 ++--------- .woodpecker/otp.yaml | 11 ++--------- 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 6fa9583ba..be8583763 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable @@ -36,8 +33,6 @@ steps: docker-stable-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - - event: push - branch: stable - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_settings diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 703f3bbfc..317cb5fbb 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable @@ -55,7 +52,6 @@ steps: docker-stable-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables @@ -78,7 +74,6 @@ steps: docker-stable-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 44d44a662..c58eee66b 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -55,8 +52,7 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *amd64_build @@ -82,8 +78,7 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *arm64_build @@ -101,7 +96,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings @@ -126,7 +120,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 008c77f95..c01c2e557 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -56,8 +53,7 @@ steps: otp-stable-amd64: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *amd64_build @@ -83,8 +79,7 @@ steps: otp-stable-arm64: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *arm64_build @@ -102,7 +97,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings @@ -127,7 +121,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings From cb2271978ec55c0714d6396f73394957b3704abf Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:17:59 +0200 Subject: [PATCH 288/317] UpdateValidator: fix tests --- .../object_validators/update_handling_test.exs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index 347c5b578..94c502ad6 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) end - test "returns an error if the object can't be updated by the actor", %{ + test "returns an error if the object can't be updated by the actor (different domain)", %{ valid_update: valid_update } do other_user = insert(:user, local: false) @@ -41,24 +41,26 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:error, _cng} = ObjectValidator.validate(update, []) end - test "validates as long as the object is same-origin with the actor", %{ + test "returns an error if the object can't be updated by the actor (same domain)", %{ + user: user, valid_update: valid_update } do - other_user = insert(:user) + user_ap_id = user.ap_id + user_domain = URI.parse(user_ap_id).host + other_user = insert(:user, local: false, domain: user_domain) update = valid_update |> Map.put("actor", other_user.ap_id) - assert {:ok, _update, []} = ObjectValidator.validate(update, []) + assert {:error, _cng} = ObjectValidator.validate(update, []) end - test "validates if the object is not of an Actor type" do - note = insert(:note) + test "validates if the object is not of an Actor type", %{user: user} do + note = insert(:note, user: user) updated_note = note.data |> Map.put("content", "edited content") - other_user = insert(:user) - {:ok, update, _} = Builder.update(other_user, updated_note) + {:ok, update, _} = Builder.update(user, updated_note) assert {:ok, _update, _} = ObjectValidator.validate(update, []) end From af6d12c0a5be10291c9e2bc73cb29bc24c29115a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:18:54 +0200 Subject: [PATCH 289/317] UpdateValidator: Check Actor owns Object or updates itself --- .../object_validators/update_validator.ex | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index aab90235f..5586b74cf 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -75,15 +75,36 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do end end - # For remote Updates, verify the host is the same. + # For remote Updates, verify the Actor is the same def validate_updating_rights_remote(cng) do with actor = get_field(cng, :actor), object = get_field(cng, :object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), - actor_uri <- URI.parse(actor), - object_uri <- URI.parse(object_id), - true <- actor_uri.host == object_uri.host do - cng + entity <- + Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do + case entity do + # Actor must own Object to update it + %Object{} -> + if actor == entity.data["actor"] do + cng + else + cng + |> add_error(:object, "Can't be updated by this actor") + end + + # Actor must only be allowed to update itself + %User{} -> + if actor == entity.ap_id do + cng + else + cng + |> add_error(:object, "Can't be updated by this actor") + end + + true -> + cng + |> add_error(:object, "Update is neither for Object or Actor") + end else _e -> cng From da28a4c44109f1af944d23b87e16ea661b76d868 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:58:43 +0200 Subject: [PATCH 290/317] ReceiverWorker: Add cancels on actor does not match signature test --- test/pleroma/workers/receiver_worker_test.exs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 12abc1a27..1c886a74e 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -302,4 +302,48 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end end end + + test "cancels when signature actor does not match payload actor" do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + req_headers = [ + ["host", "example.com"], + ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], + ["digest", "SHA-256=fake-digest"], + ["content-type", "application/activity+json"], + [ + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ] + ] + + oban_job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => update, + "req_headers" => req_headers, + "request_path" => "/inbox", + "query_string" => "" + } + } + + with_mock Pleroma.Signature, [:passthrough], + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end do + assert {:cancel, :invalid_signature} = ReceiverWorker.perform(oban_job) + end + end end From 42683e79dfe89651d8f44feef6659a5ceaa78183 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 01:34:14 +0200 Subject: [PATCH 291/317] ReceiverWorker: Check that signature matches actor --- lib/pleroma/workers/receiver_worker.ex | 14 ++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index e2c950967..507e099c2 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @@ -27,6 +28,7 @@ defmodule Pleroma.Workers.ReceiverWorker do req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, method: method, params: params, req_headers: req_headers, @@ -37,6 +39,7 @@ defmodule Pleroma.Workers.ReceiverWorker do with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -67,6 +70,16 @@ defmodule Pleroma.Workers.ReceiverWorker do def timeout(_job), do: :timer.seconds(5) + defp validate_same_actor(conn_data) do + case MappedSignatureToIdentityPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true}} -> + true + + _ -> + false + end + end + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) defp process_errors(errors) do @@ -85,6 +98,7 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, {:reject, _} = reason} -> {:cancel, reason} # HTTP Sigs {:signature, false} -> {:cancel, :invalid_signature} + {:same_actor, false} -> {:cancel, :actor_signature_mismatch} # Origin / URL validation failed somewhere possibly due to spoofing {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # Unclear if this can be reached diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 1c886a74e..bc027ad4c 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -304,7 +304,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -343,7 +343,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do with_mock Pleroma.Signature, [:passthrough], refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, validate_signature: fn _conn -> true end do - assert {:cancel, :invalid_signature} = ReceiverWorker.perform(oban_job) + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end end From 80e72b79f57bad270c530d94527f760c14d8c152 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 14:31:06 +0400 Subject: [PATCH 292/317] Add spoofing regression tests --- .gitignore | 3 + .../activity_pub_controller_test.exs | 68 +++++ .../update_handling_test.exs | 26 ++ ...mapped_signature_to_identity_plug_test.exs | 10 +- test/pleroma/workers/receiver_worker_test.exs | 239 ++++++++++++++++++ 5 files changed, 342 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 355cea069..d8e5ed553 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ pleroma.iml # asdf .tool-versions +# mise +mise.toml + # Editor temp files *~ *# diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index d5947186f..42cb35669 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -726,6 +726,74 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-forged-note" + + data = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-create", + "context" => "https://example.com/contexts/inbox-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index 94c502ad6..f04f9cc61 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -64,6 +64,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, _} = ObjectValidator.validate(update, []) end + + test "returns an error if the remote update target is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/unknown", + "actor" => remote_user.ap_id, + "content" => "edited content", + "published" => "2024-07-25T13:33:31Z", + "updated" => "2024-07-25T13:34:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 33eff1bc5..81c6b0c5d 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -47,13 +47,15 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert %{valid_signature: false} == conn.assigns end - @tag skip: "known breakage; the testsuite presently depends on it" test "it considers a mapped identity to be invalid when the identity cannot be found" do + actor = "http://niu.moe/users/rye" + conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://niu.moe/users/rye") + build_conn(:post, "/doesntmattter", %{"actor" => actor}) + |> set_signature(actor) |> MappedSignatureToIdentityPlug.call(%{}) - assert %{valid_signature: false} == conn.assigns + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) end end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index bc027ad4c..9dccd739b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -14,6 +14,43 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker + defp mismatched_signature_headers do + [ + {"host", "example.com"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp assert_mismatched_signature_cancelled(params) do + with_mocks [ + {Pleroma.Signature, [:passthrough], + [ + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end + ]}, + {Pleroma.Web.Federator, [:passthrough], + [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} + ] do + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) + end + end + test "it does not retry MRF reject" do params = insert(:note).data @@ -346,4 +383,206 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end + + test "Federator preserves request metadata needed for ReceiverWorker signature checks" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "example.com"}, + {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert %{ + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels signature actor mismatch through Federator-created jobs" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update) + end + + test "cancels signature actor mismatch before processing a forged Create" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + object_id = "https://example.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-create", + "context" => "https://example.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: create, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: like, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo) + end end From 9c540995b4ca1bb33e38692bdf7504f9f4cdffc5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 15:36:55 +0400 Subject: [PATCH 293/317] Use Mox in spoofing regression tests --- .../activity_pub_controller_test.exs | 86 ++++++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 91 ++++++++++--------- 2 files changed, 136 insertions(+), 41 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 42cb35669..8cadf6686 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -794,6 +794,92 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post signed by a different actor", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-signed-forged-note" + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-signed-forged-create", + "context" => "https://example.com/contexts/inbox-signed-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-signed-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert conn.assigns.valid_signature == false + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like signed by a different actor", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-signed-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert conn.assigns.valid_signature == false + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 9dccd739b..5e18fe771 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker @@ -27,28 +28,34 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do ] end - defp assert_mismatched_signature_cancelled(params) do - with_mocks [ - {Pleroma.Signature, [:passthrough], - [ - refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, - validate_signature: fn _conn -> true end - ]}, - {Pleroma.Web.Federator, [:passthrough], - [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} - ] do - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: params, - query_string: "" - }) + defp expect_signature_from(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) - end + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + + defp assert_mismatched_signature_cancelled(params, signer) do + expect_signature_from(signer) + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end test "it does not retry MRF reject" do @@ -341,7 +348,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -377,11 +384,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } - with_mock Pleroma.Signature, [:passthrough], - refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, - validate_signature: fn _conn -> true end do - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - end + expect_signature_from(alice) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end test "Federator preserves request metadata needed for ReceiverWorker signature checks" do @@ -411,7 +416,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch through Federator-created jobs" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -425,11 +430,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data } - assert_mismatched_signature_cancelled(update) + assert_mismatched_signature_cancelled(update, alice) end test "cancels signature actor mismatch before processing a forged Create" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") create = %{ @@ -450,11 +455,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } - assert_mismatched_signature_cancelled(create) + assert_mismatched_signature_cancelled(create, alice) end test "cancels signature actor mismatch before actually creating a forged post" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") object_id = "https://example.com/objects/actually-forged-note" @@ -479,6 +484,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } + expect_signature_from(alice) + assert {:ok, oban_job} = Federator.incoming_ap_doc(%{ method: "POST", @@ -493,7 +500,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Like" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -506,11 +513,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } - assert_mismatched_signature_cancelled(like) + assert_mismatched_signature_cancelled(like, alice) end test "cancels signature actor mismatch before actually creating a forged Like" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -523,6 +530,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } + expect_signature_from(alice) + assert {:ok, oban_job} = Federator.incoming_ap_doc(%{ method: "POST", @@ -537,7 +546,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Announce" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -550,11 +559,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } - assert_mismatched_signature_cancelled(announce) + assert_mismatched_signature_cancelled(announce, alice) end test "cancels signature actor mismatch before processing a forged Follow" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") followed = insert(:user) @@ -567,11 +576,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => followed.ap_id } - assert_mismatched_signature_cancelled(follow) + assert_mismatched_signature_cancelled(follow, alice) end test "cancels signature actor mismatch before processing a forged Undo" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") undo = %{ @@ -583,6 +592,6 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => "https://example.com/activities/existing-bob-activity" } - assert_mismatched_signature_cancelled(undo) + assert_mismatched_signature_cancelled(undo, alice) end end From bd45704dba4eb82417daa449fc0af960867fcf9b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 17:21:40 +0400 Subject: [PATCH 294/317] Clarify cross-domain spoofing regressions --- .../activity_pub_controller_test.exs | 36 ++++---- test/pleroma/workers/receiver_worker_test.exs | 90 ++++++++++--------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 8cadf6686..2be5ca6df 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -727,14 +727,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post after failed signature retry", %{conn: conn} do - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") - object_id = "https://example.com/objects/inbox-forged-note" + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/inbox-forged-note" data = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-forged-create", - "context" => "https://example.com/contexts/inbox-forged-create", + "id" => "https://two.com/activities/inbox-forged-create", + "context" => "https://two.com/contexts/inbox-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -742,7 +742,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/inbox-forged-create", + "context" => "https://two.com/contexts/inbox-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -754,7 +754,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn |> assign(:valid_signature, false) |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") |> post("/inbox", data) assert "ok" == json_response(conn, 200) @@ -767,13 +767,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged like after failed signature retry", %{conn: conn} do - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) data = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-forged-like", + "id" => "https://two.com/activities/inbox-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -783,7 +783,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn |> assign(:valid_signature, false) |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") |> post("/inbox", data) assert "ok" == json_response(conn, 200) @@ -795,16 +795,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post signed by a different actor", %{conn: conn} do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") - object_id = "https://example.com/objects/inbox-signed-forged-note" + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/inbox-signed-forged-note" data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-signed-forged-create", - "context" => "https://example.com/contexts/inbox-signed-forged-create", + "id" => "https://two.com/activities/inbox-signed-forged-create", + "context" => "https://two.com/contexts/inbox-signed-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -812,7 +812,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/inbox-signed-forged-create", + "context" => "https://two.com/contexts/inbox-signed-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -844,15 +844,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged like signed by a different actor", %{conn: conn} do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-signed-forged-like", + "id" => "https://two.com/activities/inbox-signed-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 5e18fe771..ec630eb7b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -17,13 +17,13 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do defp mismatched_signature_headers do [ - {"host", "example.com"}, + {"host", "local.test"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, {"digest", "SHA-256=fake-digest"}, {"content-type", "application/activity+json"}, { "signature", - "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" } ] end @@ -348,28 +348,33 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note, user: bob, object_local: false) + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/malicious-update-note"} + ) update = %{ "type" => "Update", "actor" => bob.ap_id, - "id" => "https://example.com/activities/malicious-update", + "id" => "https://two.com/activities/malicious-update", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data } req_headers = [ - ["host", "example.com"], + ["host", "local.test"], ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], ["digest", "SHA-256=fake-digest"], ["content-type", "application/activity+json"], [ "signature", - "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" ] ] @@ -393,8 +398,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do params = insert(:note_activity).data req_headers = [ - {"host", "example.com"}, - {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + {"host", "local.test"}, + {"signature", "keyId=\"https://one.com/users/alice#main-key\""} ] assert {:ok, oban_job} = @@ -416,15 +421,20 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch through Federator-created jobs" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note, user: bob, object_local: false) + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/federator-malicious-note"} + ) update = %{ "type" => "Update", "actor" => bob.ap_id, - "id" => "https://example.com/activities/federator-malicious-update", + "id" => "https://two.com/activities/federator-malicious-update", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data @@ -434,18 +444,18 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Create" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") create = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-create", + "id" => "https://two.com/activities/forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ "type" => "Note", - "id" => "https://example.com/objects/forged-note", + "id" => "https://two.com/objects/forged-note", "actor" => bob.ap_id, "attributedTo" => bob.ap_id, "content" => "forged post", @@ -459,16 +469,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before actually creating a forged post" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - object_id = "https://example.com/objects/actually-forged-note" + object_id = "https://two.com/objects/actually-forged-note" create = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/actually-forged-create", - "context" => "https://example.com/contexts/actually-forged-create", + "id" => "https://two.com/activities/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -476,7 +486,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -500,14 +510,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Like" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) like = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-like", + "id" => "https://two.com/activities/forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -517,14 +527,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before actually creating a forged Like" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) like = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/actually-forged-like", + "id" => "https://two.com/activities/actually-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -546,14 +556,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Announce" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) announce = %{ "type" => "Announce", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-announce", + "id" => "https://two.com/activities/forged-announce", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -563,14 +573,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Follow" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") followed = insert(:user) follow = %{ "type" => "Follow", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-follow", + "id" => "https://two.com/activities/forged-follow", "to" => [followed.ap_id], "cc" => [], "object" => followed.ap_id @@ -580,16 +590,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Undo" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") undo = %{ "type" => "Undo", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-undo", + "id" => "https://two.com/activities/forged-undo", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], - "object" => "https://example.com/activities/existing-bob-activity" + "object" => "https://two.com/activities/existing-bob-activity" } assert_mismatched_signature_cancelled(undo, alice) From 7756f491d5a4d3528fc96e87f05554b2c2380ad4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 08:43:42 +0400 Subject: [PATCH 295/317] Split failed-signature inbox retries Route failed-signature ActivityPub inbox retries through a dedicated worker so legacy and malformed retry jobs fail closed before processing. --- .../activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/federator.ex | 16 +- lib/pleroma/workers/receiver_worker.ex | 76 +-- lib/pleroma/workers/signature_retry_worker.ex | 144 +++++ .../activity_pub_controller_test.exs | 37 +- test/pleroma/workers/receiver_worker_test.exs | 503 ++++-------------- .../workers/signature_retry_worker_test.exs | 469 ++++++++++++++++ 7 files changed, 786 insertions(+), 461 deletions(-) create mode 100644 lib/pleroma/workers/signature_retry_worker.ex create mode 100644 test/pleroma/workers/signature_retry_worker_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 4f1613a07..2bfff6968 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def inbox(%{assigns: %{valid_signature: false}} = conn, params) do - Federator.incoming_ap_doc(%{ + Federator.incoming_failed_signature_ap_doc(%{ method: conn.method, req_headers: conn.req_headers, request_path: conn.request_path, diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 676fc5137..90cd2e54a 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Workers.PublisherWorker alias Pleroma.Workers.ReceiverWorker + alias Pleroma.Workers.SignatureRetryWorker require Logger @@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do end # Client API - def incoming_ap_doc(%{params: params, req_headers: req_headers}) do - ReceiverWorker.new( + def incoming_failed_signature_ap_doc(%{ + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + }) do + SignatureRetryWorker.new( %{ - "op" => "incoming_ap_doc", + "op" => "incoming_failed_signature_ap_doc", + "method" => method, "req_headers" => req_headers, "params" => params, + "request_path" => request_path, + "query_string" => query_string, "timeout" => :timer.seconds(20) }, priority: 2 diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 507e099c2..3afbe138d 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -4,55 +4,36 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.Instances - alias Pleroma.Signature - alias Pleroma.User alias Pleroma.Web.Federator - alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + alias Pleroma.Workers.SignatureRetryWorker use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true - - def perform(%Job{ - args: %{ - "op" => "incoming_ap_doc", - "method" => method, - "params" => params, - "req_headers" => req_headers, - "request_path" => request_path, - "query_string" => query_string - } - }) do - # Oban's serialization converts our tuple headers to lists. - # Revert it for the signature validation. - req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) - - conn_data = %Plug.Conn{ - assigns: %{valid_signature: true}, - method: method, - params: params, - req_headers: req_headers, - request_path: request_path, - query_string: query_string - } - - with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), - {:ok, _public_key} <- Signature.refetch_public_key(conn_data), - {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, - {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, - {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do - unless Instances.reachable?(params["actor"]) do - domain = URI.parse(params["actor"]).host - Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) - end - - {:ok, res} + def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do + if signature_retry_job?(args) do + perform_signature_retry(job) else - e -> process_errors(e) + perform_incoming(params) end end - def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do + def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do + if signature_retry_job?(args) do + perform_signature_retry(job) + else + process_errors(:missing_incoming_ap_doc_params) + end + end + + defp perform_signature_retry(%Job{args: args} = job) do + SignatureRetryWorker.perform(%Job{ + job + | args: Map.put(args, "op", "incoming_failed_signature_ap_doc") + }) + end + + defp perform_incoming(params) do with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -65,21 +46,15 @@ defmodule Pleroma.Workers.ReceiverWorker do end end + defp signature_retry_job?(args) do + Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1)) + end + @impl true def timeout(%_{args: %{"timeout" => timeout}}), do: timeout def timeout(_job), do: :timer.seconds(5) - defp validate_same_actor(conn_data) do - case MappedSignatureToIdentityPlug.call(conn_data, []) do - %Plug.Conn{assigns: %{valid_signature: true}} -> - true - - _ -> - false - end - end - defp process_errors({:error, {:error, _} = error}), do: process_errors(error) defp process_errors(errors) do @@ -103,6 +78,7 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # Unclear if this can be reached {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} + :missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params} # Catchall {:error, _} = e -> e e -> {:error, e} diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex new file mode 100644 index 000000000..56673a514 --- /dev/null +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.SignatureRetryWorker do + alias Pleroma.Instances + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] + + @impl true + def perform(%Job{ + args: %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => method, + "params" => params, + "req_headers" => req_headers, + "request_path" => request_path, + "query_string" => query_string + } + }) + when is_binary(method) and is_map(params) and is_list(req_headers) and + is_binary(request_path) and is_binary(query_string) do + with {:ok, req_headers} <- normalize_req_headers(req_headers), + conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + }, + actor_id = Utils.get_ap_id(params["actor"]), + {:signature_actor, {:ok, signature_actor_id}} <- + {:signature_actor, signature_actor_id(conn_data)}, + {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, + {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + + {:ok, res} + else + e -> process_errors(e) + end + end + + def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do + process_errors(:missing_signature_retry_metadata) + end + + def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata) + + @impl true + def timeout(%_{args: %{"timeout" => timeout}}), do: timeout + + def timeout(_job), do: :timer.seconds(5) + + defp normalize_req_headers(req_headers) do + req_headers + |> Enum.reduce_while({:ok, []}, fn + {key, value}, {:ok, acc} when is_binary(key) and is_binary(value) -> + {:cont, {:ok, [{key, value} | acc]}} + + [key, value], {:ok, acc} when is_binary(key) and is_binary(value) -> + {:cont, {:ok, [{key, value} | acc]}} + + _, _ -> + {:halt, {:error, :invalid_signature_retry_metadata}} + end) + |> case do + {:ok, headers} -> {:ok, Enum.reverse(headers)} + error -> error + end + end + + defp validate_same_actor(conn_data) do + case MappedSignatureToIdentityPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true}} -> + true + + _ -> + false + end + end + + defp validate_signature(conn_data) do + Signature.validate_signature(conn_data) + rescue + _ -> false + catch + _, _ -> false + end + + defp signature_actor_id(conn_data) do + Signature.get_actor_id(conn_data) + rescue + _ -> {:error, :invalid_signature} + catch + _, _ -> {:error, :invalid_signature} + end + + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + + defp process_errors(errors) do + case errors do + # User fetch failures + {:error, :not_found} = reason -> {:cancel, reason} + {:error, :forbidden} = reason -> {:cancel, reason} + # Inactive user + {:error, {:user_active, false} = reason} -> {:cancel, reason} + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} + # Duplicate detection during Normalization + {:error, :already_present} -> {:cancel, :already_present} + # MRFs will return a reject + {:error, {:reject, _} = reason} -> {:cancel, reason} + # HTTP Sigs + {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature} + {:signature, false} -> {:cancel, :invalid_signature} + {:same_actor, false} -> {:cancel, :actor_signature_mismatch} + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} + # Unclear if this can be reached + {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} + # Fail closed if the retry cannot reconstruct the original request. + :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata} + {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata} + # Catchall + {:error, _} = e -> e + e -> {:error, e} + end + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 2be5ca6df..62c1dd830 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Workers.ReceiverWorker + alias Pleroma.Workers.SignatureRetryWorker import Pleroma.Factory @@ -36,6 +37,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:instance, :federating], true) + defp expect_signature_retry_from(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") + + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + describe "/relay" do setup do: clear_config([:instance, :allow_relay]) @@ -727,6 +746,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") object_id = "https://two.com/objects/inbox-forged-note" @@ -750,6 +770,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } + expect_signature_retry_from(alice) + conn = conn |> assign(:valid_signature, false) @@ -760,13 +782,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) refute Object.get_by_ap_id(object_id) end test "does not create a forged like after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) @@ -779,6 +802,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "object" => note.data["id"] } + expect_signature_retry_from(alice) + conn = conn |> assign(:valid_signature, false) @@ -789,7 +814,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) end @@ -820,7 +845,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + expect_signature_retry_from(alice) conn = conn @@ -837,7 +862,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) refute Object.get_by_ap_id(object_id) @@ -858,7 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "object" => note.data["id"] } - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + expect_signature_retry_from(alice) conn = conn @@ -875,7 +900,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index ec630eb7b..67a3f902e 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -11,11 +11,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker - defp mismatched_signature_headers do + defp signature_headers_for(%User{} = signer) do [ {"host", "local.test"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, @@ -23,39 +21,15 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do {"content-type", "application/activity+json"}, { "signature", - "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" } ] end - defp expect_signature_from(%User{} = signer) do - signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") - - Tesla.Mock.mock(fn - %{url: url} when url == signer.ap_id -> - %Tesla.Env{ - status: 200, - body: Jason.encode!(signer_json), - headers: HttpRequestMock.activitypub_object_headers() - } - end) - - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) - end - - defp assert_mismatched_signature_cancelled(params, signer) do - expect_signature_from(signer) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + defp perform_incoming(params) do + ReceiverWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_ap_doc", "params" => params} + }) end test "it does not retry MRF reject" do @@ -125,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/bart") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :forbidden}} = perform_incoming(params) end test "when request returns a 404" do @@ -142,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/troymcclure") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :not_found}} = perform_incoming(params) end test "when request returns a 410" do @@ -159,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/hankscorpio") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :not_found}} = perform_incoming(params) end test "when user account is disabled" do @@ -182,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do {:ok, %User{}} = User.set_activation(user, false) - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:user_active, false}} = perform_incoming(params) end end - test "it can validate the signature" do - Tesla.Mock.mock(fn - %{url: "https://phpc.social/users/denniskoch"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/denniskoch.json"), - headers: [{"content-type", "application/activity+json"}] - } - - %{url: "https://phpc.social/users/denniskoch/collections/featured"} -> - %Tesla.Env{ - status: 200, - headers: [{"content-type", "application/activity+json"}], - body: - File.read!("test/fixtures/users_mock/masto_featured.json") - |> String.replace("{{domain}}", "phpc.social") - |> String.replace("{{nickname}}", "denniskoch") - } - end) - - params = - File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!() - - req_headers = [ - ["accept-encoding", "gzip"], - ["content-length", "5184"], - ["content-type", "application/activity+json"], - ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], - ["digest", "SHA-256=ouge/6HP2/QryG6F3JNtZ6vzs/hSwMk67xdxe87eH7A="], - ["host", "bikeshed.party"], - [ - "signature", - "keyId=\"https://mastodon.social/users/bastianallgeier#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"ymE3vn5Iw50N6ukSp8oIuXJB5SBjGAGjBasdTDvn+ahZIzq2SIJfmVCsIIzyqIROnhWyQoTbavTclVojEqdaeOx+Ejz2wBnRBmhz5oemJLk4RnnCH0lwMWyzeY98YAvxi9Rq57Gojuv/1lBqyGa+rDzynyJpAMyFk17XIZpjMKuTNMCbjMDy76ILHqArykAIL/v1zxkgwxY/+ELzxqMpNqtZ+kQ29znNMUBB3eVZ/mNAHAz6o33Y9VKxM2jw+08vtuIZOusXyiHbRiaj2g5HtN2WBUw1MzzfRfHF2/yy7rcipobeoyk5RvP5SyHV3WrIeZ3iyoNfmv33y8fxllF0EA==\"" - ], - [ - "user-agent", - "http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-07-25; +https://mastodon.social/)" - ] - ] - - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: req_headers, - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job) - end - test "cancels due to origin containment" do params = insert(:note_activity).data |> Map.put("id", "https://notorigindomain.com/activity") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job) + assert {:cancel, :origin_containment_failed} = perform_incoming(params) end test "canceled due to deleted object" do @@ -277,16 +154,98 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } end) - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) + assert {:cancel, _} = perform_incoming(params) + end - assert {:cancel, _} = ReceiverWorker.perform(oban_job) + test "delegates legacy failed-signature metadata jobs instead of processing them as trusted" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/legacy-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/legacy-forged-create", + "context" => "https://two.com/contexts/legacy-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/legacy-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:cancel, :actor_signature_mismatch} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => create, + "req_headers" => signature_headers_for(alice), + "request_path" => "/inbox", + "query_string" => "" + } + }) + + refute Pleroma.Activity.get_by_ap_id(create["id"]) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "fails closed for the old persisted failed-signature job shape" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/old-shape-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/old-shape-forged-create", + "context" => "https://two.com/contexts/old-shape-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/old-shape-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => create, + "req_headers" => signature_headers_for(alice), + "timeout" => 20_000 + } + }) + + refute Pleroma.Activity.get_by_ap_id(create["id"]) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "fails closed for malformed legacy metadata jobs without params" do + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "req_headers" => [], + "timeout" => 20_000 + } + }) end describe "Server reachability:" do @@ -346,262 +305,4 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end end end - - test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - note = - insert(:note, - user: bob, - object_local: false, - data: %{"id" => "https://two.com/objects/malicious-update-note"} - ) - - update = %{ - "type" => "Update", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/malicious-update", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data - } - - req_headers = [ - ["host", "local.test"], - ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], - ["digest", "SHA-256=fake-digest"], - ["content-type", "application/activity+json"], - [ - "signature", - "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" - ] - ] - - oban_job = %Oban.Job{ - args: %{ - "op" => "incoming_ap_doc", - "method" => "POST", - "params" => update, - "req_headers" => req_headers, - "request_path" => "/inbox", - "query_string" => "" - } - } - - expect_signature_from(alice) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - end - - test "Federator preserves request metadata needed for ReceiverWorker signature checks" do - params = insert(:note_activity).data - - req_headers = [ - {"host", "local.test"}, - {"signature", "keyId=\"https://one.com/users/alice#main-key\""} - ] - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: req_headers, - request_path: "/inbox", - params: params, - query_string: "foo=bar" - }) - - assert %{ - "method" => "POST", - "req_headers" => ^req_headers, - "request_path" => "/inbox", - "params" => ^params, - "query_string" => "foo=bar" - } = oban_job.args - end - - test "cancels signature actor mismatch through Federator-created jobs" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - note = - insert(:note, - user: bob, - object_local: false, - data: %{"id" => "https://two.com/objects/federator-malicious-note"} - ) - - update = %{ - "type" => "Update", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/federator-malicious-update", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data - } - - assert_mismatched_signature_cancelled(update, alice) - end - - test "cancels signature actor mismatch before processing a forged Create" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - create = %{ - "type" => "Create", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => %{ - "type" => "Note", - "id" => "https://two.com/objects/forged-note", - "actor" => bob.ap_id, - "attributedTo" => bob.ap_id, - "content" => "forged post", - "published" => "2024-07-25T13:33:31Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [] - } - } - - assert_mismatched_signature_cancelled(create, alice) - end - - test "cancels signature actor mismatch before actually creating a forged post" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - object_id = "https://two.com/objects/actually-forged-note" - - create = %{ - "type" => "Create", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/actually-forged-create", - "context" => "https://two.com/contexts/actually-forged-create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => %{ - "type" => "Note", - "id" => object_id, - "actor" => bob.ap_id, - "attributedTo" => bob.ap_id, - "context" => "https://two.com/contexts/actually-forged-create", - "content" => "forged post", - "published" => "2024-07-25T13:33:31Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [] - } - } - - expect_signature_from(alice) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: create, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute Pleroma.Object.get_by_ap_id(object_id) - end - - test "cancels signature actor mismatch before processing a forged Like" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - like = %{ - "type" => "Like", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-like", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - assert_mismatched_signature_cancelled(like, alice) - end - - test "cancels signature actor mismatch before actually creating a forged Like" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - like = %{ - "type" => "Like", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/actually-forged-like", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - expect_signature_from(alice) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: like, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute Pleroma.Activity.get_by_ap_id(like["id"]) - end - - test "cancels signature actor mismatch before processing a forged Announce" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - announce = %{ - "type" => "Announce", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-announce", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - assert_mismatched_signature_cancelled(announce, alice) - end - - test "cancels signature actor mismatch before processing a forged Follow" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - followed = insert(:user) - - follow = %{ - "type" => "Follow", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-follow", - "to" => [followed.ap_id], - "cc" => [], - "object" => followed.ap_id - } - - assert_mismatched_signature_cancelled(follow, alice) - end - - test "cancels signature actor mismatch before processing a forged Undo" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - undo = %{ - "type" => "Undo", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-undo", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => "https://two.com/activities/existing-bob-activity" - } - - assert_mismatched_signature_cancelled(undo, alice) - end end diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs new file mode 100644 index 000000000..02706ebad --- /dev/null +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -0,0 +1,469 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.SignatureRetryWorkerTest do + use Pleroma.DataCase, async: false + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.Federator + alias Pleroma.Workers.SignatureRetryWorker + + defp signature_headers_for(%User{} = signer) do + [ + {"host", "local.test"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp stub_actor_fetch(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") + + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + end + + defp expect_signature_from(%User{} = signer) do + stub_actor_fetch(signer) + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + + defp enqueue_failed_signature(params, signer) do + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: signature_headers_for(signer), + request_path: "/inbox", + params: params, + query_string: "" + }) + end + + defp failed_signature_job(params, req_headers, opts \\ []) do + %Oban.Job{ + args: %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => Keyword.get(opts, :method, "POST"), + "req_headers" => req_headers, + "request_path" => Keyword.get(opts, :request_path, "/inbox"), + "params" => params, + "query_string" => Keyword.get(opts, :query_string, "") + } + } + end + + defp assert_mismatched_signature_cancelled(params, signer) do + assert {:ok, oban_job} = enqueue_failed_signature(params, signer) + + assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + end + + test "Federator preserves request metadata for failed-signature retry jobs" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "local.test"}, + {"signature", "keyId=\"https://one.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker" + + assert %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels retry jobs without request metadata" do + params = insert(:note_activity).data + + assert {:cancel, :missing_signature_retry_metadata} = + SignatureRetryWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} + }) + end + + test "cancels retry jobs with malformed serialized request headers" do + params = insert(:note_activity).data + + assert {:cancel, :invalid_signature_retry_metadata} = + SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + end + + test "cancels retry jobs without a signature header" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + end + + test "cancels missing signature before fetching an unavailable payload actor" do + params = + insert(:note_activity).data + |> Map.put("actor", "https://unavailable.example/users/bob") + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + end + + test "cancels signer mismatch before fetching an unavailable payload actor" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + params = + insert(:note_activity).data + |> Map.put("actor", "https://unavailable.example/users/bob") + + assert {:cancel, :actor_signature_mismatch} = + SignatureRetryWorker.perform( + failed_signature_job(params, signature_headers_for(alice)) + ) + end + + test "cancels retry jobs with a signature header without keyId" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + + req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}] + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, req_headers)) + end + + test "cancels retry jobs with an unparsable signature keyId" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}] + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, req_headers)) + end + + test "cancels when the refetched key still cannot validate the signature" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/invalid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/invalid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + stub_actor_fetch(alice) + + assert {:ok, oban_job} = enqueue_failed_signature(create, alice) + assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + refute Activity.get_by_ap_id(create["id"]) + end + + test "processes the activity after refetching a valid matching signature" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/valid-signature-create", + "context" => "https://one.com/contexts/valid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/valid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/valid-signature-create", + "content" => "valid post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_from(alice) + + assert {:ok, oban_job} = enqueue_failed_signature(create, alice) + assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job) + assert Activity.get_by_ap_id(create["id"]) + end + + test "processes the activity when a real signature validates with a query string" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/valid-query-signature-create", + "context" => "https://one.com/contexts/valid-query-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/valid-query-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/valid-query-signature-create", + "content" => "valid signed post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + stub_actor_fetch(alice) + + date = "Thu, 25 Jul 2024 13:33:31 GMT" + digest = "SHA-256=fake-digest" + + signature = + Signature.sign(alice, %{ + "(request-target)" => "post /inbox?foo=bar", + "content-type" => "application/activity+json", + date: date, + digest: digest, + host: "local.test" + }) + + req_headers = [ + ["host", "local.test"], + ["date", date], + ["digest", digest], + ["content-type", "application/activity+json"], + ["signature", signature] + ] + + assert {:ok, %Activity{}} = + SignatureRetryWorker.perform( + failed_signature_job(create, req_headers, query_string: "foo=bar") + ) + + assert Activity.get_by_ap_id(create["id"]) + end + + test "cancels when signature actor does not match payload actor" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/malicious-update-note"} + ) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update, alice) + end + + test "cancels signature actor mismatch through Federator-created jobs" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/federator-malicious-note"} + ) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update, alice) + end + + test "cancels signature actor mismatch before processing a forged Create" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + object_id = "https://two.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + refute Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like, alice) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like, alice) + refute Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce, alice) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow, alice) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://two.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo, alice) + end +end From 4337e0eb1b40942c7860b9d32e5ecf1ddafdc0ba Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 12:33:26 +0400 Subject: [PATCH 296/317] Fail closed on unresolved signed payloads Reject unknown remote Update targets and invalidate signed payloads when their signer identity cannot be mapped, avoiding crashes and fail-open signature state. --- .../object_validators/update_validator.ex | 4 ++++ .../plugs/mapped_signature_to_identity_plug.ex | 4 ++-- .../object_validators/update_handling_test.exs | 17 +++++++++++++++++ .../mapped_signature_to_identity_plug_test.exs | 12 ++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index 5586b74cf..ad3c0e3e2 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -101,6 +101,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do |> add_error(:object, "Can't be updated by this actor") end + nil -> + cng + |> add_error(:object, "Can't be updated by this actor") + true -> cng |> add_error(:object, "Update is neither for Object or Actor") diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index c6d531086..a688b1780 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do # remove me once testsuite uses mapped capabilities instead of what we do now {:user, nil} -> Logger.debug("Failed to map identity from signature (lookup failure)") - Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") - conn + Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}") + assign(conn, :valid_signature, false) end end diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index f04f9cc61..9dec315b3 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -90,6 +90,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do refute cng.valid? assert Keyword.has_key?(cng.errors, :object) end + + test "returns an error if the remote update target IRI is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object-iri", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/objects/unknown-iri" + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 81c6b0c5d..df713762c 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -58,4 +58,16 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert conn.assigns.valid_signature == false refute Map.has_key?(conn.assigns, :user) end + + test "it considers a mapped identity to be invalid when embedded actor identity cannot be found" do + actor = "http://niu.moe/users/rye" + + conn = + build_conn(:post, "/doesntmattter", %{"actor" => %{"id" => actor}}) + |> set_signature(actor) + |> MappedSignatureToIdentityPlug.call(%{}) + + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) + end end From 99b614a52e9e804f8e3e642fe9104a18cbcfe3e1 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 13:39:59 +0400 Subject: [PATCH 297/317] Add spoofing fixes changelog entry --- changelog.d/activitypub-spoofing.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/activitypub-spoofing.security diff --git a/changelog.d/activitypub-spoofing.security b/changelog.d/activitypub-spoofing.security new file mode 100644 index 000000000..3e6baffb6 --- /dev/null +++ b/changelog.d/activitypub-spoofing.security @@ -0,0 +1 @@ +ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed From a35aa6551e79539f9452455cc54aeed63c9ba8d4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 10:39:49 +0400 Subject: [PATCH 298/317] Fix Woodpecker path filters --- .woodpecker/lint.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.15.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.18.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index b96d584ee..54de53873 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] steps: mix-format: diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 84046be41..43f30262a 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8dcec62fb..1963b36a5 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint From 3dbc570471862ad1edad323f65215f04fa0b83bf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 11:57:04 +0400 Subject: [PATCH 299/317] Woodpecker CI: Publish update-compatible OTP releases --- .woodpecker/docker-combine.yaml | 2 +- .woodpecker/docker.yaml | 2 +- .woodpecker/otp-musl.yaml | 140 +++++++++++++- .woodpecker/otp.yaml | 140 +++++++++++++- rel/files/bin/pleroma_ctl | 23 ++- test/pleroma/pleroma_ctl_test.exs | 293 ++++++++++++++++++++++++++++++ 6 files changed, 578 insertions(+), 22 deletions(-) create mode 100644 test/pleroma/pleroma_ctl_test.exs diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index be8583763..161987d64 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 317cb5fbb..4053be91d 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index c58eee66b..e0fb0fb99 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -9,6 +9,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,7 @@ variables: - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3-alpine build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings @@ -66,6 +68,32 @@ steps: - <<: *build_cmds - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-develop-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm_build + - <<: *build_cmds + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip release + + otp-stable-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm_build + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip release + otp-develop-arm64-musl: image: *build_image_arm64 when: @@ -102,7 +130,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + + upload-latest-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-amd64-musl: @@ -114,7 +155,70 @@ steps: package_name: pleroma-otp-stable-amd64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma.zip + update: 'true' + + upload-artifacts-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + + upload-latest-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma.zip + update: 'true' + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma.zip + update: 'true' upload-artifacts-arm64-musl: image: *artifacts_uploader_image @@ -126,7 +230,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + upload-latest-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-arm64-musl: @@ -138,4 +255,17 @@ steps: package_name: pleroma-otp-stable-arm64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma.zip + update: 'true' diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index c01c2e557..3c8b71814 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -9,6 +9,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,7 @@ variables: - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3 build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings @@ -67,6 +69,32 @@ steps: - <<: *build_cmds - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-develop-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm_build + - <<: *build_cmds + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip release + + otp-stable-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm_build + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm.zip release + otp-develop-arm64: image: *build_image_arm64 when: @@ -103,7 +131,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + + upload-latest-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-amd64: @@ -115,7 +156,70 @@ steps: package_name: pleroma-otp-stable-amd64 package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma.zip + update: 'true' + + upload-artifacts-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + + upload-latest-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma.zip + update: 'true' + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm + package_version: stable-${CI_COMMIT_SHA:0:8}-arm + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma.zip + update: 'true' upload-artifacts-arm64: image: *artifacts_uploader_image @@ -127,7 +231,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + + upload-latest-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-arm64: @@ -139,4 +256,17 @@ steps: package_name: pleroma-otp-stable-arm64 package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma.zip + update: 'true' diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 2e114ff50..f5d3f321b 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -79,18 +79,18 @@ update() { RELEASE_ROOT=$(dirname "$SCRIPTPATH") uri="https://git.pleroma.social" project_name="pleroma" - api_base="${uri}/api/v1/packages/${project_name}/generic" package_base="${uri}/api/packages/${project_name}/generic" - project_branch="${BRANCH:-$(detect_branch)}" - flavour="${FLAVOUR:-$(detect_flavour)}" - # API responds in JSON, optimistically try to make it one object per line - ver=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/-/latest | tr ',' '\n' | grep '"version":' | cut -d':' -f2 | tr -d '"') - file=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/files | tr ',' '\n' | grep '"name":' | cut -d':' -f2 | tr -d '"') - full_uri=${FULL_URI:-"${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/"${file}"} + if [ -n "$FULL_URI" ]; then + full_uri="$FULL_URI" + else + project_branch="${BRANCH:-$(detect_branch)}" + flavour="${FLAVOUR:-$(detect_flavour)}" + full_uri="${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/latest/pleroma.zip + fi tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" echo "Downloading the artifact from ${full_uri} to ${artifact}" - curl "$full_uri" -o "${artifact}" + curl -fL "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" unzip -q "$artifact" -d "$tmp" echo "Copying files over to $RELEASE_ROOT" @@ -144,9 +144,12 @@ else # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace. # Previously it was sent as string, which would get split on whitespace on the task side. - # Escaping does not help including non-POSIX printf %q + # Encode as Elixir binary literals to avoid string escaping and interpolation issues. PREPARED_ARGS="" - for arg in "$@"; do PREPARED_ARGS="$PREPARED_ARGS \"$arg\","; done + for arg in "$@"; do + bytes=$(printf '%s' "$arg" | od -An -v -tu1 | tr -s '[:space:]' ',' | sed 's/^,//; s/,$//') + PREPARED_ARGS="$PREPARED_ARGS <<$bytes>>," + done ACTION="$1" if [ $# -gt 0 ]; then diff --git a/test/pleroma/pleroma_ctl_test.exs b/test/pleroma/pleroma_ctl_test.exs new file mode 100644 index 000000000..d96396399 --- /dev/null +++ b/test/pleroma/pleroma_ctl_test.exs @@ -0,0 +1,293 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.PleromaCtlTest do + use ExUnit.Case, async: false + + @pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__) + + setup do + tmp_dir = + Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}") + + release_root = Path.join(tmp_dir, "release") + bin_dir = Path.join(release_root, "bin") + + File.mkdir_p!(bin_dir) + File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl")) + File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755) + + on_exit(fn -> File.rm_rf!(tmp_dir) end) + + {:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir} + end + + test "update downloads branch-scoped latest OTP package", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc") + + update_tmp_dir = Path.join(tmp_dir, "update_tmp") + File.mkdir_p!(update_tmp_dir) + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "update", + "--branch", + "develop", + "--flavour", + "amd64", + "--tmp-dir", + update_tmp_dir, + "--no-rm" + ], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + assert File.exists?(Path.join(release_root, "bin/marker")) + + assert ["-fL", url, "-o", artifact_path] = + curl_args_path + |> File.read!() + |> String.split("\n", trim: true) + + assert url == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip" + + assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip") + end + + test "update detects stable branch and local flavour", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc") + write_start_erl_data(release_root, "2.10.0") + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert curl_url(curl_args_path) == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip" + end + + test "update detects develop branch and musl arm flavour", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl") + write_start_erl_data(release_root, "2.10.0.develop") + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert curl_url(curl_args_path) == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip" + end + + test "update with zip URL bypasses branch and flavour detection", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc") + write_start_erl_data(release_root, "2.10.0.custombranch") + + custom_url = "https://example.test/custom.zip" + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "update", + "--zip-url", + custom_url, + "--tmp-dir", + create_update_tmp_dir(tmp_dir), + "--no-rm" + ], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + assert curl_url(curl_args_path) == custom_url + end + + test "passes arguments with spaces and Elixir string metacharacters", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir + } do + capture_path = Path.join(tmp_dir, "captured_args") + eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs") + + write_executable(Path.join(bin_dir, "pleroma"), """ + #!/bin/sh + { + printf '%s\n' 'defmodule Pleroma.ReleaseTasks do' + printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))' + printf '%s\n' 'end' + printf '%s\n' "$2" + } > "$PLEROMA_CTL_EVAL_FILE" + + exec elixir "$PLEROMA_CTL_EVAL_FILE" + """) + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "user", + "", + "has space", + ~s(has "quote"), + ~s(has \\ backslash), + ~S(#{:not_interpolated}) + ], + env: [ + {"PLEROMA_CTL_CAPTURE", capture_path}, + {"PLEROMA_CTL_EVAL_FILE", eval_path} + ], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert capture_path + |> File.read!() + |> :erlang.binary_to_term() == [ + "user", + "", + "has space", + ~s(has "quote"), + ~s(has \\ backslash), + ~S(#{:not_interpolated}) + ] + end + + defp write_executable(path, contents) do + File.write!(path, contents) + File.chmod!(path, 0o755) + end + + defp write_start_erl_data(release_root, version) do + releases_dir = Path.join(release_root, "releases") + File.mkdir_p!(releases_dir) + File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n") + end + + defp create_update_tmp_dir(tmp_dir) do + update_tmp_dir = Path.join(tmp_dir, "update_tmp") + File.mkdir_p!(update_tmp_dir) + update_tmp_dir + end + + defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do + write_executable(Path.join(stubs_dir, "curl"), """ + #!/bin/sh + printf '%s\n' "$@" > "#{curl_args_path}" + + while [ $# -gt 0 ]; do + case "$1" in + -o) + artifact="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + : > "$artifact" + """) + + write_executable(Path.join(stubs_dir, "unzip"), """ + #!/bin/sh + while [ $# -gt 0 ]; do + case "$1" in + -d) + dest="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + mkdir -p "$dest/release/bin" + printf 'marker' > "$dest/release/bin/marker" + """) + + write_executable(Path.join(stubs_dir, "uname"), """ + #!/bin/sh + printf '%s\n' '#{arch}' + """) + + write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc)) + + write_executable(Path.join(stubs_dir, "ldd"), """ + #!/bin/sh + printf '%s\n' 'musl libc (mock)' + """) + end + + defp getconf_stub("glibc") do + """ + #!/bin/sh + printf '%s\n' 'glibc 2.40' + """ + end + + defp getconf_stub(_libc) do + """ + #!/bin/sh + exit 1 + """ + end + + defp curl_url(curl_args_path) do + ["-fL", url, "-o", _artifact_path] = + curl_args_path + |> File.read!() + |> String.split("\n", trim: true) + + url + end +end From 47e6dbfadece58a277280a9910b340ec589e6507 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 13:38:56 +0400 Subject: [PATCH 300/317] Woodpecker CI: Work around script entrypoint truncation --- .woodpecker/changelog.yaml | 7 +++++++ .woodpecker/lint.yaml | 9 +++++++++ .woodpecker/otp-musl.yaml | 13 +++++++++++++ .woodpecker/otp.yaml | 13 +++++++++++++ .woodpecker/unit-testing-elixir-1.15.yaml | 7 +++++++ .woodpecker/unit-testing-elixir-1.18.yaml | 7 +++++++ 6 files changed, 56 insertions(+) diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 4f38ce618..1d65e8e9f 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,9 +1,16 @@ when: - event: pull_request +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: check-changelog: image: docker.io/alpine:3.23 + entrypoint: *script_file_entrypoint commands: - apk add --no-cache git - sh ./tools/check-changelog diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index b96d584ee..95325d779 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -5,10 +5,17 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: mix-format: image: &elixir-image docker.io/elixir:1.15-alpine + entrypoint: *script_file_entrypoint failure: ignore commands: - | @@ -19,6 +26,7 @@ steps: credo: image: *elixir-image + entrypoint: *script_file_entrypoint failure: ignore environment: MIX_ENV: test @@ -55,6 +63,7 @@ steps: ensure-status: image: *elixir-image + entrypoint: *script_file_entrypoint commands: | if test -f fail.stamp; then echo "One or more previous steps fails. Failing workflow..." diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index e0fb0fb99..877860255 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -18,6 +18,10 @@ labels: platform: ${platform} variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' build_cmds: &build_cmds - apk add git build-base cmake file-dev openssl vips-dev zip - echo "import Config" > config/prod.secret.exs @@ -44,6 +48,7 @@ variables: steps: otp-develop-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -53,6 +58,7 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -61,6 +67,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -70,6 +77,7 @@ steps: otp-develop-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -79,6 +87,7 @@ steps: otp-stable-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -87,6 +96,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -96,6 +106,7 @@ steps: otp-develop-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -105,6 +116,7 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -113,6 +125,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' environment: *env diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 3c8b71814..766e42e03 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -18,6 +18,10 @@ labels: platform: ${platform} variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' build_cmds: &build_cmds - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - echo "import Config" > config/prod.secret.exs @@ -45,6 +49,7 @@ variables: steps: otp-develop-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -54,6 +59,7 @@ steps: otp-stable-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -62,6 +68,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -71,6 +78,7 @@ steps: otp-develop-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -80,6 +88,7 @@ steps: otp-stable-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -88,6 +97,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -97,6 +107,7 @@ steps: otp-develop-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -106,6 +117,7 @@ steps: otp-stable-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -114,6 +126,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' environment: *env diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 84046be41..9d5cf7102 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -8,9 +8,16 @@ when: depends_on: - lint +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: unit-testing-elixir-1.15: image: elixir:1.15-alpine + entrypoint: *script_file_entrypoint environment: MIX_ENV: test DB_HOST: postgres diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8dcec62fb..8b0451ef0 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,9 +8,16 @@ when: depends_on: - lint +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: unit-testing-elixir-1.18: image: elixir:1.18-otp-27-alpine + entrypoint: *script_file_entrypoint environment: MIX_ENV: test DB_HOST: postgres From ee18feef7c2a3e0953880b7d7350d49aa463fc55 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 15:04:40 +0400 Subject: [PATCH 301/317] Woodpecker CI: Allow manual develop release runs --- .woodpecker/docker-combine.yaml | 4 ++++ .woodpecker/docker.yaml | 2 ++ .woodpecker/otp-musl.yaml | 2 ++ .woodpecker/otp.yaml | 2 ++ 4 files changed, 10 insertions(+) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 161987d64..c6493df1c 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable @@ -17,6 +19,8 @@ steps: when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 4053be91d..abc6bfa3b 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 877860255..6f007a127 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 766e42e03..7e1bad611 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable From 9fdad779b5f316d9a5c3c5f8d966a8c4c99fc40d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 16:12:47 +0400 Subject: [PATCH 302/317] Woodpecker CI: Run Docker manifest combine on amd64 --- .woodpecker/docker-combine.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index c6493df1c..6c91d26e9 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -13,6 +13,9 @@ depends_on: skip_clone: true +labels: + platform: linux/amd64 + steps: docker-develop-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest From 50651284a2988a5e806a0e802ad2257a9595f3a2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 17:21:37 +0400 Subject: [PATCH 303/317] Woodpecker CI: Run generic workflows on amd64 --- .woodpecker/changelog.yaml | 3 +++ .woodpecker/lint.yaml | 3 +++ .woodpecker/unit-testing-elixir-1.15.yaml | 3 +++ .woodpecker/unit-testing-elixir-1.18.yaml | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 1d65e8e9f..64062f17e 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,6 +1,9 @@ when: - event: pull_request +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 95325d779..7b2a1683c 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -5,6 +5,9 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 9d5cf7102..5c6be764d 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -8,6 +8,9 @@ when: depends_on: - lint +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8b0451ef0..2a40f41f7 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,6 +8,9 @@ when: depends_on: - lint +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh From 4acd8c4e7204e3eee50568f23f53bff053b9206e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 21:08:04 +0400 Subject: [PATCH 304/317] Log failed-signature retry rejections --- lib/pleroma/workers/signature_retry_worker.ex | 224 +++++++++++++----- .../workers/signature_retry_worker_test.exs | 100 +++++++- 2 files changed, 257 insertions(+), 67 deletions(-) diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex index 56673a514..2c4c097dd 100644 --- a/lib/pleroma/workers/signature_retry_worker.ex +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Workers.SignatureRetryWorker do alias Pleroma.Web.Federator alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + require Logger + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true @@ -25,37 +27,55 @@ defmodule Pleroma.Workers.SignatureRetryWorker do }) when is_binary(method) and is_map(params) and is_list(req_headers) and is_binary(request_path) and is_binary(query_string) do - with {:ok, req_headers} <- normalize_req_headers(req_headers), - conn_data = %Plug.Conn{ - assigns: %{valid_signature: true}, - method: method, - params: params, - req_headers: req_headers, - request_path: request_path, - query_string: query_string - }, - actor_id = Utils.get_ap_id(params["actor"]), - {:signature_actor, {:ok, signature_actor_id}} <- - {:signature_actor, signature_actor_id(conn_data)}, - {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, - {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), - {:ok, _public_key} <- Signature.refetch_public_key(conn_data), - {:signature, true} <- {:signature, validate_signature(conn_data)}, - {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, - {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do - unless Instances.reachable?(params["actor"]) do - domain = URI.parse(params["actor"]).host - Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) - end + case normalize_req_headers(req_headers) do + {:ok, req_headers} -> + conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + } - {:ok, res} - else - e -> process_errors(e) + signature_actor_result = signature_actor_id(conn_data) + + with actor_id = Utils.get_ap_id(params["actor"]), + {:signature_actor, {:ok, signature_actor_id}} <- + {:signature_actor, signature_actor_result}, + {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, + {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + + {:ok, res} + else + e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result)) + end + + e -> + process_errors(e, retry_log_context(params, request_path, nil)) end end - def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do - process_errors(:missing_signature_retry_metadata) + def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do + process_errors( + :missing_signature_retry_metadata, + retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil) + ) + end + + def perform(%Job{args: args}) when is_map(args) do + process_errors( + :missing_signature_retry_metadata, + retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil) + ) end def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata) @@ -109,36 +129,126 @@ defmodule Pleroma.Workers.SignatureRetryWorker do _, _ -> {:error, :invalid_signature} end - defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + defp process_errors(errors, context \\ %{}) - defp process_errors(errors) do - case errors do - # User fetch failures - {:error, :not_found} = reason -> {:cancel, reason} - {:error, :forbidden} = reason -> {:cancel, reason} - # Inactive user - {:error, {:user_active, false} = reason} -> {:cancel, reason} - # Validator will error and return a changeset error - # e.g., duplicate activities or if the object was deleted - {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} - # Duplicate detection during Normalization - {:error, :already_present} -> {:cancel, :already_present} - # MRFs will return a reject - {:error, {:reject, _} = reason} -> {:cancel, reason} - # HTTP Sigs - {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature} - {:signature, false} -> {:cancel, :invalid_signature} - {:same_actor, false} -> {:cancel, :actor_signature_mismatch} - # Origin / URL validation failed somewhere possibly due to spoofing - {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} - # Unclear if this can be reached - {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} - # Fail closed if the retry cannot reconstruct the original request. - :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata} - {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata} - # Catchall - {:error, _} = e -> e - e -> {:error, e} - end + defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context) + + defp process_errors(errors, context) do + result = + case errors do + # User fetch failures + {:error, :not_found} = reason -> + {:cancel, reason} + + {:error, :forbidden} = reason -> + {:cancel, reason} + + # Inactive user + {:error, {:user_active, false} = reason} -> + {:cancel, reason} + + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> + {:cancel, reason} + + # Duplicate detection during Normalization + {:error, :already_present} -> + {:cancel, :already_present} + + # MRFs will return a reject + {:error, {:reject, _} = reason} -> + {:cancel, reason} + + # HTTP Sigs + {:signature_actor, {:error, _}} -> + {:cancel, :invalid_signature} + + {:signature, false} -> + {:cancel, :invalid_signature} + + {:same_actor, false} -> + {:cancel, :actor_signature_mismatch} + + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> + {:cancel, :origin_containment_failed} + + # Unclear if this can be reached + {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> + {:cancel, reason} + + # Fail closed if the retry cannot reconstruct the original request. + :missing_signature_retry_metadata -> + {:cancel, :missing_signature_retry_metadata} + + {:error, :invalid_signature_retry_metadata} -> + {:cancel, :invalid_signature_retry_metadata} + + # Catchall + {:error, _} = e -> + e + + e -> + {:error, e} + end + + log_signature_retry_rejection(result, context) + result end + + defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do + signature_actor = + case signature_actor_result do + {:ok, actor} when is_binary(actor) -> actor + actor when is_binary(actor) -> actor + _ -> nil + end + + %{ + activity_id: params["id"], + payload_actor: Utils.get_ap_id(params["actor"]), + request_path: request_path, + signature_actor: signature_actor, + type: params["type"] + } + end + + defp retry_log_context(_params, request_path, signature_actor_result) do + signature_actor = + case signature_actor_result do + {:ok, actor} when is_binary(actor) -> actor + actor when is_binary(actor) -> actor + _ -> nil + end + + %{ + activity_id: nil, + payload_actor: nil, + request_path: request_path, + signature_actor: signature_actor, + type: nil + } + end + + defp log_signature_retry_rejection({:cancel, reason}, context) + when reason in [ + :actor_signature_mismatch, + :invalid_signature, + :invalid_signature_retry_metadata, + :missing_signature_retry_metadata, + :origin_containment_failed + ] do + Logger.warning( + "Failed-signature inbox retry rejected " <> + "reason=#{inspect(reason)} " <> + "payload_actor=#{inspect(context[:payload_actor])} " <> + "signature_actor=#{inspect(context[:signature_actor])} " <> + "activity_id=#{inspect(context[:activity_id])} " <> + "type=#{inspect(context[:type])} " <> + "request_path=#{inspect(context[:request_path])}" + ) + end + + defp log_signature_retry_rejection(_result, _context), do: :ok end diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 02706ebad..f4ec0e2e3 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -6,8 +6,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo + import ExUnit.CaptureLog import Pleroma.Factory + @moduletag capture_log: true + alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Signature @@ -73,7 +76,9 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do defp assert_mismatched_signature_cancelled(params, signer) do assert {:ok, oban_job} = enqueue_failed_signature(params, signer) - assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + capture_log([level: :warning], fn -> + assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + end) end test "Federator preserves request metadata for failed-signature retry jobs" do @@ -108,25 +113,54 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do test "cancels retry jobs without request metadata" do params = insert(:note_activity).data - assert {:cancel, :missing_signature_retry_metadata} = - SignatureRetryWorker.perform(%Oban.Job{ - args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} - }) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :missing_signature_retry_metadata} = + SignatureRetryWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} + }) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:missing_signature_retry_metadata" + assert log =~ "payload_actor=#{inspect(params["actor"])}" + assert log =~ "activity_id=#{inspect(params["id"])}" + assert log =~ "type=#{inspect(params["type"])}" + assert log =~ "request_path=nil" end test "cancels retry jobs with malformed serialized request headers" do params = insert(:note_activity).data - assert {:cancel, :invalid_signature_retry_metadata} = - SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature_retry_metadata} = + SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature_retry_metadata" + assert log =~ "signature_actor=nil" + assert log =~ "request_path=\"/inbox\"" end test "cancels retry jobs without a signature header" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") params = insert(:note_activity, user: alice).data - assert {:cancel, :invalid_signature} = - SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform( + failed_signature_job(params, [{"host", "local.test"}]) + ) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature" + assert log =~ "payload_actor=#{inspect(params["actor"])}" + assert log =~ "signature_actor=nil" + assert log =~ "request_path=\"/inbox\"" end test "cancels missing signature before fetching an unavailable payload actor" do @@ -194,7 +228,20 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do stub_actor_fetch(alice) assert {:ok, oban_job} = enqueue_failed_signature(create, alice) - assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature" + assert log =~ "payload_actor=\"https://one.com/users/alice\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + refute Activity.get_by_ap_id(create["id"]) end @@ -352,6 +399,39 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do assert_mismatched_signature_cancelled(create, alice) end + test "logs signature actor mismatch retry rejections" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/logged-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/logged-forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + log = assert_mismatched_signature_cancelled(create, alice) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:actor_signature_mismatch" + assert log =~ "payload_actor=\"https://two.com/users/bob\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + end + test "cancels signature actor mismatch before actually creating a forged post" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 00dd1b5103afce3e4e57cb6908d05ea6d94edeaf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 10:19:33 +0400 Subject: [PATCH 305/317] Add failed-signature retry regression tests --- .../object_validators/update_validator.ex | 2 +- .../activity_pub_controller_test.exs | 33 +++++++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 16 +++++++++ .../workers/signature_retry_worker_test.exs | 25 ++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index ad3c0e3e2..4c0d9dff7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do cng |> add_error(:object, "Can't be updated by this actor") - true -> + _ -> cng |> add_error(:object, "Update is neither for Object or Actor") end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 62c1dd830..b8af1e31b 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -819,6 +819,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not delete an object after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + object_id = note.data["id"] + + data = %{ + "type" => "Delete", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/inbox-forged-delete", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => object_id + } + + expect_signature_retry_from(alice) + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) + + refute Activity.get_by_ap_id(data["id"]) + assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id) + end + test "does not create a forged post signed by a different actor", %{conn: conn} do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 67a3f902e..ea05f38f1 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -237,6 +237,22 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do refute Pleroma.Object.get_by_ap_id(object_id) end + test "fails closed for legacy retry jobs missing one metadata field" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity).data + + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => params, + "req_headers" => signature_headers_for(alice), + "request_path" => "/inbox" + } + }) + end + test "fails closed for malformed legacy metadata jobs without params" do assert {:cancel, :missing_signature_retry_metadata} = ReceiverWorker.perform(%Oban.Job{ diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index f4ec0e2e3..94dd5f6c1 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -399,6 +399,31 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do assert_mismatched_signature_cancelled(create, alice) end + test "cancels signature actor mismatch when payload actor is embedded" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => %{"id" => bob.ap_id}, + "id" => "https://two.com/activities/embedded-actor-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/embedded-actor-forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + end + test "logs signature actor mismatch retry rejections" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 6ae02d71bd10006710e24a97bcb6b9a895ed50cd Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 10:33:42 +0400 Subject: [PATCH 306/317] Align inbox controller tests with signer mapping --- .../activity_pub_controller_test.exs | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b8af1e31b..3988c3912 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -37,6 +37,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:instance, :federating], true) + defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do + assign_valid_signature_for_actor(conn, actor_id) + end + + defp assign_valid_signature_for_actor(conn, actor) do + actor_id = Utils.get_ap_id(actor) + + conn + |> assign(:valid_signature, true) + |> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"") + end + defp expect_signature_retry_from(%User{} = signer) do signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") @@ -707,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -735,7 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -954,7 +966,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(followed_relay) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", accept) |> json_response(200) @@ -1034,16 +1046,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do test "Unknown activity types are discarded", %{conn: conn} do unknown_types = ["Poke", "Read", "Dazzle"] + actor = + insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody") + Enum.each(unknown_types, fn bad_type -> params = %{ "type" => bad_type, - "actor" => "https://unknown.mastodon.instance/users/somebody" + "actor" => actor.ap_id } |> Jason.encode!() conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(actor) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", params) |> json_response(400) @@ -1112,7 +1127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1133,7 +1148,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1202,7 +1217,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1221,7 +1236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1252,7 +1267,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1273,7 +1288,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1294,7 +1309,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1318,7 +1333,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1345,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1375,7 +1390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) @@ -1440,7 +1455,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) |> json_response(200) @@ -1530,7 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1584,7 +1599,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1617,7 +1632,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1640,7 +1655,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1663,7 +1678,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -2783,6 +2798,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:media_proxy]) setup do: clear_config([Pleroma.Upload]) + # majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test. + @tag :skip_darwin test "POST /api/ap/upload_media", %{conn: conn} do user = insert(:user) From 1a8d585cbfe8320ac6b35f86c05559592457f133 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 12:10:23 +0400 Subject: [PATCH 307/317] Woodpecker CI: Allow rerunning OTP package uploads --- .woodpecker/otp-musl.yaml | 6 ++++++ .woodpecker/otp.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 6f007a127..60558b893 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -146,6 +146,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + update: 'true' upload-latest-amd64-musl: image: *artifacts_uploader_image @@ -171,6 +172,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-amd64-musl: @@ -196,6 +198,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + update: 'true' upload-latest-arm-musl: image: *artifacts_uploader_image @@ -221,6 +224,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm-musl: @@ -246,6 +250,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + update: 'true' upload-latest-arm64-musl: image: *artifacts_uploader_image @@ -271,6 +276,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm64-musl: diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 7e1bad611..9a33c228e 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -147,6 +147,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + update: 'true' upload-latest-amd64: image: *artifacts_uploader_image @@ -172,6 +173,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-amd64: @@ -197,6 +199,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + update: 'true' upload-latest-arm: image: *artifacts_uploader_image @@ -222,6 +225,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm: @@ -247,6 +251,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + update: 'true' upload-latest-arm64: image: *artifacts_uploader_image @@ -272,6 +277,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm64: From 6553ba24aa6b1a835e923019bb2e0f277abd85f9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 16:56:01 +0400 Subject: [PATCH 308/317] Prepare 2.10.1 release --- CHANGELOG.md | 48 +++++++++++++++++++ changelog.d/assign-users.add | 1 - .../avatar-description-mastodon-api.change | 1 - changelog.d/bandit.change | 1 - changelog.d/bookmark-folders.ignore | 1 - changelog.d/boost-visibilities.add | 1 - changelog.d/cache-control-immutable.add | 1 - changelog.d/ci-artifacts.skip | 0 changelog.d/credo-aliases-sort-fixes.skip | 0 changelog.d/database-config-whitelist.add | 1 - changelog.d/elixir-1.19-cherrypicks.change | 1 - changelog.d/email_digest.fix | 1 - changelog.d/emoji-reaction-url-escape.fix | 1 - changelog.d/exclusive-lists.add | 1 - .../gopher-genserver-crash-on-boot.fix | 1 - changelog.d/hackney-downgrade.change | 1 - changelog.d/hackney-mediaproxy.change | 1 - changelog.d/hackney.change | 1 - changelog.d/hubzilla-alsoknownas.fix | 1 - changelog.d/inappropriate-docs.remove | 1 - changelog.d/instance-domain-blocks.add | 1 - changelog.d/instance-profile-fields.add | 1 - changelog.d/lint-warnings.skip | 0 changelog.d/live-dashboard-redirect.fix | 1 - changelog.d/map-side-effects.skip | 0 changelog.d/missing-static-file.fix | 1 - changelog.d/mix-exs-fix.skip | 0 changelog.d/mix-exs-update.skip | 0 .../oauth-registration-redirect_uris.fix | 1 - changelog.d/oban-web.add | 1 - changelog.d/old-migrations.fix | 1 - changelog.d/paginate-follow-requests.change | 1 - changelog.d/phoenix-livedashboard-move.change | 1 - changelog.d/plug-test-typo.skip | 0 changelog.d/poll-voters-count.fix | 1 - changelog.d/prune-hashtag-follow-3376.fix | 1 - changelog.d/rate-limiter-hardening.fix | 1 - changelog.d/reduce-flaky-tests.skip | 1 - changelog.d/relationship-expires-at.change | 1 - changelog.d/release-to-docker.add | 1 - changelog.d/restore-embeds.fix | 1 - .../reverseproxy-recursive-redirect.fix | 1 - changelog.d/search-indexing.change | 1 - changelog.d/search-indexing.skip | 0 changelog.d/twitter-api.skip | 0 changelog.d/update-comment.ignore | 1 - changelog.d/user-view.ignore | 1 - changelog.d/vix-0.36.0.fix | 1 - changelog.d/woodpecker-pr-pipeline.skip | 0 changelog.d/woodpecker-release-pipeline.skip | 0 mix.exs | 2 +- 51 files changed, 49 insertions(+), 39 deletions(-) delete mode 100644 changelog.d/assign-users.add delete mode 100644 changelog.d/avatar-description-mastodon-api.change delete mode 100644 changelog.d/bandit.change delete mode 100644 changelog.d/bookmark-folders.ignore delete mode 100644 changelog.d/boost-visibilities.add delete mode 100644 changelog.d/cache-control-immutable.add delete mode 100644 changelog.d/ci-artifacts.skip delete mode 100644 changelog.d/credo-aliases-sort-fixes.skip delete mode 100644 changelog.d/database-config-whitelist.add delete mode 100644 changelog.d/elixir-1.19-cherrypicks.change delete mode 100644 changelog.d/email_digest.fix delete mode 100644 changelog.d/emoji-reaction-url-escape.fix delete mode 100644 changelog.d/exclusive-lists.add delete mode 100644 changelog.d/gopher-genserver-crash-on-boot.fix delete mode 100644 changelog.d/hackney-downgrade.change delete mode 100644 changelog.d/hackney-mediaproxy.change delete mode 100644 changelog.d/hackney.change delete mode 100644 changelog.d/hubzilla-alsoknownas.fix delete mode 100644 changelog.d/inappropriate-docs.remove delete mode 100644 changelog.d/instance-domain-blocks.add delete mode 100644 changelog.d/instance-profile-fields.add delete mode 100644 changelog.d/lint-warnings.skip delete mode 100644 changelog.d/live-dashboard-redirect.fix delete mode 100644 changelog.d/map-side-effects.skip delete mode 100644 changelog.d/missing-static-file.fix delete mode 100644 changelog.d/mix-exs-fix.skip delete mode 100644 changelog.d/mix-exs-update.skip delete mode 100644 changelog.d/oauth-registration-redirect_uris.fix delete mode 100644 changelog.d/oban-web.add delete mode 100644 changelog.d/old-migrations.fix delete mode 100644 changelog.d/paginate-follow-requests.change delete mode 100644 changelog.d/phoenix-livedashboard-move.change delete mode 100644 changelog.d/plug-test-typo.skip delete mode 100644 changelog.d/poll-voters-count.fix delete mode 100644 changelog.d/prune-hashtag-follow-3376.fix delete mode 100644 changelog.d/rate-limiter-hardening.fix delete mode 100644 changelog.d/reduce-flaky-tests.skip delete mode 100644 changelog.d/relationship-expires-at.change delete mode 100644 changelog.d/release-to-docker.add delete mode 100644 changelog.d/restore-embeds.fix delete mode 100644 changelog.d/reverseproxy-recursive-redirect.fix delete mode 100644 changelog.d/search-indexing.change delete mode 100644 changelog.d/search-indexing.skip delete mode 100644 changelog.d/twitter-api.skip delete mode 100644 changelog.d/update-comment.ignore delete mode 100644 changelog.d/user-view.ignore delete mode 100644 changelog.d/vix-0.36.0.fix delete mode 100644 changelog.d/woodpecker-pr-pipeline.skip delete mode 100644 changelog.d/woodpecker-release-pipeline.skip diff --git a/CHANGELOG.md b/CHANGELOG.md index adc76c767..c7bb9b09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ 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/). +## 2.10.1 + +### Changed + +- Move avatar_description and header_description fields to the account object +- Update Bandit to 1.10.4 +- No-op code correctness improvements detected by Elixir 1.19 compiler +- Downgrade Hackney to 1.20.1 +- Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney +- Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation +- Paginate follow requests +- Moved Phoenix LiveDashboard to /pleroma/live_dashboard +- Add mute/block expiry to the relationship object +- Filter indexable activities before inserting indexing jobs into the queue. + +### Added + +- Allow assigning users to reports +- Allow fine-grained announce visibilities +- Add immutable tag on cache-control header for several endpoints that's serving the same exact things. +- Add reasonable defaults for :database_config_whitelist +- Support lists `exclusive` param +- Add v1/instance/domain_blocks endpoint +- Add /api/v2/instance profile fields limits info used by Mastodon +- Added Oban Web dashboard located at /pleroma/oban +- Add instructions on how to run a release in docker, to make it easier to run on older distros. + +### Fixed + +- Fix the daily email digest job which was not executing +- Encode custom emoji URLs in EmojiReact activity tags. +- Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled +- Fix fetching Hubzilla Actors with alsoKnownAs as string +- Fix /phoenix/live_dashboard redirect not working when user added a path segment +- Fix 404 error codes for missing static files +- Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. +- Correct old migrations for expiring activities and user access tokens. +- Federate `votersCount` correctly +- DB prune: Check if user follows hashtag with no objects before deletion +- Stop the rate limiter from crashing when run with wrong settings. +- Restore embeds route +- ReverseProxy: Recursively follow redirects until redirect_limit is reached +- Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 + +### Removed + +- Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS) + ## 2.10 ### Security diff --git a/changelog.d/assign-users.add b/changelog.d/assign-users.add deleted file mode 100644 index f50ad94c6..000000000 --- a/changelog.d/assign-users.add +++ /dev/null @@ -1 +0,0 @@ -Allow assigning users to reports \ No newline at end of file diff --git a/changelog.d/avatar-description-mastodon-api.change b/changelog.d/avatar-description-mastodon-api.change deleted file mode 100644 index 6a454c01e..000000000 --- a/changelog.d/avatar-description-mastodon-api.change +++ /dev/null @@ -1 +0,0 @@ -Move avatar_description and header_description fields to the account object diff --git a/changelog.d/bandit.change b/changelog.d/bandit.change deleted file mode 100644 index 3831a02c2..000000000 --- a/changelog.d/bandit.change +++ /dev/null @@ -1 +0,0 @@ -Update Bandit to 1.10.4 diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore deleted file mode 100644 index 8705ac00b..000000000 --- a/changelog.d/bookmark-folders.ignore +++ /dev/null @@ -1 +0,0 @@ -Various bookmark folders-related improvements diff --git a/changelog.d/boost-visibilities.add b/changelog.d/boost-visibilities.add deleted file mode 100644 index 317d9840d..000000000 --- a/changelog.d/boost-visibilities.add +++ /dev/null @@ -1 +0,0 @@ -Allow fine-grained announce visibilities diff --git a/changelog.d/cache-control-immutable.add b/changelog.d/cache-control-immutable.add deleted file mode 100644 index 516db67bf..000000000 --- a/changelog.d/cache-control-immutable.add +++ /dev/null @@ -1 +0,0 @@ -Add immutable tag on cache-control header for several endpoints that's serving the same exact things. \ No newline at end of file diff --git a/changelog.d/ci-artifacts.skip b/changelog.d/ci-artifacts.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/credo-aliases-sort-fixes.skip b/changelog.d/credo-aliases-sort-fixes.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/database-config-whitelist.add b/changelog.d/database-config-whitelist.add deleted file mode 100644 index a78960c98..000000000 --- a/changelog.d/database-config-whitelist.add +++ /dev/null @@ -1 +0,0 @@ -Add reasonable defaults for :database_config_whitelist \ No newline at end of file diff --git a/changelog.d/elixir-1.19-cherrypicks.change b/changelog.d/elixir-1.19-cherrypicks.change deleted file mode 100644 index 7e56be008..000000000 --- a/changelog.d/elixir-1.19-cherrypicks.change +++ /dev/null @@ -1 +0,0 @@ -No-op code correctness improvements detected by Elixir 1.19 compiler diff --git a/changelog.d/email_digest.fix b/changelog.d/email_digest.fix deleted file mode 100644 index cd15874a2..000000000 --- a/changelog.d/email_digest.fix +++ /dev/null @@ -1 +0,0 @@ -Fix the daily email digest job which was not executing diff --git a/changelog.d/emoji-reaction-url-escape.fix b/changelog.d/emoji-reaction-url-escape.fix deleted file mode 100644 index c3a1c8823..000000000 --- a/changelog.d/emoji-reaction-url-escape.fix +++ /dev/null @@ -1 +0,0 @@ -Encode custom emoji URLs in EmojiReact activity tags. diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add deleted file mode 100644 index bbd722f07..000000000 --- a/changelog.d/exclusive-lists.add +++ /dev/null @@ -1 +0,0 @@ -Support lists `exclusive` param diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix deleted file mode 100644 index 3b51662be..000000000 --- a/changelog.d/gopher-genserver-crash-on-boot.fix +++ /dev/null @@ -1 +0,0 @@ -Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled diff --git a/changelog.d/hackney-downgrade.change b/changelog.d/hackney-downgrade.change deleted file mode 100644 index a98710692..000000000 --- a/changelog.d/hackney-downgrade.change +++ /dev/null @@ -1 +0,0 @@ -Downgrade Hackney to 1.20.1 diff --git a/changelog.d/hackney-mediaproxy.change b/changelog.d/hackney-mediaproxy.change deleted file mode 100644 index 10dfb0775..000000000 --- a/changelog.d/hackney-mediaproxy.change +++ /dev/null @@ -1 +0,0 @@ -Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney diff --git a/changelog.d/hackney.change b/changelog.d/hackney.change deleted file mode 100644 index 3158cfc77..000000000 --- a/changelog.d/hackney.change +++ /dev/null @@ -1 +0,0 @@ -Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation diff --git a/changelog.d/hubzilla-alsoknownas.fix b/changelog.d/hubzilla-alsoknownas.fix deleted file mode 100644 index 2a2969807..000000000 --- a/changelog.d/hubzilla-alsoknownas.fix +++ /dev/null @@ -1 +0,0 @@ -Fix fetching Hubzilla Actors with alsoKnownAs as string diff --git a/changelog.d/inappropriate-docs.remove b/changelog.d/inappropriate-docs.remove deleted file mode 100644 index 699c9186a..000000000 --- a/changelog.d/inappropriate-docs.remove +++ /dev/null @@ -1 +0,0 @@ -Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS) diff --git a/changelog.d/instance-domain-blocks.add b/changelog.d/instance-domain-blocks.add deleted file mode 100644 index 85f01c5c2..000000000 --- a/changelog.d/instance-domain-blocks.add +++ /dev/null @@ -1 +0,0 @@ -Add v1/instance/domain_blocks endpoint diff --git a/changelog.d/instance-profile-fields.add b/changelog.d/instance-profile-fields.add deleted file mode 100644 index 712bd68d9..000000000 --- a/changelog.d/instance-profile-fields.add +++ /dev/null @@ -1 +0,0 @@ -Add /api/v2/instance profile fields limits info used by Mastodon diff --git a/changelog.d/lint-warnings.skip b/changelog.d/lint-warnings.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/live-dashboard-redirect.fix b/changelog.d/live-dashboard-redirect.fix deleted file mode 100644 index 10588d89e..000000000 --- a/changelog.d/live-dashboard-redirect.fix +++ /dev/null @@ -1 +0,0 @@ -Fix /phoenix/live_dashboard redirect not working when user added a path segment diff --git a/changelog.d/map-side-effects.skip b/changelog.d/map-side-effects.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/missing-static-file.fix b/changelog.d/missing-static-file.fix deleted file mode 100644 index c7ef805aa..000000000 --- a/changelog.d/missing-static-file.fix +++ /dev/null @@ -1 +0,0 @@ -Fix 404 error codes for missing static files diff --git a/changelog.d/mix-exs-fix.skip b/changelog.d/mix-exs-fix.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/mix-exs-update.skip b/changelog.d/mix-exs-update.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/oauth-registration-redirect_uris.fix b/changelog.d/oauth-registration-redirect_uris.fix deleted file mode 100644 index 76ace55df..000000000 --- a/changelog.d/oauth-registration-redirect_uris.fix +++ /dev/null @@ -1 +0,0 @@ -Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. diff --git a/changelog.d/oban-web.add b/changelog.d/oban-web.add deleted file mode 100644 index c59e2ebca..000000000 --- a/changelog.d/oban-web.add +++ /dev/null @@ -1 +0,0 @@ -Added Oban Web dashboard located at /pleroma/oban diff --git a/changelog.d/old-migrations.fix b/changelog.d/old-migrations.fix deleted file mode 100644 index 49566c896..000000000 --- a/changelog.d/old-migrations.fix +++ /dev/null @@ -1 +0,0 @@ -Correct old migrations for expiring activities and user access tokens. diff --git a/changelog.d/paginate-follow-requests.change b/changelog.d/paginate-follow-requests.change deleted file mode 100644 index 1a88995b7..000000000 --- a/changelog.d/paginate-follow-requests.change +++ /dev/null @@ -1 +0,0 @@ -Paginate follow requests diff --git a/changelog.d/phoenix-livedashboard-move.change b/changelog.d/phoenix-livedashboard-move.change deleted file mode 100644 index 116b1523a..000000000 --- a/changelog.d/phoenix-livedashboard-move.change +++ /dev/null @@ -1 +0,0 @@ -Moved Phoenix LiveDashboard to /pleroma/live_dashboard diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/poll-voters-count.fix b/changelog.d/poll-voters-count.fix deleted file mode 100644 index 2dbc81b5d..000000000 --- a/changelog.d/poll-voters-count.fix +++ /dev/null @@ -1 +0,0 @@ -Federate `votersCount` correctly diff --git a/changelog.d/prune-hashtag-follow-3376.fix b/changelog.d/prune-hashtag-follow-3376.fix deleted file mode 100644 index cdb4e9a79..000000000 --- a/changelog.d/prune-hashtag-follow-3376.fix +++ /dev/null @@ -1 +0,0 @@ -DB prune: Check if user follows hashtag with no objects before deletion diff --git a/changelog.d/rate-limiter-hardening.fix b/changelog.d/rate-limiter-hardening.fix deleted file mode 100644 index a3af8fcc4..000000000 --- a/changelog.d/rate-limiter-hardening.fix +++ /dev/null @@ -1 +0,0 @@ -Stop the rate limiter from crashing when run with wrong settings. diff --git a/changelog.d/reduce-flaky-tests.skip b/changelog.d/reduce-flaky-tests.skip deleted file mode 100644 index 0375762c0..000000000 --- a/changelog.d/reduce-flaky-tests.skip +++ /dev/null @@ -1 +0,0 @@ -Reduce the number of flaky tests by making them sync if they affect the global state, and silence noisy test output. diff --git a/changelog.d/relationship-expires-at.change b/changelog.d/relationship-expires-at.change deleted file mode 100644 index 286dba197..000000000 --- a/changelog.d/relationship-expires-at.change +++ /dev/null @@ -1 +0,0 @@ -Add mute/block expiry to the relationship object diff --git a/changelog.d/release-to-docker.add b/changelog.d/release-to-docker.add deleted file mode 100644 index 5fbf611a5..000000000 --- a/changelog.d/release-to-docker.add +++ /dev/null @@ -1 +0,0 @@ -Add instructions on how to run a release in docker, to make it easier to run on older distros. diff --git a/changelog.d/restore-embeds.fix b/changelog.d/restore-embeds.fix deleted file mode 100644 index 5a2a1c4fe..000000000 --- a/changelog.d/restore-embeds.fix +++ /dev/null @@ -1 +0,0 @@ -Restore embeds route diff --git a/changelog.d/reverseproxy-recursive-redirect.fix b/changelog.d/reverseproxy-recursive-redirect.fix deleted file mode 100644 index 744109fd6..000000000 --- a/changelog.d/reverseproxy-recursive-redirect.fix +++ /dev/null @@ -1 +0,0 @@ -ReverseProxy: Recursively follow redirects until redirect_limit is reached diff --git a/changelog.d/search-indexing.change b/changelog.d/search-indexing.change deleted file mode 100644 index 766934f3f..000000000 --- a/changelog.d/search-indexing.change +++ /dev/null @@ -1 +0,0 @@ -Filter indexable activities before inserting indexing jobs into the queue. diff --git a/changelog.d/search-indexing.skip b/changelog.d/search-indexing.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore deleted file mode 100644 index 733e813b3..000000000 --- a/changelog.d/update-comment.ignore +++ /dev/null @@ -1 +0,0 @@ -Update comment for prepare_object, rename prepare_outgoing diff --git a/changelog.d/user-view.ignore b/changelog.d/user-view.ignore deleted file mode 100644 index 37e9a7e09..000000000 --- a/changelog.d/user-view.ignore +++ /dev/null @@ -1 +0,0 @@ -Avoid code duplication in UserView diff --git a/changelog.d/vix-0.36.0.fix b/changelog.d/vix-0.36.0.fix deleted file mode 100644 index 43a8dd8f8..000000000 --- a/changelog.d/vix-0.36.0.fix +++ /dev/null @@ -1 +0,0 @@ -Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/woodpecker-release-pipeline.skip b/changelog.d/woodpecker-release-pipeline.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/mix.exs b/mix.exs index 5d84c6e09..5da2ca657 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.10.0"), + version: version("2.10.1"), elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From 621d86a31da0900cc6ddd5bae1b6c65204b7b5c8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 18:02:59 +0400 Subject: [PATCH 309/317] Validate WebFinger nicknames against actors --- lib/pleroma/web/activity_pub/activity_pub.ex | 106 +++++++++++++------ test/pleroma/user_test.exs | 95 +++++++++++++++-- 2 files changed, 161 insertions(+), 40 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 071d634db..0b513ee16 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do show_birthday = !!birthday - # if WebFinger request was already done, we probably have acct, otherwise - # we request WebFinger here - nickname = additional[:nickname_from_acct] || generate_nickname(data) + with {:ok, nickname} <- nickname_from_actor(data, additional) do + {:ok, + %{ + ap_id: data["id"], + uri: get_actor_url(data["url"]), + banner: normalize_image(data["image"]), + fields: fields, + emoji: emojis, + is_locked: is_locked, + is_discoverable: is_discoverable, + invisible: invisible, + avatar: normalize_image(data["icon"]), + name: data["name"], + follower_address: data["followers"], + following_address: data["following"], + featured_address: featured_address, + bio: data["summary"] || "", + actor_type: actor_type, + also_known_as: normalize_also_known_as(data["alsoKnownAs"]), + public_key: public_key, + inbox: data["inbox"], + shared_inbox: shared_inbox, + accepts_chat_messages: accepts_chat_messages, + birthday: birthday, + show_birthday: show_birthday, + pinned_objects: pinned_objects, + nickname: nickname + }} + end + end - %{ - ap_id: data["id"], - uri: get_actor_url(data["url"]), - banner: normalize_image(data["image"]), - fields: fields, - emoji: emojis, - is_locked: is_locked, - is_discoverable: is_discoverable, - invisible: invisible, - avatar: normalize_image(data["icon"]), - name: data["name"], - follower_address: data["followers"], - following_address: data["following"], - featured_address: featured_address, - bio: data["summary"] || "", - actor_type: actor_type, - also_known_as: normalize_also_known_as(data["alsoKnownAs"]), - public_key: public_key, - inbox: data["inbox"], - shared_inbox: shared_inbox, - accepts_chat_messages: accepts_chat_messages, - birthday: birthday, - show_birthday: show_birthday, - pinned_objects: pinned_objects, - nickname: nickname - } + defp nickname_from_actor(data, additional) do + generated = generated_nickname(data) + + case additional[:nickname_from_acct] do + ^generated when is_binary(generated) -> + {:ok, generated} + + acct when is_binary(acct) -> + with ^acct <- webfinger_nickname(data) do + {:ok, acct} + else + _ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}} + end + + _ -> + {:ok, generate_nickname(data)} + end + end + + defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id}) + when is_binary(username) and is_binary(ap_id) do + case URI.parse(ap_id) do + %URI{host: host} when is_binary(host) -> "#{username}@#{host}" + _ -> nil + end + end + + defp generated_nickname(_), do: nil + + defp webfinger_nickname(data) do + with generated when is_binary(generated) <- generated_nickname(data), + {:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated), + true <- ap_id == data["id"] do + acct + end end defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do - generated = "#{username}@#{URI.parse(data["id"]).host}" + generated = generated_nickname(data) if Config.get([WebFinger, :update_nickname_on_user_fetch]) do - case WebFinger.finger(generated) do - {:ok, %{"subject" => "acct:" <> acct}} -> acct + case webfinger_nickname(data) do + acct when is_binary(acct) -> acct _ -> generated end else @@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp collection_private(_data), do: {:ok, true} def user_data_from_user_object(data, additional \\ []) do - with {:ok, data} <- MRF.filter(data) do - {:ok, object_to_user_data(data, additional)} + with {:ok, data} <- MRF.filter(data), + {:ok, data} <- object_to_user_data(data, additional) do + {:ok, data} else + {:error, _} = e -> e e -> {:error, e} end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index b2533e9f1..de735cdb7 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) - test "for mastodon" do - ap_id = "a@mastodon.example" - {:ok, fetched_user} = User.get_or_fetch(ap_id) + test "fetches a mastodon split-domain nickname" do + nickname = "a@mastodon.example" + {:ok, fetched_user} = User.get_or_fetch(nickname) assert fetched_user.ap_id == "https://sub.mastodon.example/users/a" assert fetched_user.nickname == "a@mastodon.example" end - test "for pleroma" do - ap_id = "a@pleroma.example" - {:ok, fetched_user} = User.get_or_fetch(ap_id) + test "fetches a pleroma split-domain nickname" do + nickname = "a@pleroma.example" + {:ok, fetched_user} = User.get_or_fetch(nickname) assert fetched_user.ap_id == "https://sub.pleroma.example/users/a" assert fetched_user.nickname == "a@pleroma.example" @@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do assert fetched_user == "not found nonexistent" end + test "does not rename an existing remote actor from rogue WebFinger data" do + clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) + + actor_id = "https://legit-actor.example/users/alice" + + Tesla.Mock.mock(fn + %{url: "https://evil-webfinger.example/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:claimed@evil-webfinger.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => actor_id + } + ] + }) + + %{url: ^actor_id} -> + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => actor_id, + "type" => "Person", + "preferredUsername" => "alice", + "name" => "Alice", + "summary" => "", + "inbox" => "https://legit-actor.example/users/alice/inbox", + "outbox" => "https://legit-actor.example/users/alice/outbox", + "followers" => "https://legit-actor.example/users/alice/followers", + "following" => "https://legit-actor.example/users/alice/following" + }) + }} + + %{url: "https://legit-actor.example/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:alice@legit-actor.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => actor_id + } + ] + }) + end) + + assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} = + ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example") + + refute User.get_by_ap_id(actor_id) + refute User.get_by_nickname("claimed@evil-webfinger.example") + + orig_user = + insert(:user, + local: false, + nickname: "alice@legit-actor.example", + ap_id: actor_id + ) + + assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} = + ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example") + + assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example") + assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example" + refute User.get_by_nickname("claimed@evil-webfinger.example") + end + test "updates an existing user, if stale" do a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) From 8ccdd98914e3ce11948d877b05a015bfddeebe4b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 20:23:35 +0400 Subject: [PATCH 310/317] Prepare 2.10.2 release --- CHANGELOG.md | 6 ++++++ changelog.d/activitypub-spoofing.security | 1 - mix.exs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/activitypub-spoofing.security diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bb9b09d..f03be3f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 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/). +## 2.10.2 + +### Security + +- ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed + ## 2.10.1 ### Changed diff --git a/changelog.d/activitypub-spoofing.security b/changelog.d/activitypub-spoofing.security deleted file mode 100644 index 3e6baffb6..000000000 --- a/changelog.d/activitypub-spoofing.security +++ /dev/null @@ -1 +0,0 @@ -ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed diff --git a/mix.exs b/mix.exs index 5da2ca657..bb7bfb9da 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.10.1"), + version: version("2.10.2"), elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From aec0deef8b3018ddcced65c7bd883514922b7539 Mon Sep 17 00:00:00 2001 From: Yonle Date: Tue, 5 May 2026 13:49:29 +0700 Subject: [PATCH 311/317] poll_view: try to read votersCount first, and then manually count local voters. --- lib/pleroma/web/mastodon_api/views/poll_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 3b4271227..67f874b46 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,12 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end + defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters - defp voters_count(_), do: 0 defp voted_and_own_votes(%{object: object} = params, options) do From 727e9e77497f20e2b61bd6267555015d2c308696 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:33:34 +0400 Subject: [PATCH 312/317] Fix votersCount inflation in multiple-choice polls increase_vote_count/3 was incrementing votersCount on every vote activity, causing inflation when a single voter picks multiple options. Now only increments when the actor is a new unique voter, and preserves existing votersCount otherwise. Also adds is_integer guard to voters_count/1 to handle nil safely, and adds tests for the voters_count clause ordering and edge cases. --- lib/pleroma/object.ex | 18 +- .../web/mastodon_api/views/poll_view.ex | 2 +- .../web/mastodon_api/views/poll_view_test.exs | 175 ++++++++++++++++++ 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index f1e07a257..5e9314446 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -372,13 +372,21 @@ defmodule Pleroma.Object do option end) - voters = [actor | object.data["voters"] || []] |> Enum.uniq() + existing_voters = object.data["voters"] || [] + voters = [actor | existing_voters] |> Enum.uniq() + new_voter? = actor not in existing_voters + existing_voters_count = object.data["votersCount"] voters_count = - if Map.has_key?(object.data, "votersCount") do - object.data["votersCount"] + 1 - else - length(voters) + cond do + is_integer(existing_voters_count) and new_voter? -> + existing_voters_count + 1 + + is_integer(existing_voters_count) -> + existing_voters_count + + true -> + length(voters) end data = diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 67f874b46..f047804e2 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,7 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index 16281393d..6cb5934de 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do assert result[:pleroma][:non_anonymous] == true end + + test "prefers votersCount over voters list when both are present" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + + object = %{ + object + | data: Map.put(object.data, "votersCount", 42) + } + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 42 + end + + test "falls back to voters list when votersCount is absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert length(object.data["voters"]) == 1 + + data = Map.delete(object.data, "votersCount") + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 1 + end + + test "returns 0 when both votersCount and voters are absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.delete("voters") + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "returns 0 when voters list is empty" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.put("voters", []) + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "does not inflate votersCount when same voter picks multiple options" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + end + + test "preserves votersCount from remote source when existing voter picks another option" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1]) + + object = %{object | data: Map.put(object.data, "votersCount", 14)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 14 + end + + test "returns 0 when votersCount is explicitly 0" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + object = %{object | data: Map.put(object.data, "votersCount", 0)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "falls back to voters list when votersCount is nil" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + object = %{object | data: Map.put(object.data, "votersCount", nil)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == length(object.data["voters"]) + end end From c62d1919867b287338f497d089acc81cd5b9a31b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:48:20 +0400 Subject: [PATCH 313/317] Add changelog for votersCount inflation fix --- changelog.d/poll-voters-count-inflation.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/poll-voters-count-inflation.fix diff --git a/changelog.d/poll-voters-count-inflation.fix b/changelog.d/poll-voters-count-inflation.fix new file mode 100644 index 000000000..7eae41f13 --- /dev/null +++ b/changelog.d/poll-voters-count-inflation.fix @@ -0,0 +1 @@ +Fix votersCount inflation when same voter picks multiple options From 4873991983ca396a86a7a86c2b423ff29936afa2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 6 May 2026 15:40:47 +0200 Subject: [PATCH 314/317] Update Pleroma-FE build artifacts URL --- changelog.d/pleroma-fe-link.fix | 1 + config/config.exs | 2 +- lib/pleroma/frontend.ex | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pleroma-fe-link.fix diff --git a/changelog.d/pleroma-fe-link.fix b/changelog.d/pleroma-fe-link.fix new file mode 100644 index 000000000..de93f86dd --- /dev/null +++ b/changelog.d/pleroma-fe-link.fix @@ -0,0 +1 @@ +Updated Pleroma-FE build URL after Forgejo migration diff --git a/config/config.exs b/config/config.exs index 5bf2c5c2e..82bdd7750 100644 --- a/config/config.exs +++ b/config/config.exs @@ -775,7 +775,7 @@ config :pleroma, :frontends, "name" => "pleroma-fe", "git" => "https://git.pleroma.social/pleroma/pleroma-fe", "build_url" => - "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build", + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip", "ref" => "develop" }, "fedi-fe" => %{ diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index e651d7d9d..e37a3fefe 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do end defp download_build(frontend_info, dest) do - Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}") url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) + Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}") with {:ok, %{status: 200, body: zip_body}} <- Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do From 6b86e31e5d9d5d6c2175c12c3ea086de101ea33b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 14:53:06 +0400 Subject: [PATCH 315/317] Add backend MFM support --- config/config.exs | 3 +- config/description.exs | 11 +- lib/pleroma/formatter.ex | 11 ++ .../article_note_page_validator.ex | 113 ++++++++++++ .../object_validators/common_fields.ex | 1 + lib/pleroma/web/activity_pub/utils.ex | 3 +- lib/pleroma/web/common_api/activity_draft.ex | 7 +- lib/pleroma/web/common_api/utils.ex | 18 ++ mix.exs | 3 + mix.lock | 1 + priv/scrubbers/default.ex | 42 ++++- test/mix/tasks/pleroma/config_test.exs | 8 +- .../article_note_page_validator_test.exs | 165 ++++++++++++++++++ test/pleroma/web/activity_pub/utils_test.exs | 6 +- test/pleroma/web/common_api_test.exs | 41 +++++ 15 files changed, 423 insertions(+), 10 deletions(-) diff --git a/config/config.exs b/config/config.exs index 82bdd7750..2d38e3ebe 100644 --- a/config/config.exs +++ b/config/config.exs @@ -203,7 +203,8 @@ config :pleroma, :instance, "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ], autofollowed_nicknames: [], autofollowing_nicknames: [], diff --git a/config/description.exs b/config/description.exs index c388d17c3..6e4348907 100644 --- a/config/description.exs +++ b/config/description.exs @@ -815,7 +815,8 @@ config :pleroma, :config_description, [ "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ] }, %{ @@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [ label: "Post Content Type", type: {:dropdown, :atom}, description: "Default post formatting option", - suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] + suggestions: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ] }, %{ key: :redirectRootNoLogin, diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 11d5af2fb..4bf2f6b95 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) end + def markdown_to_html(text, opts) do + Earmark.as_html!( + text, + %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts) + ) + end + def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end @@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do HTML.filter_tags(text) end + def html_escape(text, "text/x.misskeymarkdown") do + HTML.filter_tags(text) + end + def html_escape(text, "text/plain") do Regex.split(@link_regex, text, include_captures: true) |> Enum.map_every(2, fn chunk -> 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 c0626ce4d..ec9259f95 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 @@ -6,6 +6,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.HTML + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier @@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do end field(:replies, {:array, ObjectValidators.ObjectID}, default: []) + field(:source, :map) end def cast_and_apply(data) do @@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def fix_attachments(data), do: data + defp remote_mention_resolver( + %{"id" => ap_id, "tag" => tags}, + "@" <> nickname = mention, + buffer, + opts, + acc + ) + when is_binary(ap_id) and is_list(tags) do + initial_host = + ap_id + |> URI.parse() + |> Map.get(:host) + + with mention_tag when not is_nil(mention_tag) <- + Enum.find(tags, &mention_tag?(&1, mention, initial_host)), + href when is_binary(href) <- mention_tag["href"], + %User{} = user <- User.get_cached_by_ap_id(href) do + link = Pleroma.Formatter.mention_from_user(user, opts) + {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} + else + _ -> {buffer, acc} + end + end + + defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc} + + defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host) + when is_binary(name) do + name == mention || mention == "#{name}@#{initial_host}" + end + + defp mention_tag?(_tag, _mention, _initial_host), do: false + + defp scrub_content(%{"content" => content} = object) when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp scrub_content(object), do: object + + defp mfm_parse_limit do + min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit])) + end + + defp normalize_source(%{"source" => source} = object) when is_binary(source) do + object + |> Map.put("source", %{"content" => source}) + |> normalize_source() + end + + defp normalize_source(%{"source" => source} = object) when is_map(source) do + source = + case source["content"] do + content when is_binary(content) -> + if String.length(content) <= mfm_parse_limit() do + source + else + Map.delete(source, "content") + end + + nil -> + source + + _ -> + Map.delete(source, "content") + end + + Map.put(object, "source", source) + end + + defp normalize_source(object), do: object + + defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object) + when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object + + defp fix_misskey_content( + %{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object + ) + when is_binary(content) do + mention_handler = fn nick, buffer, opts, acc -> + remote_mention_resolver(object, nick, buffer, opts, acc) + end + + {linked, _mentions, _tags} = + Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler) + + Map.put(object, "content", linked) + end + + defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object), + do: scrub_content(object) + + defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do + object + |> Map.put("source", %{ + "content" => content, + "mediaType" => "text/x.misskeymarkdown" + }) + |> Map.delete("_misskey_content") + |> fix_misskey_content() + end + + defp fix_misskey_content(object), do: object + defp fix(data) do data |> CommonFixes.fix_actor() @@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_tag() |> fix_replies() |> fix_attachments() + |> normalize_source() + |> fix_misskey_content() |> CommonFixes.fix_quote_url() |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() 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 22cf0cc05..9b8580200 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do quote bind_quoted: binding() do field(:content, :string) field(:contentMap, ObjectValidators.ContentLanguageMap) + field(:htmlMfm, :boolean) field(:published, ObjectValidators.DateTime) field(:updated, ObjectValidators.DateTime) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 43c0f456d..0af4ceaf5 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do "https://www.w3.org/ns/activitystreams", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld", %{ - "@language" => get_language(data) + "@language" => get_language(data), + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 16489663a..6072fff6b 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do emoji = Map.merge(emoji, summary_emoji) + media_type = Utils.get_content_type(draft.params[:content_type]) {:ok, note_data, _meta} = Builder.note(draft) object = @@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("emoji", emoji) |> Map.put("source", %{ "content" => draft.status, - "mediaType" => Utils.get_content_type(draft.params[:content_type]) + "mediaType" => media_type }) + |> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown") |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) %{draft | object: object} end + defp maybe_put(map, key, value, true), do: Map.put(map, key, value) + defp maybe_put(map, _key, _value, _condition), do: map + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %{draft | preview?: preview?} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 32572a721..26034d685 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.linkify(options) end + def format_input(text, "text/x.misskeymarkdown", options) do + text + |> Formatter.markdown_to_html(%{breaks: true}) + |> safe_mfm_to_html() + |> Formatter.linkify(options) + |> Formatter.html_escape("text/x.misskeymarkdown") + end + def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) @@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.html_escape("text/html") end + defp safe_mfm_to_html(html) do + html + |> MfmParser.Parser.parse() + |> MfmParser.Encoder.to_html() + rescue + _ -> html + catch + _, _ -> html + end + def format_naive_asctime(date) do date |> DateTime.from_naive!("Etc/UTC") |> format_asctime end diff --git a/mix.exs b/mix.exs index bb7bfb9da..88f98d54e 100644 --- a/mix.exs +++ b/mix.exs @@ -160,6 +160,9 @@ defmodule Pleroma.Mixfile do {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, + {:mfm_parser, + git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", + ref: "360a30267a847810a63ab48f606ba227b2ca05f0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, {:swoosh, "~> 1.16.12"}, diff --git a/mix.lock b/mix.lock index dd5f65f11..976a856b0 100644 --- a/mix.lock +++ b/mix.lock @@ -79,6 +79,7 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 0defdc74e..342ef9944 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do "recipients-inline", "quote-inline", "invisible", - "ellipsis" + "ellipsis", + "mfm-center", + "mfm-flip", + "mfm-font", + "mfm-blur", + "mfm-rotate", + "mfm-x2", + "mfm-x3", + "mfm-x4", + "mfm-position", + "mfm-scale", + "mfm-fg", + "mfm-bg", + "mfm-jelly", + "mfm-twitch", + "mfm-shake", + "mfm-spin", + "mfm-jump", + "mfm-bounce", + "mfm-rainbow", + "mfm-tada", + "mfm-sparkle" ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) - Meta.allow_tag_with_these_attributes(:span, ["lang"]) + Meta.allow_tag_with_these_attributes(:span, [ + "lang", + "data-mfm-h", + "data-mfm-v", + "data-mfm-x", + "data-mfm-y", + "data-mfm-alternate", + "data-mfm-speed", + "data-mfm-deg", + "data-mfm-left", + "data-mfm-serif", + "data-mfm-monospace", + "data-mfm-cursive", + "data-mfm-fantasy", + "data-mfm-emoji", + "data-mfm-math", + "data-mfm-color" + ]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index ef1adc235..f672d8c13 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do quarantined_instances: [], managed_config: true, static_dir: "instance/static/", - allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + allowed_post_formats: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], autofollowed_nicknames: [], max_pinned_statuses: 1, attachment_links: false, 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 c32811c5b..bf9c70fb6 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 @@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + test "a Misskey MFM note is rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/alice") + + note = %{ + "id" => "https://misskey.example/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => "$[spin.speed=1s mfm goes here] ", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source["mediaType"] == "text/x.misskeymarkdown" + assert content =~ ~s(class="mfm-spin") + assert content =~ ~s(data-mfm-speed="1s") + assert content =~ "mfm goes here" + refute content =~ "original content" + refute content =~ " "https://misskey.example/notes/3", + "type" => "Note", + "actor" => remote_user.ap_id, + "attributedTo" => remote_user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "tag" => [ + %{ + "type" => "Mention", + "name" => "@local_user", + "href" => local_user.ap_id + }, + %{ + "type" => "Mention", + "name" => "@uncached", + "href" => "https://misskey.example/users/uncached" + } + ], + "source" => %{ + "content" => "@local_user @uncached $[spin hello]", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content =~ local_user.ap_id + assert content =~ "@uncached" + end + + test "a Misskey MFM note drops oversized source content instead of parsing it" do + user = insert(:user, ap_id: "https://misskey.example/users/oversized") + + note = %{ + "id" => "https://misskey.example/notes/4", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "safe fallback", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "safe fallback" + refute Map.has_key?(source, "content") + end + + test "a note drops oversized non-MFM source content" do + user = insert(:user, ap_id: "https://example.com/users/source") + + note = %{ + "id" => "https://example.com/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "regular content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/markdown" + } + } + + %{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"mediaType" => "text/markdown"} + end + + test "a Misskey MFM note with legacy _misskey_content is rendered" do + user = insert(:user, ap_id: "https://misskey.example/users/legacy") + + note = %{ + "id" => "https://misskey.example/notes/5", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "_misskey_content" => "$[spin legacy]" + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"} + assert content =~ ~s(class="mfm-spin") + assert content =~ "legacy" + end + + test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/bob") + + note = %{ + "id" => "https://misskey.example/notes/2", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => + "already rendered", + "htmlMfm" => true, + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "already renderedalert('xss')" + refute Map.has_key?(source, "content") + end + test "a Note with validated likes collection validates" do insert(:user, ap_id: "https://pol.social/users/mkljczk") diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 3b77f0867..93234a015 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "und" + "@language" => "und", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } @@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "pl" + "@language" => "pl", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 017fac696..ea1795c0b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do assert object.data["source"]["content"] == post end + test "it renders MFM posts and marks their ActivityPub representation" do + user = insert(:user) + + post = "

$[spin.speed=1s 13:37]

" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + assert object.data["htmlMfm"] == true + + assert object.data["source"] == %{ + "content" => post, + "mediaType" => "text/x.misskeymarkdown" + } + + assert object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ ~s(data-mfm-speed="1s") + assert object.data["content"] =~ "13:37" + refute object.data["content"] =~ "scrub-this" + end + + test "it falls back safely for malformed MFM" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "$[spin.speed=1s=boom malformed]", + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + refute object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ "malformed" + end + test "it does not allow replies to direct messages that are not direct messages themselves" do user = insert(:user) From c780298ce71577a1bffb755d6f183f049e6edf6e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 15:05:00 +0400 Subject: [PATCH 316/317] Add changelog for MFM support --- changelog.d/mfm-backend.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/mfm-backend.add diff --git a/changelog.d/mfm-backend.add b/changelog.d/mfm-backend.add new file mode 100644 index 000000000..f815c6828 --- /dev/null +++ b/changelog.d/mfm-backend.add @@ -0,0 +1 @@ +Add backend support for Misskey Markdown (MFM) posts From 47021b5aba4151284aefeeddbc8663ac80d5e19b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 16:37:59 +0400 Subject: [PATCH 317/317] Fix MFM validator alias ordering --- .../object_validators/article_note_page_validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ec9259f95..844ad2c8f 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 @@ -8,10 +8,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.HTML alias Pleroma.User - alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI.Utils import Ecto.Changeset