From c05cbc47f9e83a7ba41124475e48cf01ecbb2e56 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 22 Feb 2020 13:14:30 +0000 Subject: [PATCH 01/79] Better advice for vacuuming after restoring. --- docs/administration/backup.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index 692aa7368..be57bf74a 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -18,9 +18,8 @@ 6. Run `sudo -Hu postgres pg_restore -d -v -1 ` 7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. 8. Restart the Pleroma service. -9. After you've restarted Pleroma, you will notice that postgres will take up more cpu resources than usual. A lot in fact. To fix this you must do a VACUUM ANLAYZE. This can also be done while the instance is still running like so: - $ sudo -u postgres psql pleroma_database_name - pleroma=# VACUUM ANALYZE; +9. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries. + [^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. ## Remove From 0cf1d4fcd0c15594f663101061670a4555132840 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 22 Feb 2020 19:48:41 +0300 Subject: [PATCH 02/79] [#1560] Restricted AP- & OStatus-related routes for non-federating instances. --- lib/pleroma/plugs/static_fe_plug.ex | 5 +- .../activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/ostatus/ostatus_controller.ex | 2 + .../controllers/remote_follow_controller.ex | 2 + .../controllers/util_controller.ex | 2 + test/web/activity_pub/publisher_test.exs | 4 + test/web/feed/user_controller_test.exs | 145 +++++++++++------- .../static_fe/static_fe_controller_test.exs | 119 ++++---------- .../remote_follow_controller_test.exs | 6 + test/web/twitter_api/util_controller_test.exs | 37 +++-- 10 files changed, 166 insertions(+), 158 deletions(-) diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index b3fb3c582..7d69e661c 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -21,6 +21,9 @@ def call(conn, _) do defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) defp accepts_html?(conn) do - conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html") + conn + |> get_req_header("accept") + |> List.first() + |> String.contains?("text/html") end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5059e3984..aee574262 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when action in [:activity, :object] ) - plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay]) + plug(Pleroma.Web.FederatingPlug) plug(:set_requester_reachable when action in [:inbox]) plug(:relay_active? when action in [:relay]) diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 01ec7941e..630cd0006 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -16,6 +16,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Router + plug(Pleroma.Web.FederatingPlug) + plug( RateLimiter, [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index fbf31c7eb..89da760da 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -16,6 +16,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do @status_types ["Article", "Event", "Note", "Video", "Page", "Question"] + plug(Pleroma.Web.FederatingPlug) + # Note: follower can submit the form (with password auth) not being signed in (having no token) plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index f08b9d28c..0a77978e3 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger + plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) + plug( OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs index 015af19ab..c8eed68b6 100644 --- a/test/web/activity_pub/publisher_test.exs +++ b/test/web/activity_pub/publisher_test.exs @@ -23,6 +23,10 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do :ok end + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) + end + describe "gather_webfinger_links/1" do test "it returns links" do user = insert(:user) diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs index 41cc9e07e..fceb2ed43 100644 --- a/test/web/feed/user_controller_test.exs +++ b/test/web/feed/user_controller_test.exs @@ -8,66 +8,78 @@ defmodule Pleroma.Web.Feed.UserControllerTest do import Pleroma.Factory import SweetXml + alias Pleroma.Config alias Pleroma.Object alias Pleroma.User - clear_config([:feed]) - - test "gets a feed", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 10, omission: "..."} - ) - - activity = insert(:note_activity) - - note = - insert(:note, - data: %{ - "content" => "This is :moominmamma: note ", - "attachment" => [ - %{ - "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"}] - } - ], - "inReplyTo" => activity.data["id"] - } - ) - - note_activity = insert(:note_activity, note: note) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - note2 = - insert(:note, - user: user, - data: %{"content" => "42 This is :moominmamma: note ", "inReplyTo" => activity.data["id"]} - ) - - _note_activity2 = insert(:note_activity, note: note2) - object = Object.normalize(note_activity) - - resp = - conn - |> put_req_header("content-type", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//entry/title/text()"l) - - assert activity_titles == ['42 This...', 'This is...'] - assert resp =~ object.data["content"] + clear_config_all([:instance, :federating]) do + Config.put([:instance, :federating], true) end - test "returns 404 for a missing feed", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/atom+xml") - |> get(user_feed_path(conn, :feed, "nonexisting")) + describe "feed" do + clear_config([:feed]) - assert response(conn, 404) + test "gets a feed", %{conn: conn} do + Config.put( + [:feed, :post_title], + %{max_length: 10, omission: "..."} + ) + + activity = insert(:note_activity) + + note = + insert(:note, + data: %{ + "content" => "This is :moominmamma: note ", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} + ] + } + ], + "inReplyTo" => activity.data["id"] + } + ) + + note_activity = insert(:note_activity, note: note) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + note2 = + insert(:note, + user: user, + data: %{ + "content" => "42 This is :moominmamma: note ", + "inReplyTo" => activity.data["id"] + } + ) + + _note_activity2 = insert(:note_activity, note: note2) + object = Object.normalize(note_activity) + + resp = + conn + |> put_req_header("content-type", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//entry/title/text()"l) + + assert activity_titles == ['42 This...', 'This is...'] + assert resp =~ object.data["content"] + end + + test "returns 404 for a missing feed", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/atom+xml") + |> get(user_feed_path(conn, :feed, "nonexisting")) + + assert response(conn, 404) + end end describe "feed_redirect" do @@ -248,4 +260,29 @@ test "html format. it returns error when user not found", %{conn: conn} do assert response == %{"error" => "Not found"} end end + + describe "feed_redirect (depending on federation enabled state)" do + setup %{conn: conn} do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/json") + + %{conn: conn, user: user} + end + + clear_config([:instance, :federating]) + + test "renders if instance is federating", %{conn: conn, user: user} do + Config.put([:instance, :federating], true) + + conn = get(conn, "/users/#{user.nickname}") + assert json_response(conn, 200) + end + + test "renders 404 if instance is NOT federating", %{conn: conn, user: user} do + Config.put([:instance, :federating], false) + + conn = get(conn, "/users/#{user.nickname}") + assert json_response(conn, 404) + end + end end diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index 2ce8f9fa3..11facab99 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -1,56 +1,42 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.CommonAPI import Pleroma.Factory clear_config_all([:static_fe, :enabled]) do - Pleroma.Config.put([:static_fe, :enabled], true) + Config.put([:static_fe, :enabled], true) end - describe "user profile page" do - test "just the profile as HTML", %{conn: conn} do - user = insert(:user) + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + user = insert(:user) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/users/#{user.nickname}") + %{conn: conn, user: user} + end + + describe "user profile html" do + test "just the profile as HTML", %{conn: conn, user: user} do + conn = get(conn, "/users/#{user.nickname}") assert html_response(conn, 200) =~ user.nickname end - test "renders json unless there's an html accept header", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/#{user.nickname}") - - assert json_response(conn, 200) - end - test "404 when user not found", %{conn: conn} do - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/users/limpopo") + conn = get(conn, "/users/limpopo") assert html_response(conn, 404) =~ "not found" end - test "profile does not include private messages", %{conn: conn} do - user = insert(:user) + test "profile does not include private messages", %{conn: conn, user: user} do CommonAPI.post(user, %{"status" => "public"}) CommonAPI.post(user, %{"status" => "private", "visibility" => "private"}) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/users/#{user.nickname}") + conn = get(conn, "/users/#{user.nickname}") html = html_response(conn, 200) @@ -58,14 +44,10 @@ test "profile does not include private messages", %{conn: conn} do refute html =~ ">private<" end - test "pagination", %{conn: conn} do - user = insert(:user) + test "pagination", %{conn: conn, user: user} do Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/users/#{user.nickname}") + conn = get(conn, "/users/#{user.nickname}") html = html_response(conn, 200) @@ -75,15 +57,11 @@ test "pagination", %{conn: conn} do refute html =~ ">test1<" end - test "pagination, page 2", %{conn: conn} do - user = insert(:user) + test "pagination, page 2", %{conn: conn, user: user} do activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end) {:ok, a11} = Enum.at(activities, 11) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/users/#{user.nickname}?max_id=#{a11.id}") + conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}") html = html_response(conn, 200) @@ -94,15 +72,11 @@ test "pagination, page 2", %{conn: conn} do end end - describe "notice rendering" do - test "single notice page", %{conn: conn} do - user = insert(:user) + describe "notice html" do + test "single notice page", %{conn: conn, user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"}) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{activity.id}") + conn = get(conn, "/notice/#{activity.id}") html = html_response(conn, 200) assert html =~ "
" @@ -110,8 +84,7 @@ test "single notice page", %{conn: conn} do assert html =~ "testing a thing!" end - test "shows the whole thread", %{conn: conn} do - user = insert(:user) + test "shows the whole thread", %{conn: conn, user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "space: the final frontier"}) CommonAPI.post(user, %{ @@ -119,70 +92,47 @@ test "shows the whole thread", %{conn: conn} do "in_reply_to_status_id" => activity.id }) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{activity.id}") + conn = get(conn, "/notice/#{activity.id}") html = html_response(conn, 200) assert html =~ "the final frontier" assert html =~ "voyages" end - test "redirect by AP object ID", %{conn: conn} do - user = insert(:user) - + test "redirect by AP object ID", %{conn: conn, user: user} do {:ok, %Activity{data: %{"object" => object_url}}} = CommonAPI.post(user, %{"status" => "beam me up"}) - conn = - conn - |> put_req_header("accept", "text/html") - |> get(URI.parse(object_url).path) + conn = get(conn, URI.parse(object_url).path) assert html_response(conn, 302) =~ "redirected" end - test "redirect by activity ID", %{conn: conn} do - user = insert(:user) - + test "redirect by activity ID", %{conn: conn, user: user} do {:ok, %Activity{data: %{"id" => id}}} = CommonAPI.post(user, %{"status" => "I'm a doctor, not a devops!"}) - conn = - conn - |> put_req_header("accept", "text/html") - |> get(URI.parse(id).path) + conn = get(conn, URI.parse(id).path) assert html_response(conn, 302) =~ "redirected" end test "404 when notice not found", %{conn: conn} do - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/88c9c317") + conn = get(conn, "/notice/88c9c317") assert html_response(conn, 404) =~ "not found" end - test "404 for private status", %{conn: conn} do - user = insert(:user) - + test "404 for private status", %{conn: conn, user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"}) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{activity.id}") + conn = get(conn, "/notice/#{activity.id}") assert html_response(conn, 404) =~ "not found" end - test "302 for remote cached status", %{conn: conn} do - user = insert(:user) - + test "302 for remote cached status", %{conn: conn, user: user} do message = %{ "@context" => "https://www.w3.org/ns/activitystreams", "to" => user.follower_address, @@ -199,10 +149,7 @@ test "302 for remote cached status", %{conn: conn} do assert {:ok, activity} = Transmogrifier.handle_incoming(message) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{activity.id}") + conn = get(conn, "/notice/#{activity.id}") assert html_response(conn, 302) =~ "redirected" end diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs index 80a42989d..73062f18f 100644 --- a/test/web/twitter_api/remote_follow_controller_test.exs +++ b/test/web/twitter_api/remote_follow_controller_test.exs @@ -5,8 +5,10 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.CommonAPI + import ExUnit.CaptureLog import Pleroma.Factory @@ -15,6 +17,10 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do :ok end + clear_config_all([:instance, :federating]) do + Config.put([:instance, :federating], true) + end + clear_config([:instance]) clear_config([:frontend_configurations, :pleroma_fe]) clear_config([:user, :deny_follow_blocked]) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 56633ffce..992cc44a5 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do use Pleroma.Web.ConnCase use Oban.Testing, repo: Pleroma.Repo + alias Pleroma.Config alias Pleroma.Tests.ObanHelpers alias Pleroma.User @@ -178,7 +179,7 @@ test "it updates notification privacy option", %{user: user, conn: conn} do describe "GET /api/statusnet/config" do test "it returns config in xml format", %{conn: conn} do - instance = Pleroma.Config.get(:instance) + instance = Config.get(:instance) response = conn @@ -195,12 +196,12 @@ test "it returns config in xml format", %{conn: conn} do end test "it returns config in json format", %{conn: conn} do - instance = Pleroma.Config.get(:instance) - Pleroma.Config.put([:instance, :managed_config], true) - Pleroma.Config.put([:instance, :registrations_open], false) - Pleroma.Config.put([:instance, :invites_enabled], true) - Pleroma.Config.put([:instance, :public], false) - Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) + instance = Config.get(:instance) + Config.put([:instance, :managed_config], true) + Config.put([:instance, :registrations_open], false) + Config.put([:instance, :invites_enabled], true) + Config.put([:instance, :public], false) + Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) response = conn @@ -234,7 +235,7 @@ test "it returns config in json format", %{conn: conn} do end test "returns the state of safe_dm_mentions flag", %{conn: conn} do - Pleroma.Config.put([:instance, :safe_dm_mentions], true) + Config.put([:instance, :safe_dm_mentions], true) response = conn @@ -243,7 +244,7 @@ test "returns the state of safe_dm_mentions flag", %{conn: conn} do assert response["site"]["safeDMMentionsEnabled"] == "1" - Pleroma.Config.put([:instance, :safe_dm_mentions], false) + Config.put([:instance, :safe_dm_mentions], false) response = conn @@ -254,8 +255,8 @@ test "returns the state of safe_dm_mentions flag", %{conn: conn} do end test "it returns the managed config", %{conn: conn} do - Pleroma.Config.put([:instance, :managed_config], false) - Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) + Config.put([:instance, :managed_config], false) + Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) response = conn @@ -264,7 +265,7 @@ test "it returns the managed config", %{conn: conn} do refute response["site"]["pleromafe"] - Pleroma.Config.put([:instance, :managed_config], true) + Config.put([:instance, :managed_config], true) response = conn @@ -287,7 +288,7 @@ test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} d } ] - Pleroma.Config.put(:frontend_configurations, config) + Config.put(:frontend_configurations, config) response = conn @@ -320,7 +321,7 @@ test "returns json with custom emoji with tags", %{conn: conn} do clear_config([:instance, :healthcheck]) test "returns 503 when healthcheck disabled", %{conn: conn} do - Pleroma.Config.put([:instance, :healthcheck], false) + Config.put([:instance, :healthcheck], false) response = conn @@ -331,7 +332,7 @@ test "returns 503 when healthcheck disabled", %{conn: conn} do end test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do - Pleroma.Config.put([:instance, :healthcheck], true) + Config.put([:instance, :healthcheck], true) with_mock Pleroma.Healthcheck, system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do @@ -351,7 +352,7 @@ test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do end test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do - Pleroma.Config.put([:instance, :healthcheck], true) + Config.put([:instance, :healthcheck], true) with_mock Pleroma.Healthcheck, system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do @@ -426,6 +427,10 @@ test "it returns version in json format", %{conn: conn} do end describe "POST /main/ostatus - remote_subscribe/2" do + clear_config([:instance, :federating]) do + Config.put([:instance, :federating], true) + end + test "renders subscribe form", %{conn: conn} do user = insert(:user) From f446744a6a72d707504c2ba20ea2326f956b5097 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 26 Feb 2020 20:13:53 +0400 Subject: [PATCH 03/79] Allow account registration without an email --- CHANGELOG.md | 1 + lib/pleroma/user.ex | 11 +++- .../controllers/account_controller.ex | 16 ++++-- test/user_test.exs | 21 +++++++- .../controllers/account_controller_test.exs | 52 ++++++++++++++++++- 5 files changed, 92 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08bb7e1c7..a924d4083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled) - Logger: default log level changed from `warn` to `info`. - Config mix task `migrate_to_db` truncates `config` table before migrating the config file. +- Allow account registration without an email
API Changes diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 56e599ecc..5271d8dbe 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User do @@ -530,7 +530,14 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do end def maybe_validate_required_email(changeset, true), do: changeset - def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email]) + + def maybe_validate_required_email(changeset, _) do + if Pleroma.Config.get([:instance, :account_activation_required]) do + validate_required(changeset, [:email]) + else + changeset + end + end defp put_ap_id(changeset) do ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)}) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 38d14256f..88c997b9f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.AccountController do @@ -76,7 +76,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do @doc "POST /api/v1/accounts" def create( %{assigns: %{app: app}} = conn, - %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params + %{"username" => nickname, "password" => _, "agreement" => true} = params ) do params = params @@ -93,7 +93,8 @@ def create( |> Map.put("bio", params["bio"] || "") |> Map.put("confirm", params["password"]) - with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), + with :ok <- validate_email_param(params), + {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do json(conn, %{ token_type: "Bearer", @@ -114,6 +115,15 @@ def create(conn, _) do render_error(conn, :forbidden, "Invalid credentials") end + defp validate_email_param(%{"email" => _}), do: :ok + + defp validate_email_param(_) do + case Pleroma.Config.get([:instance, :account_activation_required]) do + true -> {:error, %{"error" => "Missing parameters"}} + _ -> :ok + end + end + @doc "GET /api/v1/accounts/verify_credentials" def verify_credentials(%{assigns: %{user: user}} = conn, _) do chat_token = Phoenix.Token.sign(conn, "user socket", user.id) diff --git a/test/user_test.exs b/test/user_test.exs index 2fc42a90d..b07fed42b 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.UserTest do @@ -412,7 +412,11 @@ test "it sends a welcome message if it is set" do assert activity.actor == welcome_user.ap_id end - test "it requires an email, name, nickname and password, bio is optional" do + clear_config([:instance, :account_activation_required]) + + test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do + Pleroma.Config.put([:instance, :account_activation_required], true) + @full_user_data |> Map.keys() |> Enum.each(fn key -> @@ -423,6 +427,19 @@ test "it requires an email, name, nickname and password, bio is optional" do end) end + test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do + Pleroma.Config.put([:instance, :account_activation_required], false) + + @full_user_data + |> Map.keys() + |> Enum.each(fn key -> + params = Map.delete(@full_user_data, key) + changeset = User.register_changeset(%User{}, params) + + assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid? + end) + end + test "it restricts certain nicknames" do [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 8625bb9cf..ff7cb88d1 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do @@ -601,6 +601,8 @@ test "blocking / unblocking a user" do [valid_params: valid_params] end + clear_config([:instance, :account_activation_required]) + test "Account registration via Application", %{conn: conn} do conn = post(conn, "/api/v1/apps", %{ @@ -731,7 +733,7 @@ test "returns bad_request if missing required params", %{ assert json_response(res, 200) [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] - |> Stream.zip(valid_params) + |> Stream.zip(Map.delete(valid_params, :email)) |> Enum.each(fn {ip, {attr, _}} -> res = conn @@ -743,6 +745,52 @@ test "returns bad_request if missing required params", %{ end) end + test "returns bad_request if missing email params when :account_activation_required is enabled", + %{conn: conn, valid_params: valid_params} do + Pleroma.Config.put([:instance, :account_activation_required], true) + + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 5}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response(res, 400) == %{"error" => "Missing parameters"} + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 6}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response(res, 400) == %{"error" => "{\"email\":[\"can't be blank\"]}"} + end + + test "allow registration without an email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 7}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response(res, 200) + end + + test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 8}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response(res, 200) + end + test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do conn = put_req_header(conn, "authorization", "Bearer " <> "invalid-token") From 3ef2ff3e479e69653537e6bbcc92a29590cab971 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 29 Feb 2020 01:23:36 +0100 Subject: [PATCH 04/79] auth_controller.ex: Add admin scope to MastoFE Related: https://git.pleroma.social/pleroma/pleroma/issues/1265 --- lib/pleroma/web/mastodon_api/controllers/auth_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index d9e51de7f..b63d96784 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -86,6 +86,6 @@ defp local_mastodon_root_path(conn) do @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} defp get_or_make_app do %{client_name: @local_mastodon_name, redirect_uris: "."} - |> App.get_or_make(["read", "write", "follow", "push"]) + |> App.get_or_make(["read", "write", "follow", "push", "admin"]) end end From 523f73dccd4e8f4028488e37f7333732db1eebd7 Mon Sep 17 00:00:00 2001 From: Phil Hagelberg Date: Sat, 29 Feb 2020 18:53:49 -0800 Subject: [PATCH 05/79] Fix static FE plug to handle missing Accept header. --- lib/pleroma/plugs/static_fe_plug.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index b3fb3c582..a8b22c243 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -21,6 +21,9 @@ def call(conn, _) do defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) defp accepts_html?(conn) do - conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html") + case get_req_header(conn, "accept") do + [accept | _] -> String.contains?(accept, "text/html") + _ -> false + end end end From deb5f5c40e638472ca5a01370ad127fc4c437150 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 2 Mar 2020 04:01:37 +0100 Subject: [PATCH 06/79] pleroma_api.md: direct_conversation_id vs. conversation_id Related: https://git.pleroma.social/pleroma/pleroma/issues/1594 --- docs/API/pleroma_api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 761d5c69c..12e63ef9f 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -288,10 +288,11 @@ Pleroma Conversations have the same general structure that Mastodon Conversation 2. Pleroma Conversations statuses can be requested by Conversation id. 3. Pleroma Conversations can be replied to. -Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation. +Conversations have the additional field `recipients` under the `pleroma` key. This holds a list of all the accounts that will receive a message in this conversation. The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation. +⚠ Conversation IDs can be found in direct messages with the `pleroma.direct_conversation_id` key, do not confuse it with `pleroma.conversation_id`. ## `GET /api/v1/pleroma/conversations/:id/statuses` ### Timeline for a given conversation From cc98d010edc444e260c81ac9f264a27d9afd5daf Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Feb 2020 16:21:48 +0300 Subject: [PATCH 07/79] relay list shows hosts without accepted follow --- lib/mix/tasks/pleroma/relay.ex | 2 +- lib/pleroma/activity.ex | 7 ++++ lib/pleroma/web/activity_pub/relay.ex | 19 ++++++++-- test/fixtures/relay/accept-follow.json | 15 ++++++++ test/fixtures/relay/relay.json | 20 ++++++++++ test/support/http_request_mock.ex | 8 ++++ test/tasks/relay_test.exs | 3 ++ .../activity_pub_controller_test.exs | 38 +++++++++++++++++++ 8 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/relay/accept-follow.json create mode 100644 test/fixtures/relay/relay.json diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index 7ef5f9678..b0fadeae9 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -35,7 +35,7 @@ def run(["unfollow", target]) do def run(["list"]) do start_pleroma() - with {:ok, list} <- Relay.list() do + with {:ok, list} <- Relay.list(true) do list |> Enum.each(&shell_info(&1)) else {:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 397eb6e3f..6ca05f74e 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -308,6 +308,13 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do |> where([a], fragment("? ->> 'state' = 'pending'", a.data)) end + def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do + Queries.by_type("Follow") + |> where([a], fragment("?->>'state' = 'pending'", a.data)) + |> where([a], a.actor == ^ap_id) + |> Repo.all() + end + def restrict_deactivated_users(query) do deactivated_users = from(u in User.Query.build(%{deactivated: true}), select: u.ap_id) diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index bb5542c89..729c23af7 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -60,15 +60,28 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do def publish(_), do: {:error, "Not implemented"} - @spec list() :: {:ok, [String.t()]} | {:error, any()} - def list do + @spec list(boolean()) :: {:ok, [String.t()]} | {:error, any()} + def list(with_not_accepted \\ false) do with %User{} = user <- get_actor() do - list = + accepted = user |> User.following() |> Enum.map(fn entry -> URI.parse(entry).host end) |> Enum.uniq() + list = + if with_not_accepted do + without_accept = + user + |> Pleroma.Activity.following_requests_for_actor() + |> Enum.map(fn a -> URI.parse(a.data["object"]).host <> " (no Accept received)" end) + |> Enum.uniq() + + accepted ++ without_accept + else + accepted + end + {:ok, list} else error -> format_error(error) diff --git a/test/fixtures/relay/accept-follow.json b/test/fixtures/relay/accept-follow.json new file mode 100644 index 000000000..1b166f2da --- /dev/null +++ b/test/fixtures/relay/accept-follow.json @@ -0,0 +1,15 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://relay.mastodon.host/actor", + "id": "https://relay.mastodon.host/activities/ec477b69-db26-4019-923e-cf809de516ab", + "object": { + "actor": "{{ap_id}}", + "id": "{{activity_id}}", + "object": "https://relay.mastodon.host/actor", + "type": "Follow" + }, + "to": [ + "{{ap_id}}" + ], + "type": "Accept" +} \ No newline at end of file diff --git a/test/fixtures/relay/relay.json b/test/fixtures/relay/relay.json new file mode 100644 index 000000000..77ae7f06c --- /dev/null +++ b/test/fixtures/relay/relay.json @@ -0,0 +1,20 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "endpoints": { + "sharedInbox": "https://relay.mastodon.host/inbox" + }, + "followers": "https://relay.mastodon.host/followers", + "following": "https://relay.mastodon.host/following", + "inbox": "https://relay.mastodon.host/inbox", + "name": "ActivityRelay", + "type": "Application", + "id": "https://relay.mastodon.host/actor", + "publicKey": { + "id": "https://relay.mastodon.host/actor#main-key", + "owner": "https://relay.mastodon.host/actor", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuNYHNYETdsZFsdcTTEQo\nlsTP9yz4ZjOGrQ1EjoBA7NkjBUxxUAPxZbBjWPT9F+L3IbCX1IwI2OrBM/KwDlug\nV41xnjNmxSCUNpxX5IMZtFaAz9/hWu6xkRTs9Bh6XWZxi+db905aOqszb9Mo3H2g\nQJiAYemXwTh2kBO7XlBDbsMhO11Tu8FxcWTMdR54vlGv4RoiVh8dJRa06yyiTs+m\njbj/OJwR06mHHwlKYTVT/587NUb+e9QtCK6t/dqpyZ1o7vKSK5PSldZVjwHt292E\nXVxFOQVXi7JazTwpdPww79ECSe8ThCykOYCNkm3RjsKuLuokp7Vzq1hXIoeBJ7z2\ndU8vbgg/JyazsOsTxkVs2nd2i9/QW2SH+sX9X3357+XLSCh/A8p8fv/GeoN7UCXe\n4DWHFJZDlItNFfymiPbQH+omuju8qrfW9ngk1gFeI2mahXFQVu7x0qsaZYioCIrZ\nwq0zPnUGl9u0tLUXQz+ZkInRrEz+JepDVauy5/3QdzMLG420zCj/ygDrFzpBQIrc\n62Z6URueUBJox0UK71K+usxqOrepgw8haFGMvg3STFo34pNYjoK4oKO+h5qZEDFD\nb1n57t6JWUaBocZbJns9RGASq5gih+iMk2+zPLWp1x64yvuLsYVLPLBHxjCxS6lA\ndWcopZHi7R/OsRz+vTT7420CAwEAAQ==\n-----END PUBLIC KEY-----" + }, + "summary": "ActivityRelay bot", + "preferredUsername": "relay", + "url": "https://relay.mastodon.host/actor" +} \ No newline at end of file diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index d46887865..e72638814 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1277,6 +1277,10 @@ def get("http://example.com/rel_me/error", _, _, _) do {:ok, %Tesla.Env{status: 404, body: ""}} end + def get("https://relay.mastodon.host/actor", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ @@ -1289,6 +1293,10 @@ def get(url, query, body, headers) do def post(url, query \\ [], body \\ [], headers \\ []) + def post("https://relay.mastodon.host/inbox", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: ""}} + end + def post("http://example.org/needs_refresh", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 04a1e45d7..43565b7c7 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -38,6 +38,9 @@ test "relay is followed" do assert activity.data["type"] == "Follow" assert activity.data["actor"] == local_user.ap_id assert activity.data["object"] == target_user.ap_id + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + assert_receive {:mix_shell, :info, ["mastodon.example.org (no Accept received)"]} end end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index ba2ce1dd9..0c80e2434 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -341,6 +341,44 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do assert "ok" == json_response(conn, 200) assert Instances.reachable?(sender_url) end + + test "accept follow activity", %{conn: conn} do + Pleroma.Config.put([:instance, :federating], true) + relay = Relay.get_actor() + + assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor") + + followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor") + relay = refresh_record(relay) + + accept = + File.read!("test/fixtures/relay/accept-follow.json") + |> String.replace("{{ap_id}}", relay.ap_id) + |> String.replace("{{activity_id}}", activity.data["id"]) + + assert "ok" == + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", accept) + |> json_response(200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + assert Pleroma.FollowingRelationship.following?( + relay, + followed_relay + ) + + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + assert_receive {:mix_shell, :info, ["relay.mastodon.host"]} + end end describe "/users/:nickname/inbox" do From b4367125e9afc92ac27ff12552775f8e765140f1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 2 Mar 2020 21:43:18 +0300 Subject: [PATCH 08/79] [#1560] Added tests for non-federating instance bahaviour to ActivityPubControllerTest. --- docs/API/differences_in_mastoapi_responses.md | 2 +- docs/clients.md | 2 +- test/plugs/oauth_plug_test.exs | 2 +- .../activity_pub_controller_test.exs | 91 ++++++++++++++++++- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 06de90f71..476a4a2bf 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -180,7 +180,7 @@ Post here request with grant_type=refresh_token to obtain new access token. Retu ## Account Registration `POST /api/v1/accounts` -Has theses additionnal parameters (which are the same as in Pleroma-API): +Has theses additional parameters (which are the same as in Pleroma-API): * `fullname`: optional * `bio`: optional * `captcha_solution`: optional, contains provider-specific captcha solution, diff --git a/docs/clients.md b/docs/clients.md index 8ac9ad3de..1eae0f0c6 100644 --- a/docs/clients.md +++ b/docs/clients.md @@ -1,5 +1,5 @@ # Pleroma Clients -Note: Additionnal clients may be working but theses are officially supporting Pleroma. +Note: Additional clients may be working but theses are officially supporting Pleroma. Feel free to contact us to be added to this list! ## Desktop diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs index dea11cdb0..0eef27c1f 100644 --- a/test/plugs/oauth_plug_test.exs +++ b/test/plugs/oauth_plug_test.exs @@ -38,7 +38,7 @@ test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do assert conn.assigns[:user] == opts[:user] end - test "with valid token(downcase) in url parameters, it assings the user", opts do + test "with valid token(downcase) in url parameters, it assigns the user", opts do conn = :get |> build_conn("/?access_token=#{opts[:token]}") diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index ba2ce1dd9..af0417406 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -25,9 +25,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do :ok end - clear_config_all([:instance, :federating], - do: Pleroma.Config.put([:instance, :federating], true) - ) + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) + end describe "/relay" do clear_config([:instance, :allow_relay]) @@ -1008,7 +1008,7 @@ test "it tracks a signed activity fetch when the json is cached", %{conn: conn} end end - describe "Additionnal ActivityPub C2S endpoints" do + describe "Additional ActivityPub C2S endpoints" do test "/api/ap/whoami", %{conn: conn} do user = insert(:user) @@ -1047,4 +1047,87 @@ test "uploadMedia", %{conn: conn} do assert object["actor"] == user.ap_id end end + + describe "when instance is not federating," do + clear_config([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], false) + end + + test "returns 404 for GET routes", %{conn: conn} do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/json") + + get_uris = [ + "/users/#{user.nickname}", + "/users/#{user.nickname}/outbox", + "/users/#{user.nickname}/inbox?page=true", + "/users/#{user.nickname}/followers", + "/users/#{user.nickname}/following", + "/internal/fetch", + "/relay", + "/relay/following", + "/relay/followers", + "/api/ap/whoami" + ] + + for get_uri <- get_uris do + conn + |> get(get_uri) + |> json_response(404) + + conn + |> assign(:user, user) + |> get(get_uri) + |> json_response(404) + end + end + + test "returns 404 for activity-related POST routes", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + + post_activity_data = + "test/fixtures/mastodon-post-activity.json" + |> File.read!() + |> Poison.decode!() + + post_activity_uris = [ + "/inbox", + "/relay/inbox", + "/users/#{user.nickname}/inbox", + "/users/#{user.nickname}/outbox" + ] + + for post_activity_uri <- post_activity_uris do + conn + |> post(post_activity_uri, post_activity_data) + |> json_response(404) + + conn + |> assign(:user, user) + |> post(post_activity_uri, post_activity_data) + |> json_response(404) + end + end + + test "returns 404 for media upload attempt", %{conn: conn} do + user = insert(:user) + desc = "Description of the image" + + image = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + conn + |> assign(:user, user) + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> json_response(404) + end + end end From bd8624d649643c5a14bb24d8b2f2aed0454fb50d Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 2 Mar 2020 22:02:21 +0300 Subject: [PATCH 09/79] [#1560] Added tests for non-federating instance bahaviour to OStatusControllerTest. --- test/web/ostatus/ostatus_controller_test.exs | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 50235dfef..2b7bc662d 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -277,4 +277,33 @@ test "404s when attachment isn't audio or video", %{conn: conn} do |> response(404) end end + + describe "when instance is not federating," do + clear_config([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], false) + end + + test "returns 404 for GET routes", %{conn: conn} do + conn = put_req_header(conn, "accept", "application/json") + + note_activity = insert(:note_activity, local: true) + [_, activity_uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + + object = Object.normalize(note_activity) + [_, object_uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + + get_uris = [ + "/activities/#{activity_uuid}", + "/objects/#{object_uuid}", + "/notice/#{note_activity.id}", + "/notice/#{note_activity.id}/embed_player" + ] + + for get_uri <- get_uris do + conn + |> get(get_uri) + |> json_response(404) + end + end + end end From b6fc98d9cd3a32b39606c65cb4f298d280e2537c Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 3 Mar 2020 22:22:02 +0300 Subject: [PATCH 10/79] [#1560] ActivityPubController federation state restrictions adjustments. Adjusted tests. --- lib/pleroma/plugs/federating_plug.ex | 4 +- .../activity_pub/activity_pub_controller.ex | 44 ++++++++++++++----- lib/pleroma/web/router.ex | 3 ++ .../activity_pub_controller_test.exs | 29 ++---------- 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index 4dc4e9279..4c5aca3e9 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -10,7 +10,7 @@ def init(options) do end def call(conn, _opts) do - if Pleroma.Config.get([:instance, :federating]) do + if federating?() do conn else conn @@ -20,4 +20,6 @@ def call(conn, _opts) do |> halt() end end + + def federating?, do: Pleroma.Config.get([:instance, :federating]) end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index aee574262..e1984f88f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -18,19 +18,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.FederatingPlug alias Pleroma.Web.Federator require Logger action_fallback(:errors) + # Note: some of the following actions (like :update_inbox) may be server-to-server as well + @client_to_server_actions [ + :whoami, + :read_inbox, + :update_outbox, + :upload_media, + :followers, + :following + ] + + plug(FederatingPlug when action not in @client_to_server_actions) + plug( Pleroma.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] when action in [:activity, :object] ) - plug(Pleroma.Web.FederatingPlug) plug(:set_requester_reachable when action in [:inbox]) plug(:relay_active? when action in [:relay]) @@ -255,8 +267,16 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do json(conn, "ok") end - # only accept relayed Creates - def inbox(conn, %{"type" => "Create"} = params) do + # POST /relay/inbox -or- POST /internal/fetch/inbox + def inbox(conn, params) do + if params["type"] == "Create" && FederatingPlug.federating?() do + post_inbox_relayed_create(conn, params) + else + post_inbox_fallback(conn, params) + end + end + + defp post_inbox_relayed_create(conn, params) do Logger.debug( "Signature missing or not from author, relayed Create message, fetching object from source" ) @@ -266,7 +286,7 @@ def inbox(conn, %{"type" => "Create"} = params) do json(conn, "ok") end - def inbox(conn, params) do + defp post_inbox_fallback(conn, params) do headers = Enum.into(conn.req_headers, %{}) if String.contains?(headers["signature"], params["actor"]) do @@ -314,7 +334,7 @@ def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do def whoami(_conn, _params), do: {:error, :not_found} def read_inbox( - %{assigns: %{user: %{nickname: nickname} = user}} = conn, + %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname, "page" => page?} = params ) when page? in [true, "true"] do @@ -337,7 +357,7 @@ def read_inbox( }) end - def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{ + def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{ "nickname" => nickname }) do with {:ok, user} <- User.ensure_keys_present(user) do @@ -356,7 +376,7 @@ def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do |> json(err) end - def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{ + def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ "nickname" => nickname }) do err = @@ -370,7 +390,7 @@ def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{ |> json(err) end - def handle_user_activity(user, %{"type" => "Create"} = params) do + def handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do object = params["object"] |> Map.merge(Map.take(params, ["to", "cc"])) @@ -386,7 +406,7 @@ def handle_user_activity(user, %{"type" => "Create"} = params) do }) end - def handle_user_activity(user, %{"type" => "Delete"} = params) do + def handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do with %Object{} = object <- Object.normalize(params["object"]), true <- user.is_moderator || user.ap_id == object.data["actor"], {:ok, delete} <- ActivityPub.delete(object) do @@ -396,7 +416,7 @@ def handle_user_activity(user, %{"type" => "Delete"} = params) do end end - def handle_user_activity(user, %{"type" => "Like"} = params) do + def handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do with %Object{} = object <- Object.normalize(params["object"]), {:ok, activity, _object} <- ActivityPub.like(user, object) do {:ok, activity} @@ -434,7 +454,7 @@ def update_outbox( end end - def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do + def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do err = dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", nickname: nickname, @@ -492,7 +512,7 @@ defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do - HTTP Code: 201 Created - HTTP Body: ActivityPub object to be inserted into another's `attachment` field """ - def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do with {:ok, object} <- ActivityPub.upload( file, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 980242c68..5f3a06caa 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -541,6 +541,7 @@ defmodule Pleroma.Web.Router do get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end + # Server to Server (S2S) AP interactions pipeline :activitypub do plug(:accepts, ["activity+json", "json"]) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -554,6 +555,7 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/outbox", ActivityPubController, :outbox) end + # Client to Server (C2S) AP interactions pipeline :activitypub_client do plug(:accepts, ["activity+json", "json"]) plug(:fetch_session) @@ -568,6 +570,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.EnsureUserKeyPlug) end + # Note: propagate _any_ updates to `@client_to_server_actions` in `ActivityPubController` scope "/", Pleroma.Web.ActivityPub do pipe_through([:activitypub_client]) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index af0417406..b853474d4 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -775,7 +775,7 @@ test "it returns the followers in a collection", %{conn: conn} do assert result["first"]["orderedItems"] == [user.ap_id] end - test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do + test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do user = insert(:user) user_two = insert(:user, hide_followers: true) User.follow(user, user_two) @@ -1060,14 +1060,8 @@ test "returns 404 for GET routes", %{conn: conn} do get_uris = [ "/users/#{user.nickname}", "/users/#{user.nickname}/outbox", - "/users/#{user.nickname}/inbox?page=true", - "/users/#{user.nickname}/followers", - "/users/#{user.nickname}/following", "/internal/fetch", - "/relay", - "/relay/following", - "/relay/followers", - "/api/ap/whoami" + "/relay" ] for get_uri <- get_uris do @@ -1098,8 +1092,7 @@ test "returns 404 for activity-related POST routes", %{conn: conn} do post_activity_uris = [ "/inbox", "/relay/inbox", - "/users/#{user.nickname}/inbox", - "/users/#{user.nickname}/outbox" + "/users/#{user.nickname}/inbox" ] for post_activity_uri <- post_activity_uris do @@ -1113,21 +1106,5 @@ test "returns 404 for activity-related POST routes", %{conn: conn} do |> json_response(404) end end - - test "returns 404 for media upload attempt", %{conn: conn} do - user = insert(:user) - desc = "Description of the image" - - image = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - conn - |> assign(:user, user) - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(404) - end end end From ad22e94f336875141a2e2db786b1f15f65402f3e Mon Sep 17 00:00:00 2001 From: eugenijm Date: Thu, 5 Mar 2020 15:01:45 +0300 Subject: [PATCH 11/79] Exclude private and direct statuses visible to the admin when using godmode --- lib/pleroma/web/admin_api/admin_api_controller.ex | 4 ++-- test/web/admin_api/admin_api_controller_test.exs | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de0755ee5..178627030 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -745,14 +745,14 @@ def report_notes_delete(%{assigns: %{user: user}} = conn, %{ end end - def list_statuses(%{assigns: %{user: admin}} = conn, params) do + def list_statuses(%{assigns: %{user: _admin}} = conn, params) do godmode = params["godmode"] == "true" || params["godmode"] == true local_only = params["local_only"] == "true" || params["local_only"] == true with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true {page, page_size} = page_params(params) activities = - ActivityPub.fetch_statuses(admin, %{ + ActivityPub.fetch_statuses(nil, %{ "godmode" => godmode, "local_only" => local_only, "limit" => page_size, diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 45b22ea24..5c7858c05 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -3066,7 +3066,7 @@ test "pleroma restarts", %{conn: conn} do end describe "GET /api/pleroma/admin/statuses" do - test "returns all public, unlisted, and direct statuses", %{conn: conn, admin: admin} do + test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do blocked = insert(:user) user = insert(:user) User.block(admin, blocked) @@ -3085,7 +3085,7 @@ test "returns all public, unlisted, and direct statuses", %{conn: conn, admin: a |> json_response(200) refute "private" in Enum.map(response, & &1["visibility"]) - assert length(response) == 4 + assert length(response) == 3 end test "returns only local statuses with local_only on", %{conn: conn} do @@ -3102,12 +3102,16 @@ test "returns only local statuses with local_only on", %{conn: conn} do assert length(response) == 1 end - test "returns private statuses with godmode on", %{conn: conn} do + test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do user = insert(:user) + + {:ok, _} = + CommonAPI.post(user, %{"status" => "@#{admin.nickname}", "visibility" => "direct"}) + {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"}) {:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) conn = get(conn, "/api/pleroma/admin/statuses?godmode=true") - assert json_response(conn, 200) |> length() == 2 + assert json_response(conn, 200) |> length() == 3 end end From 40765875d41f181b4ac54a772b4c61d6afc0bc34 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 5 Mar 2020 21:19:21 +0300 Subject: [PATCH 12/79] [#1560] Misc. improvements in ActivityPubController federation state restrictions. --- lib/pleroma/plugs/federating_plug.ex | 14 +++++++---- .../activity_pub/activity_pub_controller.ex | 25 +++++++++++++------ .../activity_pub_controller_test.exs | 9 ++++--- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index 4c5aca3e9..456c1bfb9 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -13,13 +13,17 @@ def call(conn, _opts) do if federating?() do conn else - conn - |> put_status(404) - |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) - |> Phoenix.Controller.render("404.json") - |> halt() + fail(conn) end end def federating?, do: Pleroma.Config.get([:instance, :federating]) + + def fail(conn) do + conn + |> put_status(404) + |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) + |> Phoenix.Controller.render("404.json") + |> halt() + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index e1984f88f..9beaaf8c9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do @client_to_server_actions [ :whoami, :read_inbox, + :outbox, :update_outbox, :upload_media, :followers, @@ -140,10 +141,14 @@ defp set_cache_ttl_for(conn, entity) do # GET /relay/following def following(%{assigns: %{relay: true}} = conn, _params) do - conn - |> put_resp_content_type("application/activity+json") - |> put_view(UserView) - |> render("following.json", %{user: Relay.get_actor()}) + if FederatingPlug.federating?() do + conn + |> put_resp_content_type("application/activity+json") + |> put_view(UserView) + |> render("following.json", %{user: Relay.get_actor()}) + else + FederatingPlug.fail(conn) + end end def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do @@ -177,10 +182,14 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d # GET /relay/followers def followers(%{assigns: %{relay: true}} = conn, _params) do - conn - |> put_resp_content_type("application/activity+json") - |> put_view(UserView) - |> render("followers.json", %{user: Relay.get_actor()}) + if FederatingPlug.federating?() do + conn + |> put_resp_content_type("application/activity+json") + |> put_view(UserView) + |> render("followers.json", %{user: Relay.get_actor()}) + else + FederatingPlug.fail(conn) + end end def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index b853474d4..9c922e991 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -577,7 +577,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do end end - describe "/users/:nickname/outbox" do + describe "GET /users/:nickname/outbox" do test "it will not bomb when there is no activity", %{conn: conn} do user = insert(:user) @@ -614,7 +614,9 @@ test "it returns an announce activity in a collection", %{conn: conn} do assert response(conn, 200) =~ announce_activity.data["object"] end + end + describe "POST /users/:nickname/outbox" do test "it rejects posts from other users", %{conn: conn} do data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!() user = insert(:user) @@ -1059,9 +1061,10 @@ test "returns 404 for GET routes", %{conn: conn} do get_uris = [ "/users/#{user.nickname}", - "/users/#{user.nickname}/outbox", "/internal/fetch", - "/relay" + "/relay", + "/relay/following", + "/relay/followers" ] for get_uri <- get_uris do From 86e7709a4d870cac151ed44f8bb0bd7fd054f15d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 8 Mar 2020 23:24:30 +0300 Subject: [PATCH 13/79] mix.exs: bump version to development one --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 89b56bc5d..bb86c38d0 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.0.0"), + version: version("2.0.50"), elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From 5fc92deef37dcc4db476520d89dd79e616356e63 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 9 Mar 2020 20:51:44 +0300 Subject: [PATCH 14/79] [#1560] Ensured authentication or enabled federation for federation-related routes. New tests + tests refactoring. --- .../plugs/ensure_authenticated_plug.ex | 19 +- lib/pleroma/plugs/federating_plug.ex | 2 +- .../activity_pub/activity_pub_controller.ex | 76 ++--- lib/pleroma/web/feed/user_controller.ex | 7 +- lib/pleroma/web/ostatus/ostatus_controller.ex | 10 +- lib/pleroma/web/router.ex | 5 +- test/plugs/ensure_authenticated_plug_test.exs | 66 +++- test/support/conn_case.ex | 19 ++ .../activity_pub_controller_test.exs | 308 ++++++++++++------ test/web/feed/user_controller_test.exs | 197 ++--------- .../media_proxy_controller_test.exs | 3 +- test/web/ostatus/ostatus_controller_test.exs | 110 +++---- 12 files changed, 418 insertions(+), 404 deletions(-) diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex index 6f9b840a9..054d2297f 100644 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -15,9 +15,24 @@ def call(%{assigns: %{user: %User{}}} = conn, _) do conn end - def call(conn, _) do + def call(conn, options) do + perform = + cond do + options[:if_func] -> options[:if_func].() + options[:unless_func] -> !options[:unless_func].() + true -> true + end + + if perform do + fail(conn) + else + conn + end + end + + def fail(conn) do conn |> render_error(:forbidden, "Invalid credentials.") - |> halt + |> halt() end end diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index c6d622ce4..7d947339f 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -19,7 +19,7 @@ def call(conn, _opts) do def federating?, do: Pleroma.Config.get([:instance, :federating]) - def fail(conn) do + defp fail(conn) do conn |> put_status(404) |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 525e61360..8b9eb4a2c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Delivery alias Pleroma.Object alias Pleroma.Object.Fetcher + alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.InternalFetchActor @@ -25,18 +26,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do action_fallback(:errors) - # Note: some of the following actions (like :update_inbox) may be server-to-server as well - @client_to_server_actions [ - :whoami, - :read_inbox, - :outbox, - :update_outbox, - :upload_media, - :followers, - :following - ] + @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers] - plug(FederatingPlug when action not in @client_to_server_actions) + plug(FederatingPlug when action in @federating_only_actions) + + plug( + EnsureAuthenticatedPlug, + [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions + ) + + plug( + EnsureAuthenticatedPlug + when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers] + ) plug( Pleroma.Plugs.Cache, @@ -47,7 +49,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do plug(:set_requester_reachable when action in [:inbox]) plug(:relay_active? when action in [:relay]) - def relay_active?(conn, _) do + defp relay_active?(conn, _) do if Pleroma.Config.get([:instance, :allow_relay]) do conn else @@ -140,14 +142,12 @@ defp set_cache_ttl_for(conn, entity) do end # GET /relay/following - def following(%{assigns: %{relay: true}} = conn, _params) do - if FederatingPlug.federating?() do + def relay_following(conn, _params) do + with %{halted: false} = conn <- FederatingPlug.call(conn, []) do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("following.json", %{user: Relay.get_actor()}) - else - FederatingPlug.fail(conn) end end @@ -181,14 +181,12 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d end # GET /relay/followers - def followers(%{assigns: %{relay: true}} = conn, _params) do - if FederatingPlug.federating?() do + def relay_followers(conn, _params) do + with %{halted: false} = conn <- FederatingPlug.call(conn, []) do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("followers.json", %{user: Relay.get_actor()}) - else - FederatingPlug.fail(conn) end end @@ -221,13 +219,16 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d end end - def outbox(conn, %{"nickname" => nickname, "page" => page?} = params) + def outbox( + %{assigns: %{user: for_user}} = conn, + %{"nickname" => nickname, "page" => page?} = params + ) when page? in [true, "true"] do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do activities = if params["max_id"] do - ActivityPub.fetch_user_activities(user, nil, %{ + ActivityPub.fetch_user_activities(user, for_user, %{ "max_id" => params["max_id"], # This is a hack because postgres generates inefficient queries when filtering by # 'Answer', poll votes will be hidden by the visibility filter in this case anyway @@ -235,7 +236,7 @@ def outbox(conn, %{"nickname" => nickname, "page" => page?} = params) "limit" => 10 }) else - ActivityPub.fetch_user_activities(user, nil, %{ + ActivityPub.fetch_user_activities(user, for_user, %{ "limit" => 10, "include_poll_votes" => true }) @@ -298,7 +299,8 @@ defp post_inbox_relayed_create(conn, params) do defp post_inbox_fallback(conn, params) do headers = Enum.into(conn.req_headers, %{}) - if String.contains?(headers["signature"], params["actor"]) do + if headers["signature"] && params["actor"] && + String.contains?(headers["signature"], params["actor"]) do Logger.debug( "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!" ) @@ -306,7 +308,9 @@ defp post_inbox_fallback(conn, params) do Logger.debug(inspect(conn.req_headers)) end - json(conn, dgettext("errors", "error")) + conn + |> put_status(:bad_request) + |> json(dgettext("errors", "error")) end defp represent_service_actor(%User{} = user, conn) do @@ -340,8 +344,6 @@ def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do |> render("user.json", %{user: user}) end - def whoami(_conn, _params), do: {:error, :not_found} - def read_inbox( %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname, "page" => page?} = params @@ -377,14 +379,6 @@ def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{ end end - def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do - err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname) - - conn - |> put_status(:forbidden) - |> json(err) - end - def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ "nickname" => nickname }) do @@ -399,7 +393,7 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ |> json(err) end - def handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do + defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do object = params["object"] |> Map.merge(Map.take(params, ["to", "cc"])) @@ -415,7 +409,7 @@ def handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do }) end - def handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do + defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do with %Object{} = object <- Object.normalize(params["object"]), true <- user.is_moderator || user.ap_id == object.data["actor"], {:ok, delete} <- ActivityPub.delete(object) do @@ -425,7 +419,7 @@ def handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do end end - def handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do + defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do with %Object{} = object <- Object.normalize(params["object"]), {:ok, activity, _object} <- ActivityPub.like(user, object) do {:ok, activity} @@ -434,7 +428,7 @@ def handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do end end - def handle_user_activity(_, _) do + defp handle_user_activity(_, _) do {:error, dgettext("errors", "Unhandled activity type")} end @@ -475,13 +469,13 @@ def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => ni |> json(err) end - def errors(conn, {:error, :not_found}) do + defp errors(conn, {:error, :not_found}) do conn |> put_status(:not_found) |> json(dgettext("errors", "Not found")) end - def errors(conn, _e) do + defp errors(conn, _e) do conn |> put_status(:internal_server_error) |> json(dgettext("errors", "error")) diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 59aabb549..9ba602d9f 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -25,7 +25,12 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do - ActivityPubController.call(conn, :user) + with %{halted: false} = conn <- + Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn, + unless_func: &Pleroma.Web.FederatingPlug.federating?/0 + ) do + ActivityPubController.call(conn, :user) + end end def feed_redirect(conn, %{"nickname" => nickname}) do diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index e3f42b5c4..6fd3cfce5 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -16,7 +16,9 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Router - plug(Pleroma.Web.FederatingPlug) + plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.FederatingPlug.federating?/0 + ) plug( RateLimiter, @@ -137,13 +139,13 @@ def notice_player(conn, %{"id" => id}) do end end - def errors(conn, {:error, :not_found}) do + defp errors(conn, {:error, :not_found}) do render_error(conn, :not_found, "Not found") end - def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) + defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) - def errors(conn, _) do + defp errors(conn, _) do render_error(conn, :internal_server_error, "Something went wrong") end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 5f3a06caa..e4e3ee704 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -570,7 +570,6 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.EnsureUserKeyPlug) end - # Note: propagate _any_ updates to `@client_to_server_actions` in `ActivityPubController` scope "/", Pleroma.Web.ActivityPub do pipe_through([:activitypub_client]) @@ -600,8 +599,8 @@ defmodule Pleroma.Web.Router do post("/inbox", ActivityPubController, :inbox) end - get("/following", ActivityPubController, :following, assigns: %{relay: true}) - get("/followers", ActivityPubController, :followers, assigns: %{relay: true}) + get("/following", ActivityPubController, :relay_following) + get("/followers", ActivityPubController, :relay_followers) end scope "/internal/fetch", Pleroma.Web.ActivityPub do diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs index 18be5edd0..7f3559b83 100644 --- a/test/plugs/ensure_authenticated_plug_test.exs +++ b/test/plugs/ensure_authenticated_plug_test.exs @@ -8,24 +8,62 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.User - test "it halts if no user is assigned", %{conn: conn} do - conn = - conn - |> EnsureAuthenticatedPlug.call(%{}) + describe "without :if_func / :unless_func options" do + test "it halts if user is NOT assigned", %{conn: conn} do + conn = EnsureAuthenticatedPlug.call(conn, %{}) - assert conn.status == 403 - assert conn.halted == true + assert conn.status == 403 + assert conn.halted == true + end + + test "it continues if a user is assigned", %{conn: conn} do + conn = assign(conn, :user, %User{}) + ret_conn = EnsureAuthenticatedPlug.call(conn, %{}) + + assert ret_conn == conn + end end - test "it continues if a user is assigned", %{conn: conn} do - conn = - conn - |> assign(:user, %User{}) + describe "with :if_func / :unless_func options" do + setup do + %{ + true_fn: fn -> true end, + false_fn: fn -> false end + } + end - ret_conn = - conn - |> EnsureAuthenticatedPlug.call(%{}) + test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do + conn = assign(conn, :user, %User{}) + assert EnsureAuthenticatedPlug.call(conn, if_func: true_fn) == conn + assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn + assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn + assert EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) == conn + end - assert ret_conn == conn + test "it continues if a user is NOT assigned but :if_func evaluates to `false`", + %{conn: conn, false_fn: false_fn} do + assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn + end + + test "it continues if a user is NOT assigned but :unless_func evaluates to `true`", + %{conn: conn, true_fn: true_fn} do + assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn + end + + test "it halts if a user is NOT assigned and :if_func evaluates to `true`", + %{conn: conn, true_fn: true_fn} do + conn = EnsureAuthenticatedPlug.call(conn, if_func: true_fn) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it halts if a user is NOT assigned and :unless_func evaluates to `false`", + %{conn: conn, false_fn: false_fn} do + conn = EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) + + assert conn.status == 403 + assert conn.halted == true + end end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 0f2e81f9e..d6595f971 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -48,6 +48,25 @@ defp oauth_access(scopes, opts \\ []) do %{user: user, token: token, conn: conn} end + + defp ensure_federating_or_authenticated(conn, url, user) do + Pleroma.Config.put([:instance, :federating], false) + + conn + |> get(url) + |> response(403) + + conn + |> assign(:user, user) + |> get(url) + |> response(200) + + Pleroma.Config.put([:instance, :federating], true) + + conn + |> get(url) + |> response(200) + end end end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 04800b7ea..a939d0beb 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do import Pleroma.Factory alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Delivery alias Pleroma.Instances alias Pleroma.Object @@ -25,8 +26,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do :ok end - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) + clear_config([:instance, :federating]) do + Config.put([:instance, :federating], true) end describe "/relay" do @@ -42,12 +43,21 @@ test "with the relay active, it returns the relay user", %{conn: conn} do end test "with the relay disabled, it returns 404", %{conn: conn} do - Pleroma.Config.put([:instance, :allow_relay], false) + Config.put([:instance, :allow_relay], false) conn |> get(activity_pub_path(conn, :relay)) |> json_response(404) - |> assert + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) end end @@ -60,6 +70,16 @@ test "it returns the internal fetch user", %{conn: conn} do assert res["id"] =~ "/fetch" end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get(activity_pub_path(conn, :internal_fetch)) + |> json_response(404) + end end describe "/users/:nickname" do @@ -123,9 +143,34 @@ test "it returns 404 for remote users", %{ assert json_response(conn, 404) end + + test "it returns error when user is not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/users/jimm") + |> json_response(404) + + assert response == "Not found" + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + + conn = + put_req_header( + conn, + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user) + end end - describe "/object/:uuid" do + describe "/objects/:uuid" do test "it returns a json representation of the object with accept application/json", %{ conn: conn } do @@ -236,6 +281,18 @@ test "cached purged after object deletion", %{conn: conn} do assert "Not found" == json_response(conn2, :not_found) end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user) + end end describe "/activities/:uuid" do @@ -307,6 +364,18 @@ test "cached purged after activity deletion", %{conn: conn} do assert "Not found" == json_response(conn2, :not_found) end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user) + end end describe "/inbox" do @@ -341,6 +410,34 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do assert "ok" == json_response(conn, 200) assert Instances.reachable?(sender_url) end + + test "without valid signature, " <> + "it only accepts Create activities and requires enabled federation", + %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() + + conn = put_req_header(conn, "content-type", "application/activity+json") + + Config.put([:instance, :federating], false) + + conn + |> post("/inbox", data) + |> json_response(403) + + conn + |> post("/inbox", non_create_data) + |> json_response(403) + + Config.put([:instance, :federating], true) + + ret_conn = post(conn, "/inbox", data) + assert "ok" == json_response(ret_conn, 200) + + conn + |> post("/inbox", non_create_data) + |> json_response(400) + end end describe "/users/:nickname/inbox" do @@ -479,22 +576,11 @@ test "it accepts messages from actors that are followed by the user", %{ test "it rejects reads from other users", %{conn: conn} do user = insert(:user) - otheruser = insert(:user) - - conn = - conn - |> assign(:user, otheruser) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/inbox") - - assert json_response(conn, 403) - end - - test "it doesn't crash without an authenticated user", %{conn: conn} do - user = insert(:user) + other_user = insert(:user) conn = conn + |> assign(:user, other_user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/inbox") @@ -575,14 +661,30 @@ test "it removes all follower collections but actor's", %{conn: conn} do refute recipient.follower_address in activity.data["cc"] refute recipient.follower_address in activity.data["to"] end + + test "it requires authentication", %{conn: conn} do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/activity+json") + + ret_conn = get(conn, "/users/#{user.nickname}/inbox") + assert json_response(ret_conn, 403) + + ret_conn = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/inbox") + + assert json_response(ret_conn, 200) + end end describe "GET /users/:nickname/outbox" do - test "it will not bomb when there is no activity", %{conn: conn} do + test "it returns 200 even if there're no activities", %{conn: conn} do user = insert(:user) conn = conn + |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox") @@ -597,6 +699,7 @@ test "it returns a note activity in a collection", %{conn: conn} do conn = conn + |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox?page=true") @@ -609,26 +712,38 @@ test "it returns an announce activity in a collection", %{conn: conn} do conn = conn + |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox?page=true") assert response(conn, 200) =~ announce_activity.data["object"] end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user) + end end describe "POST /users/:nickname/outbox" do - test "it rejects posts from other users", %{conn: conn} do + test "it rejects posts from other users / unauuthenticated users", %{conn: conn} do data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!() user = insert(:user) - otheruser = insert(:user) + other_user = insert(:user) + conn = put_req_header(conn, "content-type", "application/activity+json") - conn = - conn - |> assign(:user, otheruser) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) + conn + |> post("/users/#{user.nickname}/outbox", data) + |> json_response(403) - assert json_response(conn, 403) + conn + |> assign(:user, other_user) + |> post("/users/#{user.nickname}/outbox", data) + |> json_response(403) end test "it inserts an incoming create activity into the database", %{conn: conn} do @@ -743,24 +858,42 @@ test "it returns relay followers", %{conn: conn} do result = conn - |> assign(:relay, true) |> get("/relay/followers") |> json_response(200) assert result["first"]["orderedItems"] == [user.ap_id] end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get("/relay/followers") + |> json_response(404) + end end describe "/relay/following" do test "it returns relay following", %{conn: conn} do result = conn - |> assign(:relay, true) |> get("/relay/following") |> json_response(200) assert result["first"]["orderedItems"] == [] end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get("/relay/following") + |> json_response(404) + end end describe "/users/:nickname/followers" do @@ -771,6 +904,7 @@ test "it returns the followers in a collection", %{conn: conn} do result = conn + |> assign(:user, user_two) |> get("/users/#{user_two.nickname}/followers") |> json_response(200) @@ -784,19 +918,22 @@ test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do result = conn + |> assign(:user, user) |> get("/users/#{user_two.nickname}/followers") |> json_response(200) assert is_binary(result["first"]) end - test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated", + test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user", %{conn: conn} do - user = insert(:user, hide_followers: true) + user = insert(:user) + other_user = insert(:user, hide_followers: true) result = conn - |> get("/users/#{user.nickname}/followers?page=1") + |> assign(:user, user) + |> get("/users/#{other_user.nickname}/followers?page=1") assert result.status == 403 assert result.resp_body == "" @@ -828,6 +965,7 @@ test "it works for more than 10 users", %{conn: conn} do result = conn + |> assign(:user, user) |> get("/users/#{user.nickname}/followers") |> json_response(200) @@ -837,12 +975,21 @@ test "it works for more than 10 users", %{conn: conn} do result = conn + |> assign(:user, user) |> get("/users/#{user.nickname}/followers?page=2") |> json_response(200) assert length(result["orderedItems"]) == 5 assert result["totalItems"] == 15 end + + test "returns 403 if requester is not logged in", %{conn: conn} do + user = insert(:user) + + conn + |> get("/users/#{user.nickname}/followers") + |> json_response(403) + end end describe "/users/:nickname/following" do @@ -853,6 +1000,7 @@ test "it returns the following in a collection", %{conn: conn} do result = conn + |> assign(:user, user) |> get("/users/#{user.nickname}/following") |> json_response(200) @@ -860,25 +1008,28 @@ test "it returns the following in a collection", %{conn: conn} do end test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do - user = insert(:user, hide_follows: true) - user_two = insert(:user) + user = insert(:user) + user_two = insert(:user, hide_follows: true) User.follow(user, user_two) result = conn - |> get("/users/#{user.nickname}/following") + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/following") |> json_response(200) assert is_binary(result["first"]) end - test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated", + test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user", %{conn: conn} do - user = insert(:user, hide_follows: true) + user = insert(:user) + user_two = insert(:user, hide_follows: true) result = conn - |> get("/users/#{user.nickname}/following?page=1") + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/following?page=1") assert result.status == 403 assert result.resp_body == "" @@ -911,6 +1062,7 @@ test "it works for more than 10 users", %{conn: conn} do result = conn + |> assign(:user, user) |> get("/users/#{user.nickname}/following") |> json_response(200) @@ -920,12 +1072,21 @@ test "it works for more than 10 users", %{conn: conn} do result = conn + |> assign(:user, user) |> get("/users/#{user.nickname}/following?page=2") |> json_response(200) assert length(result["orderedItems"]) == 5 assert result["totalItems"] == 15 end + + test "returns 403 if requester is not logged in", %{conn: conn} do + user = insert(:user) + + conn + |> get("/users/#{user.nickname}/following") + |> json_response(403) + end end describe "delivery tracking" do @@ -1011,7 +1172,7 @@ test "it tracks a signed activity fetch when the json is cached", %{conn: conn} end describe "Additional ActivityPub C2S endpoints" do - test "/api/ap/whoami", %{conn: conn} do + test "GET /api/ap/whoami", %{conn: conn} do user = insert(:user) conn = @@ -1022,12 +1183,16 @@ test "/api/ap/whoami", %{conn: conn} do user = User.get_cached_by_id(user.id) assert UserView.render("user.json", %{user: user}) == json_response(conn, 200) + + conn + |> get("/api/ap/whoami") + |> json_response(403) end clear_config([:media_proxy]) clear_config([Pleroma.Upload]) - test "uploadMedia", %{conn: conn} do + test "POST /api/ap/upload_media", %{conn: conn} do user = insert(:user) desc = "Description of the image" @@ -1047,67 +1212,10 @@ test "uploadMedia", %{conn: conn} do assert object["name"] == desc assert object["type"] == "Document" assert object["actor"] == user.ap_id - end - end - describe "when instance is not federating," do - clear_config([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], false) - end - - test "returns 404 for GET routes", %{conn: conn} do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/json") - - get_uris = [ - "/users/#{user.nickname}", - "/internal/fetch", - "/relay", - "/relay/following", - "/relay/followers" - ] - - for get_uri <- get_uris do - conn - |> get(get_uri) - |> json_response(404) - - conn - |> assign(:user, user) - |> get(get_uri) - |> json_response(404) - end - end - - test "returns 404 for activity-related POST routes", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - - post_activity_data = - "test/fixtures/mastodon-post-activity.json" - |> File.read!() - |> Poison.decode!() - - post_activity_uris = [ - "/inbox", - "/relay/inbox", - "/users/#{user.nickname}/inbox" - ] - - for post_activity_uri <- post_activity_uris do - conn - |> post(post_activity_uri, post_activity_data) - |> json_response(404) - - conn - |> assign(:user, user) - |> post(post_activity_uri, post_activity_data) - |> json_response(404) - end + conn + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> json_response(403) end end end diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs index 00712ab5a..00c50f003 100644 --- a/test/web/feed/user_controller_test.exs +++ b/test/web/feed/user_controller_test.exs @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do alias Pleroma.Object alias Pleroma.User - clear_config_all([:instance, :federating]) do + clear_config([:instance, :federating]) do Config.put([:instance, :federating], true) end @@ -82,160 +82,9 @@ test "returns 404 for a missing feed", %{conn: conn} do end end + # Note: see ActivityPubControllerTest for JSON format tests describe "feed_redirect" do - test "undefined format. it redirects to feed", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - response = - conn - |> put_req_header("accept", "application/xml") - |> get("/users/#{user.nickname}") - |> response(302) - - assert response == - "You are being redirected." - end - - test "undefined format. it returns error when user not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/xml") - |> get(user_feed_path(conn, :feed, "jimm")) - |> response(404) - - assert response == ~S({"error":"Not found"}) - end - - test "activity+json format. it redirects on actual feed of user", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - response = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}") - |> json_response(200) - - assert response["endpoints"] == %{ - "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize", - "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps", - "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token", - "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox", - "uploadMedia" => "#{Pleroma.Web.base_url()}/api/ap/upload_media" - } - - assert response["@context"] == [ - "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", - %{"@language" => "und"} - ] - - assert Map.take(response, [ - "followers", - "following", - "id", - "inbox", - "manuallyApprovesFollowers", - "name", - "outbox", - "preferredUsername", - "summary", - "tag", - "type", - "url" - ]) == %{ - "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers", - "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following", - "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}", - "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox", - "manuallyApprovesFollowers" => false, - "name" => user.name, - "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox", - "preferredUsername" => user.nickname, - "summary" => user.bio, - "tag" => [], - "type" => "Person", - "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}" - } - end - - test "activity+json format. it returns error whe use not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/users/jimm") - |> json_response(404) - - assert response == "Not found" - end - - test "json format. it redirects on actual feed of user", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - response = - conn - |> put_req_header("accept", "application/json") - |> get("/users/#{user.nickname}") - |> json_response(200) - - assert response["endpoints"] == %{ - "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize", - "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps", - "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token", - "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox", - "uploadMedia" => "#{Pleroma.Web.base_url()}/api/ap/upload_media" - } - - assert response["@context"] == [ - "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", - %{"@language" => "und"} - ] - - assert Map.take(response, [ - "followers", - "following", - "id", - "inbox", - "manuallyApprovesFollowers", - "name", - "outbox", - "preferredUsername", - "summary", - "tag", - "type", - "url" - ]) == %{ - "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers", - "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following", - "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}", - "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox", - "manuallyApprovesFollowers" => false, - "name" => user.name, - "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox", - "preferredUsername" => user.nickname, - "summary" => user.bio, - "tag" => [], - "type" => "Person", - "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}" - } - end - - test "json format. it returns error whe use not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/json") - |> get("/users/jimm") - |> json_response(404) - - assert response == "Not found" - end - - test "html format. it redirects on actual feed of user", %{conn: conn} do + test "with html format, it redirects to user feed", %{conn: conn} do note_activity = insert(:note_activity) user = User.get_cached_by_ap_id(note_activity.data["actor"]) @@ -251,7 +100,7 @@ test "html format. it redirects on actual feed of user", %{conn: conn} do ).resp_body end - test "html format. it returns error when user not found", %{conn: conn} do + test "with html format, it returns error when user is not found", %{conn: conn} do response = conn |> get("/users/jimm") @@ -259,30 +108,30 @@ test "html format. it returns error when user not found", %{conn: conn} do assert response == %{"error" => "Not found"} end - end - describe "feed_redirect (depending on federation enabled state)" do - setup %{conn: conn} do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/json") + test "with non-html / non-json format, it redirects to user feed in atom format", %{ + conn: conn + } do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) - %{conn: conn, user: user} + conn = + conn + |> put_req_header("accept", "application/xml") + |> get("/users/#{user.nickname}") + + assert conn.status == 302 + assert redirected_to(conn) == "#{Pleroma.Web.base_url()}/users/#{user.nickname}/feed.atom" end - clear_config([:instance, :federating]) + test "with non-html / non-json format, it returns error when user is not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get(user_feed_path(conn, :feed, "jimm")) + |> response(404) - test "renders if instance is federating", %{conn: conn, user: user} do - Config.put([:instance, :federating], true) - - conn = get(conn, "/users/#{user.nickname}") - assert json_response(conn, 200) - end - - test "renders 404 if instance is NOT federating", %{conn: conn, user: user} do - Config.put([:instance, :federating], false) - - conn = get(conn, "/users/#{user.nickname}") - assert json_response(conn, 404) + assert response == ~S({"error":"Not found"}) end end end diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index f035dfeee..7ac7e4af1 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -52,9 +52,8 @@ test "redirects on valid url when filename invalidated", %{conn: conn} do url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") invalid_url = String.replace(url, "test.png", "test-file.png") response = get(conn, invalid_url) - html = "You are being redirected." assert response.status == 302 - assert response.resp_body == html + assert redirected_to(response) == url end test "it performs ReverseProxy.call when signature valid", %{conn: conn} do diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 725ab1785..3b84358e4 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do import Pleroma.Factory + alias Pleroma.Config alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -16,22 +17,24 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do :ok end - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) + clear_config([:instance, :federating]) do + Config.put([:instance, :federating], true) end - describe "GET object/2" do + # Note: see ActivityPubControllerTest for JSON format tests + describe "GET /objects/:uuid (text/html)" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + %{conn: conn} + end + test "redirects to /notice/id for html format", %{conn: conn} do note_activity = insert(:note_activity) object = Object.normalize(note_activity) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) url = "/objects/#{uuid}" - conn = - conn - |> put_req_header("accept", "text/html") - |> get(url) - + conn = get(conn, url) assert redirected_to(conn) == "/notice/#{note_activity.id}" end @@ -45,23 +48,25 @@ test "404s on private objects", %{conn: conn} do |> response(404) end - test "404s on nonexisting objects", %{conn: conn} do + test "404s on non-existing objects", %{conn: conn} do conn |> get("/objects/123") |> response(404) end end - describe "GET activity/2" do + # Note: see ActivityPubControllerTest for JSON format tests + describe "GET /activities/:uuid (text/html)" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + %{conn: conn} + end + test "redirects to /notice/id for html format", %{conn: conn} do note_activity = insert(:note_activity) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/activities/#{uuid}") - + conn = get(conn, "/activities/#{uuid}") assert redirected_to(conn) == "/notice/#{note_activity.id}" end @@ -79,19 +84,6 @@ test "404s on nonexistent activities", %{conn: conn} do |> get("/activities/123") |> response(404) end - - test "gets an activity in AS2 format", %{conn: conn} do - note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - url = "/activities/#{uuid}" - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get(url) - - assert json_response(conn, 200) - end end describe "GET notice/2" do @@ -170,7 +162,7 @@ test "404s a private notice", %{conn: conn} do assert response(conn, 404) end - test "404s a nonexisting notice", %{conn: conn} do + test "404s a non-existing notice", %{conn: conn} do url = "/notice/123" conn = @@ -179,10 +171,21 @@ test "404s a nonexisting notice", %{conn: conn} do assert response(conn, 404) end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + note_activity = insert(:note_activity) + + conn = put_req_header(conn, "accept", "text/html") + + ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) + end end describe "GET /notice/:id/embed_player" do - test "render embed player", %{conn: conn} do + setup do note_activity = insert(:note_activity) object = Pleroma.Object.normalize(note_activity) @@ -204,9 +207,11 @@ test "render embed player", %{conn: conn} do |> Ecto.Changeset.change(data: object_data) |> Pleroma.Repo.update() - conn = - conn - |> get("/notice/#{note_activity.id}/embed_player") + %{note_activity: note_activity} + end + + test "renders embed player", %{conn: conn, note_activity: note_activity} do + conn = get(conn, "/notice/#{note_activity.id}/embed_player") assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"] @@ -272,38 +277,19 @@ test "404s when attachment isn't audio or video", %{conn: conn} do |> Ecto.Changeset.change(data: object_data) |> Pleroma.Repo.update() - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - end - - describe "when instance is not federating," do - clear_config([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], false) + conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) end - test "returns 404 for GET routes", %{conn: conn} do - conn = put_req_header(conn, "accept", "application/json") + test "it requires authentication if instance is NOT federating", %{ + conn: conn, + note_activity: note_activity + } do + user = insert(:user) + conn = put_req_header(conn, "accept", "text/html") - note_activity = insert(:note_activity, local: true) - [_, activity_uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - object = Object.normalize(note_activity) - [_, object_uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - - get_uris = [ - "/activities/#{activity_uuid}", - "/objects/#{object_uuid}", - "/notice/#{note_activity.id}", - "/notice/#{note_activity.id}/embed_player" - ] - - for get_uri <- get_uris do - conn - |> get(get_uri) - |> json_response(404) - end + ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) end end end From d9134d4430d4592030af12354aa2231e7c260140 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 10 Mar 2020 11:49:02 +0100 Subject: [PATCH 15/79] installation/otp_en.md: Fix pleroma.nginx target [deb] Needs to be backported to stable. Related: https://git.pleroma.social/pleroma/pleroma-support/issues/29 --- docs/installation/otp_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 32551f7b6..fb99af699 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -156,8 +156,8 @@ cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf ``` ```sh tab="Debian/Ubuntu" -cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx -ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx +cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.conf +ln -s /etc/nginx/sites-available/pleroma.conf /etc/nginx/sites-enabled/pleroma.conf ``` If your distro does not have either of those you can append `include /etc/nginx/pleroma.conf` to the end of the http section in /etc/nginx/nginx.conf and From 5af798f24659a1558149cf6deddfa55fbc493ac2 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 10 Mar 2020 13:08:00 -0500 Subject: [PATCH 16/79] Fix enforcement of character limits --- lib/pleroma/web/common_api/utils.ex | 2 +- test/web/common_api/common_api_test.exs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 8746273c4..348fdedf1 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -591,7 +591,7 @@ def validate_character_limit(full_payload, _attachments) do limit = Pleroma.Config.get([:instance, :limit]) length = String.length(full_payload) - if length < limit do + if length <= limit do :ok else {:error, dgettext("errors", "The status is over the character limit")} diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 299d968db..b80523160 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -202,13 +202,15 @@ test "it returns error when status is empty and no attachments" do CommonAPI.post(user, %{"status" => ""}) end - test "it returns error when character limit is exceeded" do + test "it validates character limits are correctly enforced" do Pleroma.Config.put([:instance, :limit], 5) user = insert(:user) assert {:error, "The status is over the character limit"} = CommonAPI.post(user, %{"status" => "foobar"}) + + assert {:ok, activity} = CommonAPI.post(user, %{"status" => "12345"}) end test "it can handle activities that expire" do From a747eb6df9c554c9b17de03f2c1332f6fb9ba164 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 11 Mar 2020 06:35:18 +0100 Subject: [PATCH 17/79] static-fe.css: Restore from before a65ee8ea Related: https://git.pleroma.social/pleroma/pleroma/issues/1616 --- priv/static/static/static-fe.css | Bin 0 -> 2629 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 priv/static/static/static-fe.css diff --git a/priv/static/static/static-fe.css b/priv/static/static/static-fe.css new file mode 100644 index 0000000000000000000000000000000000000000..19c56387b1ea9aa19f3d0f8d596c949f0b0c6485 GIT binary patch literal 2629 zcmc&#$!^;)5WV{=h!_RhAW|eRiLIcQ)Xh&6Jy0Sgv8G6YqP(=|zn8nM$Z3%vK;ug= z)GY7Ko0*)WBltA|>Mw`Iwi3K&C@MJRA_LReYF5neAIx*fnT$%<5PHl&2U!<&Sdh{_ zI){BtBz~Cuo=h4@n2Xy$Q4yAbWzrr1>?OA@4Ln49f@Gb6^__aM))pc}Se1cAFkPr? zp3awxx%J4oKv+ZqBGPxtPe)P()EFxBe=Q+~g(N#71Hl(6)x-zPDQ*TVL^+p|HyN1u z2g_x!8wUk8hC3*sM_crw)9q_HVG;{NhyQ{!mE)wO9Lfw(T*{Y;)aALHf>gEo-t!y4 zDY2+5Oac}92J8sJ{6@7I5$+O=;hnX9m4rTPYmIFdXI9Xr9j3UD7`bQ{)R5-0`}xws zER=p_2avo0vH#z@ju1O!nt}{?E}=^_l5|{01r$D5Ls+PGvGVeL)*oX(i4m%zTo9i{ z;)9G3F1Ub_a5W{2Gdn>}z%Z-%4vRBSYj9PL{MGnkL5T|zBhs}L9wP=DJnTm#dF$G# zyX-#9Ku&qFwTdvob#}$vJ_DFi(srb?k0*Og5W>t_N37jmgDu7_zzzRFKq+_wa;zpC z)PzDoc;C?@sQU_o>)Xk&+>=(%rQ!QD{5EE!*uEO19v$!acUnVR;(irmt(n^21*b%D;l)8?5o`(?b&Aud)nhFQjYH!O^c2GX;Kj{L-G zX`D@#-oRmN*ww01l*-X1aYdy`^1)yrtnHPaWC6$CazL2gs38`E)05SWhmvb3argBgff$&0=VK+P!$ zTlW-ek*_dP+$++I+c$Y^@#m|e#>G}~arJ#jW9V((tFrtdd=-xufRFFg0963M;3mm(wtjpO?&Id`^2u$4cWeI} G;Qj%!G8J0@ literal 0 HcmV?d00001 From a06104b9d59f93d3275dff30854543c2e705b4fe Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 11 Mar 2020 08:35:46 +0100 Subject: [PATCH 18/79] CLI_tasks/user.md: Fix `pleroma.user new` documentation Closes: https://git.pleroma.social/pleroma/pleroma/issues/1621 [ci skip] --- docs/administration/CLI_tasks/user.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index da8363131..64385ad28 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -5,11 +5,11 @@ ## Create a user ```sh tab="OTP" -./bin/pleroma_ctl user new [] +./bin/pleroma_ctl user new [] ``` ```sh tab="From Source" -mix pleroma.user new [] +mix pleroma.user new [] ``` From 6316726a5fe9d65ee865d281efc2d2652001eb8c Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 11 Mar 2020 08:46:57 +0100 Subject: [PATCH 19/79] CLI_tasks: Use manpage formatting conventions - [] for options - <> for mandatory arguments - foo ... when foo can be repeated [ci skip] --- docs/administration/CLI_tasks/database.md | 8 ++++---- docs/administration/CLI_tasks/digest.md | 4 ++-- docs/administration/CLI_tasks/emoji.md | 8 ++++---- docs/administration/CLI_tasks/instance.md | 4 ++-- docs/administration/CLI_tasks/uploads.md | 4 ++-- docs/administration/CLI_tasks/user.md | 12 ++++++------ 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/administration/CLI_tasks/database.md b/docs/administration/CLI_tasks/database.md index 51c7484ba..ff400c8ed 100644 --- a/docs/administration/CLI_tasks/database.md +++ b/docs/administration/CLI_tasks/database.md @@ -10,11 +10,11 @@ Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration. ```sh tab="OTP" -./bin/pleroma_ctl database remove_embedded_objects [] +./bin/pleroma_ctl database remove_embedded_objects [option ...] ``` ```sh tab="From Source" -mix pleroma.database remove_embedded_objects [] +mix pleroma.database remove_embedded_objects [option ...] ``` ### Options @@ -28,11 +28,11 @@ This will prune remote posts older than 90 days (configurable with [`config :ple The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free. ```sh tab="OTP" -./bin/pleroma_ctl database prune_objects [] +./bin/pleroma_ctl database prune_objects [option ...] ``` ```sh tab="From Source" -mix pleroma.database prune_objects [] +mix pleroma.database prune_objects [option ...] ``` ### Options diff --git a/docs/administration/CLI_tasks/digest.md b/docs/administration/CLI_tasks/digest.md index 1badda8c3..2eb31379e 100644 --- a/docs/administration/CLI_tasks/digest.md +++ b/docs/administration/CLI_tasks/digest.md @@ -5,11 +5,11 @@ ## Send digest email since given date (user registration date by default) ignoring user activity status. ```sh tab="OTP" - ./bin/pleroma_ctl digest test [] + ./bin/pleroma_ctl digest test [since_date] ``` ```sh tab="From Source" -mix pleroma.digest test [] +mix pleroma.digest test [since_date] ``` diff --git a/docs/administration/CLI_tasks/emoji.md b/docs/administration/CLI_tasks/emoji.md index a3207bc6c..efec8222c 100644 --- a/docs/administration/CLI_tasks/emoji.md +++ b/docs/administration/CLI_tasks/emoji.md @@ -5,11 +5,11 @@ ## Lists emoji packs and metadata specified in the manifest ```sh tab="OTP" -./bin/pleroma_ctl emoji ls-packs [] +./bin/pleroma_ctl emoji ls-packs [option ...] ``` ```sh tab="From Source" -mix pleroma.emoji ls-packs [] +mix pleroma.emoji ls-packs [option ...] ``` @@ -19,11 +19,11 @@ mix pleroma.emoji ls-packs [] ## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME` ```sh tab="OTP" -./bin/pleroma_ctl emoji get-packs [] +./bin/pleroma_ctl emoji get-packs [option ...] ``` ```sh tab="From Source" -mix pleroma.emoji get-packs [] +mix pleroma.emoji get-packs [option ...] ``` ### Options diff --git a/docs/administration/CLI_tasks/instance.md b/docs/administration/CLI_tasks/instance.md index 1a3b268be..52e264bb1 100644 --- a/docs/administration/CLI_tasks/instance.md +++ b/docs/administration/CLI_tasks/instance.md @@ -4,11 +4,11 @@ ## Generate a new configuration file ```sh tab="OTP" - ./bin/pleroma_ctl instance gen [] + ./bin/pleroma_ctl instance gen [option ...] ``` ```sh tab="From Source" -mix pleroma.instance gen [] +mix pleroma.instance gen [option ...] ``` diff --git a/docs/administration/CLI_tasks/uploads.md b/docs/administration/CLI_tasks/uploads.md index e36c94c38..6a15d22f6 100644 --- a/docs/administration/CLI_tasks/uploads.md +++ b/docs/administration/CLI_tasks/uploads.md @@ -4,11 +4,11 @@ ## Migrate uploads from local to remote storage ```sh tab="OTP" - ./bin/pleroma_ctl uploads migrate_local [] + ./bin/pleroma_ctl uploads migrate_local [option ...] ``` ```sh tab="From Source" -mix pleroma.uploads migrate_local [] +mix pleroma.uploads migrate_local [option ...] ``` ### Options diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 64385ad28..f535dad82 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -5,11 +5,11 @@ ## Create a user ```sh tab="OTP" -./bin/pleroma_ctl user new [] +./bin/pleroma_ctl user new [option ...] ``` ```sh tab="From Source" -mix pleroma.user new [] +mix pleroma.user new [option ...] ``` @@ -33,11 +33,11 @@ mix pleroma.user list ## Generate an invite link ```sh tab="OTP" - ./bin/pleroma_ctl user invite [] + ./bin/pleroma_ctl user invite [option ...] ``` ```sh tab="From Source" -mix pleroma.user invite [] +mix pleroma.user invite [option ...] ``` @@ -137,11 +137,11 @@ mix pleroma.user reset_password ## Set the value of the given user's settings ```sh tab="OTP" - ./bin/pleroma_ctl user set [] + ./bin/pleroma_ctl user set [option ...] ``` ```sh tab="From Source" -mix pleroma.user set [] +mix pleroma.user set [option ...] ``` ### Options From 5b696a8ac1b5a06e60c2143cf88e014b28e14702 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 11 Mar 2020 14:05:56 +0300 Subject: [PATCH 20/79] [#1560] Enforced authentication for non-federating instances in StaticFEController. --- .../web/static_fe/static_fe_controller.ex | 20 +++++++++++-------- test/support/conn_case.ex | 9 +++++++-- .../static_fe/static_fe_controller_test.exs | 14 +++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 5ac75f1c4..5027d5c23 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -17,6 +17,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:assign_id) + plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.FederatingPlug.federating?/0 + ) + @page_keys ["max_id", "min_id", "limit", "since_id", "order"] defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), @@ -33,7 +37,7 @@ defp not_found(conn, message) do |> render("error.html", %{message: message, meta: ""}) end - def get_counts(%Activity{} = activity) do + defp get_counts(%Activity{} = activity) do %Object{data: data} = Object.normalize(activity) %{ @@ -43,9 +47,9 @@ def get_counts(%Activity{} = activity) do } end - def represent(%Activity{} = activity), do: represent(activity, false) + defp represent(%Activity{} = activity), do: represent(activity, false) - def represent(%Activity{object: %Object{data: data}} = activity, selected) do + defp represent(%Activity{object: %Object{data: data}} = activity, selected) do {:ok, user} = User.get_or_fetch(activity.object.data["actor"]) link = @@ -147,17 +151,17 @@ def show(%{assigns: %{activity_id: _}} = conn, _params) do end end - def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), + defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), do: assign(conn, :notice_id, notice_id) - def assign_id(%{path_info: ["users", user_id]} = conn, _opts), + defp assign_id(%{path_info: ["users", user_id]} = conn, _opts), do: assign(conn, :username_or_id, user_id) - def assign_id(%{path_info: ["objects", object_id]} = conn, _opts), + defp assign_id(%{path_info: ["objects", object_id]} = conn, _opts), do: assign(conn, :object_id, object_id) - def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts), + defp assign_id(%{path_info: ["activities", activity_id]} = conn, _opts), do: assign(conn, :activity_id, activity_id) - def assign_id(conn, _opts), do: conn + defp assign_id(conn, _opts), do: conn end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index d6595f971..064874201 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -26,6 +26,8 @@ defmodule Pleroma.Web.ConnCase do use Pleroma.Tests.Helpers import Pleroma.Web.Router.Helpers + alias Pleroma.Config + # The default endpoint for testing @endpoint Pleroma.Web.Endpoint @@ -50,7 +52,10 @@ defp oauth_access(scopes, opts \\ []) do end defp ensure_federating_or_authenticated(conn, url, user) do - Pleroma.Config.put([:instance, :federating], false) + initial_setting = Config.get([:instance, :federating]) + on_exit(fn -> Config.put([:instance, :federating], initial_setting) end) + + Config.put([:instance, :federating], false) conn |> get(url) @@ -61,7 +66,7 @@ defp ensure_federating_or_authenticated(conn, url, user) do |> get(url) |> response(200) - Pleroma.Config.put([:instance, :federating], true) + Config.put([:instance, :federating], true) conn |> get(url) diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index 11facab99..a072cc78f 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -12,6 +12,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do Config.put([:static_fe, :enabled], true) end + clear_config([:instance, :federating]) do + Config.put([:instance, :federating], true) + end + setup %{conn: conn} do conn = put_req_header(conn, "accept", "text/html") user = insert(:user) @@ -70,6 +74,10 @@ test "pagination, page 2", %{conn: conn, user: user} do refute html =~ ">test20<" refute html =~ ">test29<" end + + test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) + end end describe "notice html" do @@ -153,5 +161,11 @@ test "302 for remote cached status", %{conn: conn, user: user} do assert html_response(conn, 302) =~ "redirected" end + + test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"}) + + ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) + end end end From 863ec33ba2a90708d199f18683ffe0c4658c710a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 11 Mar 2020 12:21:44 +0100 Subject: [PATCH 21/79] Add support for funkwhale Audio activity reel2bits fixture not included as it lacks the Actor fixture for it. Closes: https://git.pleroma.social/pleroma/pleroma/issues/1624 Closes: https://git.pleroma.social/pleroma/pleroma/issues/764 --- .../web/activity_pub/transmogrifier.ex | 5 ++- .../web/mastodon_api/views/status_view.ex | 2 +- test/fixtures/tesla_mock/funkwhale_audio.json | 44 +++++++++++++++++++ .../tesla_mock/funkwhale_channel.json | 44 +++++++++++++++++++ test/support/http_request_mock.ex | 15 +++++++ .../mastodon_api/views/status_view_test.exs | 16 +++++++ test/web/oauth/oauth_controller_test.exs | 2 +- 7 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/tesla_mock/funkwhale_audio.json create mode 100644 test/fixtures/tesla_mock/funkwhale_channel.json diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 9cd3de705..f52b065f6 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -229,7 +229,8 @@ def fix_url(%{"url" => url} = object) when is_map(url) do Map.put(object, "url", url["href"]) end - def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do + def fix_url(%{"type" => object_type, "url" => url} = object) + when object_type in ["Video", "Audio"] and is_list(url) do first_element = Enum.at(url, 0) link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) @@ -398,7 +399,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, options ) - when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer"] do + when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do actor = Containment.get_actor(data) data = diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index f7469cdff..a042075f5 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -421,7 +421,7 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end def render_content(%{data: %{"type" => object_type}} = object) - when object_type in ["Video", "Event"] do + when object_type in ["Video", "Event", "Audio"] do with name when not is_nil(name) and name != "" <- object.data["name"] do "

#{name}

#{object.data["content"]}" else diff --git a/test/fixtures/tesla_mock/funkwhale_audio.json b/test/fixtures/tesla_mock/funkwhale_audio.json new file mode 100644 index 000000000..15736b1f8 --- /dev/null +++ b/test/fixtures/tesla_mock/funkwhale_audio.json @@ -0,0 +1,44 @@ +{ + "id": "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871", + "type": "Audio", + "name": "Compositions - Test Audio for Pleroma", + "attributedTo": "https://channels.tests.funkwhale.audio/federation/actors/compositions", + "published": "2020-03-11T10:01:52.714918+00:00", + "to": "https://www.w3.org/ns/activitystreams#Public", + "url": [ + { + "type": "Link", + "mimeType": "audio/ogg", + "href": "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false" + }, + { + "type": "Link", + "mimeType": "text/html", + "href": "https://channels.tests.funkwhale.audio/library/tracks/74" + } + ], + "content": "

This is a test Audio for Pleroma.

", + "mediaType": "text/html", + "tag": [ + { + "type": "Hashtag", + "name": "#funkwhale" + }, + { + "type": "Hashtag", + "name": "#test" + }, + { + "type": "Hashtag", + "name": "#tests" + } + ], + "summary": "#funkwhale #test #tests", + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers" + } + ] +} diff --git a/test/fixtures/tesla_mock/funkwhale_channel.json b/test/fixtures/tesla_mock/funkwhale_channel.json new file mode 100644 index 000000000..cf9ee8151 --- /dev/null +++ b/test/fixtures/tesla_mock/funkwhale_channel.json @@ -0,0 +1,44 @@ +{ + "id": "https://channels.tests.funkwhale.audio/federation/actors/compositions", + "outbox": "https://channels.tests.funkwhale.audio/federation/actors/compositions/outbox", + "inbox": "https://channels.tests.funkwhale.audio/federation/actors/compositions/inbox", + "preferredUsername": "compositions", + "type": "Person", + "name": "Compositions", + "followers": "https://channels.tests.funkwhale.audio/federation/actors/compositions/followers", + "following": "https://channels.tests.funkwhale.audio/federation/actors/compositions/following", + "manuallyApprovesFollowers": false, + "url": [ + { + "type": "Link", + "href": "https://channels.tests.funkwhale.audio/channels/compositions", + "mediaType": "text/html" + }, + { + "type": "Link", + "href": "https://channels.tests.funkwhale.audio/api/v1/channels/compositions/rss", + "mediaType": "application/rss+xml" + } + ], + "icon": { + "type": "Image", + "url": "https://channels.tests.funkwhale.audio/media/attachments/75/b4/f1/nosmile.jpeg", + "mediaType": "image/jpeg" + }, + "summary": "

I'm testing federation with the fediverse :)

", + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers" + } + ], + "publicKey": { + "owner": "https://channels.tests.funkwhale.audio/federation/actors/compositions", + "publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAv25u57oZfVLV3KltS+HcsdSx9Op4MmzIes1J8Wu8s0KbdXf2zEwS\nsVqyHgs/XCbnzsR3FqyJTo46D2BVnvZcuU5srNcR2I2HMaqQ0oVdnATE4K6KdcgV\nN+98pMWo56B8LTgE1VpvqbsrXLi9jCTzjrkebVMOP+ZVu+64v1qdgddseblYMnBZ\nct0s7ONbHnqrWlTGf5wES1uIZTVdn5r4MduZG+Uenfi1opBS0lUUxfWdW9r0oF2b\nyneZUyaUCbEroeKbqsweXCWVgnMarUOsgqC42KM4cf95lySSwTSaUtZYIbTw7s9W\n2jveU/rVg8BYZu5JK5obgBoxtlUeUoSswwIDAQAB\n-----END RSA PUBLIC KEY-----\n", + "id": "https://channels.tests.funkwhale.audio/federation/actors/compositions#main-key" + }, + "endpoints": { + "sharedInbox": "https://channels.tests.funkwhale.audio/federation/shared/inbox" + } +} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index d46887865..0079d8c44 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1273,6 +1273,21 @@ def get("https://patch.cx/users/rin", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}} end + def get( + "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_audio.json")}} + end + + def get("https://channels.tests.funkwhale.audio/federation/actors/compositions", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json")}} + end + def get("http://example.com/rel_me/error", _, _, _) do {:ok, %Tesla.Env{status: 404, body: ""}} end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 191895c6f..3e1812a1f 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -420,6 +420,22 @@ test "a peertube video" do assert length(represented[:media_attachments]) == 1 end + test "funkwhale audio" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + end + test "a Mobilizon event" do user = insert(:user) diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index cff469c28..5f86d999c 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -581,7 +581,7 @@ test "redirects with oauth authorization, " <> # In case scope param is missing, expecting _all_ app-supported scopes to be granted for user <- [non_admin, admin], {requested_scopes, expected_scopes} <- - %{scopes_subset => scopes_subset, nil => app_scopes} do + %{scopes_subset => scopes_subset, nil: app_scopes} do conn = post( build_conn(), From 282a93554fbf919ff553d839eeea98abe1f861d4 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 11 Mar 2020 16:25:53 +0300 Subject: [PATCH 22/79] merging release default config on app start --- lib/mix/tasks/pleroma/docs.ex | 2 +- lib/pleroma/application.ex | 1 + lib/pleroma/config/holder.ex | 38 +++++++++++++++---- lib/pleroma/config/loader.ex | 32 ++++++---------- lib/pleroma/config/transfer_task.ex | 2 +- lib/pleroma/docs/json.ex | 2 +- .../web/admin_api/admin_api_controller.ex | 2 +- test/config/holder_test.exs | 16 ++++---- test/config/loader_test.exs | 19 +--------- test/config/transfer_task_test.exs | 2 +- 10 files changed, 58 insertions(+), 58 deletions(-) diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex index 3c870f876..6088fc71d 100644 --- a/lib/mix/tasks/pleroma/docs.ex +++ b/lib/mix/tasks/pleroma/docs.ex @@ -28,7 +28,7 @@ def run(_) do defp do_run(implementation) do start_pleroma() - with descriptions <- Pleroma.Config.Loader.load("config/description.exs"), + with descriptions <- Pleroma.Config.Loader.read("config/description.exs"), {:ok, file_path} <- Pleroma.Docs.Generator.process( implementation, diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 18854b850..c5b9a98fd 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -31,6 +31,7 @@ def user_agent do # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do + Pleroma.Config.Holder.to_ets() Pleroma.HTML.compile_scrubbers() Pleroma.Config.DeprecationWarnings.warn() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() diff --git a/lib/pleroma/config/holder.ex b/lib/pleroma/config/holder.ex index f1a339703..88e1db313 100644 --- a/lib/pleroma/config/holder.ex +++ b/lib/pleroma/config/holder.ex @@ -3,14 +3,38 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.Holder do - @config Pleroma.Config.Loader.load_and_merge() + @config Pleroma.Config.Loader.default_config() - @spec config() :: keyword() - def config, do: @config + @spec to_ets() :: true + def to_ets do + :ets.new(:default_config, [:named_table, :protected]) - @spec config(atom()) :: any() - def config(group), do: @config[group] + default_config = + if System.get_env("RELEASE_NAME") do + release_config = + [:code.root_dir(), "releases", System.get_env("RELEASE_VSN"), "releases.exs"] + |> Path.join() + |> Pleroma.Config.Loader.read() - @spec config(atom(), atom()) :: any() - def config(group, key), do: @config[group][key] + Pleroma.Config.Loader.merge(@config, release_config) + else + @config + end + + :ets.insert(:default_config, {:config, default_config}) + end + + @spec default_config() :: keyword() + def default_config, do: from_ets() + + @spec default_config(atom()) :: keyword() + def default_config(group), do: Keyword.get(from_ets(), group) + + @spec default_config(atom(), atom()) :: keyword() + def default_config(group, key), do: get_in(from_ets(), [group, key]) + + defp from_ets do + [{:config, default_config}] = :ets.lookup(:default_config, :config) + default_config + end end diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index df2d18725..b2cb34129 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -13,32 +13,22 @@ defmodule Pleroma.Config.Loader do ] if Code.ensure_loaded?(Config.Reader) do - @spec load(Path.t()) :: keyword() - def load(path), do: Config.Reader.read!(path) - - defp do_merge(conf1, conf2), do: Config.Reader.merge(conf1, conf2) + @reader Config.Reader else # support for Elixir less than 1.9 - @spec load(Path.t()) :: keyword() - def load(path) do - path - |> Mix.Config.eval!() - |> elem(0) - end - - defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2) + @reader Mix.Config end - @spec load_and_merge() :: keyword() - def load_and_merge do - all_paths = - if Pleroma.Config.get(:release), - do: ["config/config.exs", "config/releases.exs"], - else: ["config/config.exs"] + @spec read(Path.t()) :: keyword() + def read(path), do: @reader.read!(path) - all_paths - |> Enum.map(&load(&1)) - |> Enum.reduce([], &do_merge(&2, &1)) + @spec merge(keyword(), keyword()) :: keyword() + def merge(c1, c2), do: @reader.merge(c1, c2) + + @spec default_config() :: keyword() + def default_config do + "config/config.exs" + |> read() |> filter() end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 435fc7450..7c3449b5e 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -83,7 +83,7 @@ defp merge_and_update(setting) do key = ConfigDB.from_string(setting.key) group = ConfigDB.from_string(setting.group) - default = Pleroma.Config.Holder.config(group, key) + default = Pleroma.Config.Holder.default_config(group, key) value = ConfigDB.from_binary(setting.value) merged_value = diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index 6508a7bdb..74f8b2615 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -15,7 +15,7 @@ def process(descriptions) do end def compile do - with config <- Pleroma.Config.Loader.load("config/description.exs") do + with config <- Pleroma.Config.Loader.read("config/description.exs") do config[:pleroma][:config_description] |> Pleroma.Docs.Generator.convert_to_strings() |> Jason.encode!() diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de0755ee5..47b7d2da3 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -834,7 +834,7 @@ def config_show(conn, _params) do configs = ConfigDB.get_all_as_keyword() merged = - Config.Holder.config() + Config.Holder.default_config() |> ConfigDB.merge(configs) |> Enum.map(fn {group, values} -> Enum.map(values, fn {key, value} -> diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs index 2368d4856..15d48b5c7 100644 --- a/test/config/holder_test.exs +++ b/test/config/holder_test.exs @@ -7,8 +7,8 @@ defmodule Pleroma.Config.HolderTest do alias Pleroma.Config.Holder - test "config/0" do - config = Holder.config() + test "default_config/0" do + config = Holder.default_config() assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads" assert config[:tesla][:adapter] == Tesla.Mock @@ -20,15 +20,15 @@ test "config/0" do refute config[:phoenix][:serve_endpoints] end - test "config/1" do - pleroma_config = Holder.config(:pleroma) + test "default_config/1" do + pleroma_config = Holder.default_config(:pleroma) assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads" - tesla_config = Holder.config(:tesla) + tesla_config = Holder.default_config(:tesla) assert tesla_config[:adapter] == Tesla.Mock end - test "config/2" do - assert Holder.config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"] - assert Holder.config(:tesla, :adapter) == Tesla.Mock + test "default_config/2" do + assert Holder.default_config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"] + assert Holder.default_config(:tesla, :adapter) == Tesla.Mock end end diff --git a/test/config/loader_test.exs b/test/config/loader_test.exs index 4c93e5d4d..607572f4e 100644 --- a/test/config/loader_test.exs +++ b/test/config/loader_test.exs @@ -7,28 +7,13 @@ defmodule Pleroma.Config.LoaderTest do alias Pleroma.Config.Loader - test "load/1" do - config = Loader.load("test/fixtures/config/temp.secret.exs") + test "read/1" do + config = Loader.read("test/fixtures/config/temp.secret.exs") assert config[:pleroma][:first_setting][:key] == "value" assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo] assert config[:quack][:level] == :info end - test "load_and_merge/0" do - config = Loader.load_and_merge() - - refute config[:pleroma][Pleroma.Repo] - refute config[:pleroma][Pleroma.Web.Endpoint] - refute config[:pleroma][:env] - refute config[:pleroma][:configurable_from_database] - refute config[:pleroma][:database] - refute config[:phoenix][:serve_endpoints] - - assert config[:pleroma][:ecto_repos] == [Pleroma.Repo] - assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads" - assert config[:tesla][:adapter] == Tesla.Mock - end - test "filter_group/2" do assert Loader.filter_group(:pleroma, pleroma: [ diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index ce31d1e87..01d04761d 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -70,7 +70,7 @@ test "transfer config values for 1 group and some keys" do assert Application.get_env(:quack, :level) == :info assert Application.get_env(:quack, :meta) == [:none] - default = Pleroma.Config.Holder.config(:quack, :webhook_url) + default = Pleroma.Config.Holder.default_config(:quack, :webhook_url) assert Application.get_env(:quack, :webhook_url) == default on_exit(fn -> From 193d67cde590efd9a75ac11da76657151f58afdd Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 11 Mar 2020 16:43:58 +0300 Subject: [PATCH 23/79] compile fix --- lib/pleroma/config/loader.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index b2cb34129..6ca6550bd 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -14,13 +14,19 @@ defmodule Pleroma.Config.Loader do if Code.ensure_loaded?(Config.Reader) do @reader Config.Reader + + def read(path), do: @reader.read!(path) else # support for Elixir less than 1.9 @reader Mix.Config + def read(path) do + path + |> @reader.eval!() + |> elem(0) + end end @spec read(Path.t()) :: keyword() - def read(path), do: @reader.read!(path) @spec merge(keyword(), keyword()) :: keyword() def merge(c1, c2), do: @reader.merge(c1, c2) From fce090c1de543f0bcebf47cfc2a32f99f8ef401f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 11 Mar 2020 17:22:50 +0300 Subject: [PATCH 24/79] using Pleroma.Config instead of ets --- lib/pleroma/application.ex | 2 +- lib/pleroma/config/holder.ex | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c5b9a98fd..33f1705df 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -31,7 +31,7 @@ def user_agent do # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do - Pleroma.Config.Holder.to_ets() + Pleroma.Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() Pleroma.Config.DeprecationWarnings.warn() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() diff --git a/lib/pleroma/config/holder.ex b/lib/pleroma/config/holder.ex index 88e1db313..f037d5d48 100644 --- a/lib/pleroma/config/holder.ex +++ b/lib/pleroma/config/holder.ex @@ -5,10 +5,8 @@ defmodule Pleroma.Config.Holder do @config Pleroma.Config.Loader.default_config() - @spec to_ets() :: true - def to_ets do - :ets.new(:default_config, [:named_table, :protected]) - + @spec save_default() :: :ok + def save_default do default_config = if System.get_env("RELEASE_NAME") do release_config = @@ -21,20 +19,17 @@ def to_ets do @config end - :ets.insert(:default_config, {:config, default_config}) + Pleroma.Config.put(:default_config, default_config) end @spec default_config() :: keyword() - def default_config, do: from_ets() + def default_config, do: get_default() @spec default_config(atom()) :: keyword() - def default_config(group), do: Keyword.get(from_ets(), group) + def default_config(group), do: Keyword.get(get_default(), group) @spec default_config(atom(), atom()) :: keyword() - def default_config(group, key), do: get_in(from_ets(), [group, key]) + def default_config(group, key), do: get_in(get_default(), [group, key]) - defp from_ets do - [{:config, default_config}] = :ets.lookup(:default_config, :config) - default_config - end + defp get_default, do: Pleroma.Config.get(:default_config) end From c3b9fbd3a759d281ef2e81395b78549e43cab63c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 11 Mar 2020 17:58:25 +0300 Subject: [PATCH 25/79] Revert "Set better Cache-Control header for static content" On furher investigation it seems like all that did was cause unintuitive behavior. The emoji request flood that was the reason for introducing it isn't really that big of a deal either, since Plug.Static only needs to read file modification time and size to determine the ETag. Closes #1613 --- lib/pleroma/web/endpoint.ex | 2 +- test/plugs/cache_control_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 118c3ac6f..72cb3ee27 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Plugs.HTTPSecurityPlug) plug(Pleroma.Plugs.UploadedMedia) - @static_cache_control "public max-age=86400 must-revalidate" + @static_cache_control "public, no-cache" # 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/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs index 005912ffb..6b567e81d 100644 --- a/test/plugs/cache_control_test.exs +++ b/test/plugs/cache_control_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Web.CacheControlTest do test "Verify Cache-Control header on static assets", %{conn: conn} do conn = get(conn, "/index.html") - assert Conn.get_resp_header(conn, "cache-control") == ["public max-age=86400 must-revalidate"] + assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"] end test "Verify Cache-Control header on the API", %{conn: conn} do From d1379c4de8ca27fa6d02d20a0029b248efe1d09e Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 13 Feb 2020 03:39:47 +0100 Subject: [PATCH 26/79] Formatting: Do not use \n and prefer
instead It moves bbcode to bbcode_pleroma as the former is owned by kaniini and transfering ownership wasn't done in a timely manner. Closes: https://git.pleroma.social/pleroma/pleroma/issues/1374 Closes: https://git.pleroma.social/pleroma/pleroma/issues/1375 --- CHANGELOG.md | 4 + lib/pleroma/earmark_renderer.ex | 256 ++++++++++++++++++ lib/pleroma/web/common_api/utils.ex | 2 +- mix.exs | 2 +- mix.lock | 6 +- test/earmark_renderer_test.ex | 79 ++++++ test/web/common_api/common_api_utils_test.exs | 28 +- 7 files changed, 357 insertions(+), 20 deletions(-) create mode 100644 lib/pleroma/earmark_renderer.ex create mode 100644 test/earmark_renderer_test.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 100228c6c..4168086e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [unreleased] +### Changed +- **Breaking:** BBCode and Markdown formatters will no longer return any `\n` and only use `
` for newlines + ## [2.0.0] - 2019-03-08 ### Security - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request. diff --git a/lib/pleroma/earmark_renderer.ex b/lib/pleroma/earmark_renderer.ex new file mode 100644 index 000000000..6211a3b4a --- /dev/null +++ b/lib/pleroma/earmark_renderer.ex @@ -0,0 +1,256 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +# +# This file is derived from Earmark, under the following copyright: +# Copyright © 2014 Dave Thomas, The Pragmatic Programmers +# SPDX-License-Identifier: Apache-2.0 +# Upstream: https://github.com/pragdave/earmark/blob/master/lib/earmark/html_renderer.ex +defmodule Pleroma.EarmarkRenderer do + @moduledoc false + + alias Earmark.Block + alias Earmark.Context + alias Earmark.HtmlRenderer + alias Earmark.Options + + import Earmark.Inline, only: [convert: 3] + import Earmark.Helpers.HtmlHelpers + import Earmark.Message, only: [add_messages_from: 2, get_messages: 1, set_messages: 2] + import Earmark.Context, only: [append: 2, set_value: 2] + import Earmark.Options, only: [get_mapper: 1] + + @doc false + def render(blocks, %Context{options: %Options{}} = context) do + messages = get_messages(context) + + {contexts, html} = + get_mapper(context.options).( + blocks, + &render_block(&1, put_in(context.options.messages, [])) + ) + |> Enum.unzip() + + all_messages = + contexts + |> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end) + + {put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()} + end + + ############# + # Paragraph # + ############# + defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do + lines = convert(lines, lnb, context) + add_attrs(lines, "

#{lines.value}

", attrs, [], lnb) + end + + ######## + # Html # + ######## + defp render_block(%Block.Html{html: html}, context) do + {context, html} + end + + defp render_block(%Block.HtmlComment{lines: lines}, context) do + {context, lines} + end + + defp render_block(%Block.HtmlOneline{html: html}, context) do + {context, html} + end + + ######### + # Ruler # + ######### + defp render_block(%Block.Ruler{lnb: lnb, attrs: attrs}, context) do + add_attrs(context, "
", attrs, [], lnb) + end + + ########### + # Heading # + ########### + defp render_block( + %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs}, + context + ) do + converted = convert(content, lnb, context) + html = "#{converted.value}" + add_attrs(converted, html, attrs, [], lnb) + end + + ############## + # Blockquote # + ############## + + defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do + {context1, body} = render(blocks, context) + html = "
#{body}
" + add_attrs(context1, html, attrs, [], lnb) + end + + ######### + # Table # + ######### + + defp render_block( + %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs}, + context + ) do + {context1, html} = add_attrs(context, "", attrs, [], lnb) + context2 = set_value(context1, html) + + context3 = + if header do + append(add_trs(append(context2, ""), [header], "th", aligns, lnb), "") + else + # Maybe an error, needed append(context, html) + context2 + end + + context4 = append(add_trs(append(context3, ""), rows, "td", aligns, lnb), "") + + {context4, [context4.value, "
"]} + end + + ######## + # Code # + ######## + + defp render_block( + %Block.Code{lnb: lnb, language: language, attrs: attrs} = block, + %Context{options: options} = context + ) do + class = + if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: "" + + tag = ~s[
]
+    lines = options.render_code.(block)
+    html = ~s[#{tag}#{lines}
] + add_attrs(context, html, attrs, [], lnb) + end + + ######### + # Lists # + ######### + + defp render_block( + %Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start}, + context + ) do + {context1, content} = render(items, context) + html = "<#{type}#{start}>#{content}" + add_attrs(context1, html, attrs, [], lnb) + end + + # format a single paragraph list item, and remove the para tags + defp render_block( + %Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs}, + context + ) + when length(blocks) == 1 do + {context1, content} = render(blocks, context) + content = Regex.replace(~r{}, content, "") + html = "
  • #{content}
  • " + add_attrs(context1, html, attrs, [], lnb) + end + + # format a spaced list item + defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do + {context1, content} = render(blocks, context) + html = "
  • #{content}
  • " + add_attrs(context1, html, attrs, [], lnb) + end + + ################## + # Footnote Block # + ################## + + defp render_block(%Block.FnList{blocks: footnotes}, context) do + items = + Enum.map(footnotes, fn note -> + blocks = append_footnote_link(note) + %Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks} + end) + + {context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context) + {context1, Enum.join([~s[
    ], "
    ", html, "
    "])} + end + + ####################################### + # Isolated IALs are rendered as paras # + ####################################### + + defp render_block(%Block.Ial{verbatim: verbatim}, context) do + {context, "

    {:#{verbatim}}

    "} + end + + #################### + # IDDef is ignored # + #################### + + defp render_block(%Block.IdDef{}, context), do: {context, ""} + + ##################################### + # And here are the inline renderers # + ##################################### + + defdelegate br, to: HtmlRenderer + defdelegate codespan(text), to: HtmlRenderer + defdelegate em(text), to: HtmlRenderer + defdelegate strong(text), to: HtmlRenderer + defdelegate strikethrough(text), to: HtmlRenderer + + defdelegate link(url, text), to: HtmlRenderer + defdelegate link(url, text, title), to: HtmlRenderer + + defdelegate image(path, alt, title), to: HtmlRenderer + + defdelegate footnote_link(ref, backref, number), to: HtmlRenderer + + # Table rows + defp add_trs(context, rows, tag, aligns, lnb) do + numbered_rows = + rows + |> Enum.zip(Stream.iterate(lnb, &(&1 + 1))) + + numbered_rows + |> Enum.reduce(context, fn {row, lnb}, ctx -> + append(add_tds(append(ctx, ""), row, tag, aligns, lnb), "") + end) + end + + defp add_tds(context, row, tag, aligns, lnb) do + Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb)) + end + + defp add_td_fn(row, tag, aligns, lnb) do + fn n, ctx -> + style = + case Enum.at(aligns, n - 1, :default) do + :default -> "" + align -> " style=\"text-align: #{align}\"" + end + + col = Enum.at(row, n - 1) + converted = convert(col, lnb, set_messages(ctx, [])) + append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}") + end + end + + ############################### + # Append Footnote Return Link # + ############################### + + defdelegate append_footnote_link(note), to: HtmlRenderer + defdelegate append_footnote_link(note, fnlink), to: HtmlRenderer + + defdelegate render_code(lines), to: HtmlRenderer + + defp code_classes(language, prefix) do + ["" | String.split(prefix || "")] + |> Enum.map(fn pfx -> "#{pfx}#{language}" end) + |> Enum.join(" ") + end +end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 348fdedf1..635e7cd38 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -331,7 +331,7 @@ def format_input(text, "text/html", options) do def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) - |> Earmark.as_html!() + |> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |> Formatter.linkify(options) |> Formatter.html_escape("text/html") end diff --git a/mix.exs b/mix.exs index bb86c38d0..dd598345c 100644 --- a/mix.exs +++ b/mix.exs @@ -126,7 +126,7 @@ defp deps do {:ex_aws_s3, "~> 2.0"}, {:sweet_xml, "~> 0.6.6"}, {:earmark, "~> 1.3"}, - {:bbcode, "~> 0.1.1"}, + {:bbcode_pleroma, "~> 0.2.0"}, {:ex_machina, "~> 2.3", only: :test}, {:credo, "~> 1.1.0", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.3", only: :test}, diff --git a/mix.lock b/mix.lock index c8b30a6f9..1b4fbc927 100644 --- a/mix.lock +++ b/mix.lock @@ -3,10 +3,11 @@ "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]}, "base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "3b29948de2013d3f93aa898c884a9dff847e7aec75d9d6d8c1dc4c61c2716c42"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, - "bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5a981b98ac7d366a9b6bf40eac389aaf4d6e623c631e6b6f8a6b571efaafd338"}, + "bbcode": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/bbcode.git", "f2d267675e9a7e1ad1ea9beb4cc23382762b66c2", [ref: "v0.2.0"]}, + "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "aef93694067a43697ae0531727e097754a9e992a1e7946296f5969d6dd9ac986"}, + "cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "738d0e17a93c2ccfe4ddc707bdc8e672e9074c8569498483feb1c4530fb91b2b"}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, @@ -110,4 +111,3 @@ "web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, } - diff --git a/test/earmark_renderer_test.ex b/test/earmark_renderer_test.ex new file mode 100644 index 000000000..220d97d16 --- /dev/null +++ b/test/earmark_renderer_test.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.EarmarkRendererTest do + use ExUnit.Case + + test "Paragraph" do + code = ~s[Hello\n\nWorld!] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

    Hello

    World!

    " + end + + test "raw HTML" do + code = ~s[OwO] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

    #{code}

    " + end + + test "rulers" do + code = ~s[before\n\n-----\n\nafter] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

    before


    after

    " + end + + test "headings" do + code = ~s[# h1\n## h2\n### h3\n] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

    h1

    h2

    h3

    ] + end + + test "blockquote" do + code = ~s[> whoms't are you quoting?] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

    whoms’t are you quoting?

    " + end + + test "code" do + code = ~s[`mix`] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

    mix

    ] + + code = ~s[``mix``] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

    mix

    ] + + code = ~s[```\nputs "Hello World"\n```] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[
    puts "Hello World"
    ] + end + + test "lists" do + code = ~s[- one\n- two\n- three\n- four] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "
    • one
    • two
    • three
    • four
    " + + code = ~s[1. one\n2. two\n3. three\n4. four\n] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "
    1. one
    2. two
    3. three
    4. four
    " + end + + test "delegated renderers" do + code = ~s[a
    b] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

    #{code}

    " + + code = ~s[*aaaa~*] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

    aaaa~

    ] + + code = ~s[**aaaa~**] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

    aaaa~

    ] + + # strikethrought + code = ~s[aaaa~] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

    aaaa~

    ] + end +end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index b380d10d8..45fc94522 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -89,8 +89,8 @@ test "works for bare text/html" do assert output == expected - text = "

    hello world!

    \n\n

    second paragraph

    " - expected = "

    hello world!

    \n\n

    second paragraph

    " + text = "

    hello world!


    \n

    second paragraph

    " + expected = "

    hello world!


    \n

    second paragraph

    " {output, [], []} = Utils.format_input(text, "text/html") @@ -99,14 +99,14 @@ test "works for bare text/html" do test "works for bare text/markdown" do text = "**hello world**" - expected = "

    hello world

    \n" + expected = "

    hello world

    " {output, [], []} = Utils.format_input(text, "text/markdown") assert output == expected text = "**hello world**\n\n*another paragraph*" - expected = "

    hello world

    \n

    another paragraph

    \n" + expected = "

    hello world

    another paragraph

    " {output, [], []} = Utils.format_input(text, "text/markdown") @@ -118,7 +118,7 @@ test "works for bare text/markdown" do by someone """ - expected = "

    cool quote

    \n
    \n

    by someone

    \n" + expected = "

    cool quote

    by someone

    " {output, [], []} = Utils.format_input(text, "text/markdown") @@ -134,7 +134,7 @@ test "works for bare text/bbcode" do assert output == expected text = "[b]hello world![/b]\n\nsecond paragraph!" - expected = "hello world!
    \n
    \nsecond paragraph!" + expected = "hello world!

    second paragraph!" {output, [], []} = Utils.format_input(text, "text/bbcode") @@ -143,7 +143,7 @@ test "works for bare text/bbcode" do text = "[b]hello world![/b]\n\nsecond paragraph!" expected = - "hello world!
    \n
    \n<strong>second paragraph!</strong>" + "hello world!

    <strong>second paragraph!</strong>" {output, [], []} = Utils.format_input(text, "text/bbcode") @@ -156,16 +156,14 @@ test "works for text/markdown with mentions" do text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" - expected = - ~s(

    hello world

    \n

    another @user__test and @user__test google.com paragraph

    \n) - {output, _, _} = Utils.format_input(text, "text/markdown") - assert output == expected + assert output == + ~s(

    hello world

    another @user__test and @user__test google.com paragraph

    ) end end From fffc382f138442035337e55eb930324d13bbdca8 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 13 Mar 2020 19:30:42 +0400 Subject: [PATCH 27/79] Fix hashtags WebSocket streaming --- lib/pleroma/activity/ir/topics.ex | 2 +- test/activity/ir/topics_test.exs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex index 4acc1a3e0..9e65bedad 100644 --- a/lib/pleroma/activity/ir/topics.ex +++ b/lib/pleroma/activity/ir/topics.ex @@ -39,7 +39,7 @@ defp visibility_tags(object, activity) do end end - defp item_creation_tags(tags, %{data: %{"type" => "Create"}} = object, activity) do + defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) end diff --git a/test/activity/ir/topics_test.exs b/test/activity/ir/topics_test.exs index e75f83586..44aec1e19 100644 --- a/test/activity/ir/topics_test.exs +++ b/test/activity/ir/topics_test.exs @@ -59,8 +59,8 @@ test "non-local action does not produce public:local topic", %{activity: activit describe "public visibility create events" do setup do activity = %Activity{ - object: %Object{data: %{"type" => "Create", "attachment" => []}}, - data: %{"to" => [Pleroma.Constants.as_public()]} + object: %Object{data: %{"attachment" => []}}, + data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]} } {:ok, activity: activity} @@ -98,8 +98,8 @@ test "only converts strinngs to hash tags", %{ describe "public visibility create events with attachments" do setup do activity = %Activity{ - object: %Object{data: %{"type" => "Create", "attachment" => ["foo"]}}, - data: %{"to" => [Pleroma.Constants.as_public()]} + object: %Object{data: %{"attachment" => ["foo"]}}, + data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]} } {:ok, activity: activity} From ad31d0726ac1aabfb97ed9746591e315420f17bb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 11:30:27 -0500 Subject: [PATCH 28/79] Do not trust remote Cache-Control headers for mediaproxy --- lib/pleroma/reverse_proxy/reverse_proxy.ex | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index a281a00dc..8db3f78bb 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -7,7 +7,7 @@ defmodule Pleroma.ReverseProxy do @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ ~w(if-unmodified-since if-none-match if-range range) - @resp_cache_headers ~w(etag date last-modified cache-control) + @resp_cache_headers ~w(etag date last-modified) @keep_resp_headers @resp_cache_headers ++ ~w(content-type content-disposition content-encoding content-range) ++ ~w(accept-ranges vary) @@ -34,9 +34,6 @@ defmodule Pleroma.ReverseProxy do * request: `#{inspect(@keep_req_headers)}` * response: `#{inspect(@keep_resp_headers)}` - If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be - set to `#{inspect(@default_cache_control_header)}`. - Options: * `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP @@ -297,16 +294,12 @@ defp build_resp_headers(headers, opts) do defp build_resp_cache_headers(headers, _opts) do has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) - has_cache_control? = List.keymember?(headers, "cache-control", 0) cond do - has_cache? && has_cache_control? -> - headers - has_cache? -> # There's caching header present but no cache-control -- we need to explicitely override it # to public as Plug defaults to "max-age=0, private, must-revalidate" - List.keystore(headers, "cache-control", 0, {"cache-control", "public"}) + List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header}) true -> List.keystore( From e04e16bbc05b035c11b83d5134436d791c512421 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 11:31:55 -0500 Subject: [PATCH 29/79] Do not strip Cache-Control headers from media. Trust the Pleroma backend. --- installation/pleroma.nginx | 2 -- 1 file changed, 2 deletions(-) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index 7f48b614b..688be3e71 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -90,8 +90,6 @@ server { proxy_ignore_client_abort on; proxy_buffering on; chunked_transfer_encoding on; - proxy_ignore_headers Cache-Control; - proxy_hide_header Cache-Control; proxy_pass http://127.0.0.1:4000; } } From c62195127d93761703954af97e328675ee853805 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 11:46:40 -0500 Subject: [PATCH 30/79] Update comment to reflect what the code is actually doing --- lib/pleroma/reverse_proxy/reverse_proxy.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 8db3f78bb..072a3d263 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -297,8 +297,8 @@ defp build_resp_cache_headers(headers, _opts) do cond do has_cache? -> - # There's caching header present but no cache-control -- we need to explicitely override it - # to public as Plug defaults to "max-age=0, private, must-revalidate" + # There's caching header present but no cache-control -- we need to set our own + # as Plug defaults to "max-age=0, private, must-revalidate" List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header}) true -> From 413177c8f0e4b15eb085c4efa26c94d572ee8d88 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 12:02:58 -0500 Subject: [PATCH 31/79] Set correct Cache-Control header for local media --- lib/pleroma/plugs/uploaded_media.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index f372829a2..57097baae 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -14,6 +14,8 @@ defmodule Pleroma.Plugs.UploadedMedia do # no slashes @path "media" + @default_cache_control_header "public max-age=86400 must-revalidate" + def init(_opts) do static_plug_opts = [] @@ -58,6 +60,10 @@ defp get_media(conn, {:static_dir, directory}, _, opts) do Map.get(opts, :static_plug_opts) |> Map.put(:at, [@path]) |> Map.put(:from, directory) + |> Map.put(:cache_control_for_etags, @default_cache_control_header) + |> Map.put(:headers, %{ + "cache-control" => @default_cache_control_header + }) conn = Plug.Static.call(conn, static_opts) From 470090471dabdd3863b9082f1c7aba6c84e7e703 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 12:20:33 -0500 Subject: [PATCH 32/79] Fix test to use new cache-control settings --- test/reverse_proxy_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy_test.exs index 18d70862c..f1690ade9 100644 --- a/test/reverse_proxy_test.exs +++ b/test/reverse_proxy_test.exs @@ -294,7 +294,7 @@ test "add cache-control", %{conn: conn} do |> expect(:stream_body, fn _ -> :done end) conn = ReverseProxy.call(conn, "/cache") - assert {"cache-control", "public"} in conn.resp_headers + assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers end end From db36b48180fda3b0632a5088e45fb0dbf42952c1 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 12:23:14 -0500 Subject: [PATCH 33/79] Remove test verifying we preserve cache-control headers; we don't --- test/reverse_proxy_test.exs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy_test.exs index f1690ade9..87c6aca4e 100644 --- a/test/reverse_proxy_test.exs +++ b/test/reverse_proxy_test.exs @@ -275,17 +275,6 @@ test "returns 400 on non GET, HEAD requests", %{conn: conn} do end describe "cache resp headers" do - test "returns headers", %{conn: conn} do - ClientMock - |> expect(:request, fn :get, "/cache/" <> ttl, _, _, _ -> - {:ok, 200, [{"cache-control", "public, max-age=" <> ttl}], %{}} - end) - |> expect(:stream_body, fn _ -> :done end) - - conn = ReverseProxy.call(conn, "/cache/10") - assert {"cache-control", "public, max-age=10"} in conn.resp_headers - end - test "add cache-control", %{conn: conn} do ClientMock |> expect(:request, fn :get, "/cache", _, _, _ -> From 3b1b183b42019adc9d09b0c1af703b25e313167d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 12:27:50 -0500 Subject: [PATCH 34/79] Synchronize cache-control header for local media with the mediaproxy --- lib/pleroma/plugs/uploaded_media.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index 57097baae..74427709d 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Plugs.UploadedMedia do # no slashes @path "media" - @default_cache_control_header "public max-age=86400 must-revalidate" + @default_cache_control_header "public, max-age=1209600" def init(_opts) do static_plug_opts = From 7321429a2ea134d7d920d8c977c4ec7bdcafc5e1 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Mar 2020 12:42:06 -0500 Subject: [PATCH 35/79] Lint --- lib/pleroma/reverse_proxy/reverse_proxy.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 072a3d263..8b713b8f4 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -299,7 +299,12 @@ defp build_resp_cache_headers(headers, _opts) do has_cache? -> # There's caching header present but no cache-control -- we need to set our own # as Plug defaults to "max-age=0, private, must-revalidate" - List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header}) + List.keystore( + headers, + "cache-control", + 0, + {"cache-control", @default_cache_control_header} + ) true -> List.keystore( From fc4496d4fa45b0389f8476b2c2ee00d647a1dfbe Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 13 Mar 2020 21:15:42 +0300 Subject: [PATCH 36/79] rate limiter: disable based on if remote ip was found, not on if the plug was enabled The current rate limiter disable logic won't trigger when the remote ip is not forwarded, only when the remoteip plug is not enabled, which is not the case on most instances since it's enabled by default. This changes the behavior to warn and disable when the remote ip was not forwarded, even if the RemoteIP plug is enabled. Also closes #1620 --- config/test.exs | 2 + .../plugs/rate_limiter/rate_limiter.ex | 27 +++--- lib/pleroma/plugs/remote_ip.ex | 7 +- test/plugs/rate_limiter_test.exs | 84 +++++++------------ .../controllers/account_controller_test.exs | 4 - 5 files changed, 55 insertions(+), 69 deletions(-) diff --git a/config/test.exs b/config/test.exs index a17886265..b8ea63c94 100644 --- a/config/test.exs +++ b/config/test.exs @@ -92,6 +92,8 @@ config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true +config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex index c3f6351c8..1529da717 100644 --- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex @@ -78,7 +78,7 @@ def init(plug_opts) do end def call(conn, plug_opts) do - if disabled?() do + if disabled?(conn) do handle_disabled(conn) else action_settings = action_settings(plug_opts) @@ -87,9 +87,9 @@ def call(conn, plug_opts) do end defp handle_disabled(conn) do - if Config.get(:env) == :prod do - Logger.warn("Rate limiter is disabled for localhost/socket") - end + Logger.warn( + "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter." + ) conn end @@ -109,16 +109,21 @@ defp handle(conn, action_settings) do end end - def disabled? do + def disabled?(conn) do localhost_or_socket = - Config.get([Pleroma.Web.Endpoint, :http, :ip]) - |> Tuple.to_list() - |> Enum.join(".") - |> String.match?(~r/^local|^127.0.0.1/) + case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do + {127, 0, 0, 1} -> true + {0, 0, 0, 0, 0, 0, 0, 1} -> true + {:local, _} -> true + _ -> false + end - remote_ip_disabled = not Config.get([Pleroma.Plugs.RemoteIp, :enabled]) + remote_ip_not_found = + if Map.has_key?(conn.assigns, :remote_ip_found), + do: !conn.assigns.remote_ip_found, + else: false - localhost_or_socket and remote_ip_disabled + localhost_or_socket and remote_ip_not_found end @inspect_bucket_not_found {:error, :not_found} diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 2eca4f8f6..0ac9050d0 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -7,6 +7,8 @@ defmodule Pleroma.Plugs.RemoteIp do This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. """ + import Plug.Conn + @behaviour Plug @headers ~w[ @@ -26,11 +28,12 @@ defmodule Pleroma.Plugs.RemoteIp do def init(_), do: nil - def call(conn, _) do + def call(%{remote_ip: original_remote_ip} = conn, _) do config = Pleroma.Config.get(__MODULE__, []) if Keyword.get(config, :enabled, false) do - RemoteIp.call(conn, remote_ip_opts(config)) + %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config)) + assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) else conn end diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs index 8023271e4..81e2009c8 100644 --- a/test/plugs/rate_limiter_test.exs +++ b/test/plugs/rate_limiter_test.exs @@ -3,8 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.RateLimiterTest do - use ExUnit.Case, async: true - use Plug.Test + use Pleroma.Web.ConnCase alias Pleroma.Config alias Pleroma.Plugs.RateLimiter @@ -36,63 +35,44 @@ test "config is required for plug to work" do |> RateLimiter.init() |> RateLimiter.action_settings() end + end - test "it is disabled for localhost" do - Config.put([:rate_limit, @limiter_name], {1, 1}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1}) - Config.put([Pleroma.Plugs.RemoteIp, :enabled], false) + test "it is disabled if it remote ip plug is enabled but no remote ip is found" do + Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1}) + assert RateLimiter.disabled?(Plug.Conn.assign(build_conn(), :remote_ip_found, false)) + end - assert RateLimiter.disabled?() == true - end + test "it restricts based on config values" do + limiter_name = :test_plug_opts + scale = 80 + limit = 5 - test "it is disabled for socket" do - Config.put([:rate_limit, @limiter_name], {1, 1}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {:local, "/path/to/pleroma.sock"}) - Config.put([Pleroma.Plugs.RemoteIp, :enabled], false) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + Config.put([:rate_limit, limiter_name], {scale, limit}) - assert RateLimiter.disabled?() == true - end - - test "it is enabled for socket when remote ip is enabled" do - Config.put([:rate_limit, @limiter_name], {1, 1}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {:local, "/path/to/pleroma.sock"}) - Config.put([Pleroma.Plugs.RemoteIp, :enabled], true) - - assert RateLimiter.disabled?() == false - end - - test "it restricts based on config values" do - limiter_name = :test_plug_opts - scale = 80 - limit = 5 - - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - Config.put([:rate_limit, limiter_name], {scale, limit}) - - plug_opts = RateLimiter.init(name: limiter_name) - conn = conn(:get, "/") - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - Process.sleep(10) - end + plug_opts = RateLimiter.init(name: limiter_name) + conn = conn(:get, "/") + for i <- 1..5 do conn = RateLimiter.call(conn, plug_opts) - assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - Process.sleep(50) - - conn = conn(:get, "/") - - conn = RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - - refute conn.status == Plug.Conn.Status.code(:too_many_requests) - refute conn.resp_body - refute conn.halted + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + Process.sleep(10) end + + conn = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + Process.sleep(50) + + conn = conn(:get, "/") + + conn = RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + + refute conn.status == Plug.Conn.Status.code(:too_many_requests) + refute conn.resp_body + refute conn.halted end describe "options" do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 7f7d8cea3..7efccd9c4 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -756,10 +756,6 @@ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_ end describe "create account by app / rate limit" do - clear_config([Pleroma.Plugs.RemoteIp, :enabled]) do - Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], true) - end - clear_config([:rate_limit, :app_account_creation]) do Pleroma.Config.put([:rate_limit, :app_account_creation], {10_000, 2}) end From 2966377cb96b88948c1ceae742f70784ca6f5aee Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Mar 2020 15:12:29 -0500 Subject: [PATCH 37/79] Update AdminFE --- ...5.edcdbe30.css => chunk-0d8f.650c8e81.css} | Bin ...b.0f1ee211.css => chunk-136a.3936457d.css} | Bin ...a.dc3643e6.css => chunk-15fa.5a5f973d.css} | Bin ...f.6dd5bbb7.css => chunk-46cf.a43e9415.css} | Bin ...a.bbab87da.css => chunk-46ef.d45db7be.css} | Bin ...a.a8b5ee5b.css => chunk-4e7d.7aace723.css} | Bin 5332 -> 5332 bytes ...6.ad5e9ff3.css => chunk-4ffb.dd09fe2e.css} | Bin ...6.85f319f7.css => chunk-876c.90dffac4.css} | Bin ...0.49362218.css => chunk-87b3.2affd602.css} | Bin 9292 -> 9407 bytes ...8.80435fa1.css => chunk-cf57.4d39576f.css} | Bin 3143 -> 3221 bytes ...d.802cfba1.css => chunk-e5cf.cba3ae06.css} | Bin priv/static/adminfe/index.html | 2 +- priv/static/adminfe/static/js/app.55df3157.js | Bin 179675 -> 0 bytes .../adminfe/static/js/app.55df3157.js.map | Bin 398361 -> 0 bytes priv/static/adminfe/static/js/app.d2c3c6b3.js | Bin 0 -> 181998 bytes .../adminfe/static/js/app.d2c3c6b3.js.map | Bin 0 -> 403968 bytes .../adminfe/static/js/chunk-03b0.7a203856.js | Bin 100666 -> 0 bytes .../static/js/chunk-03b0.7a203856.js.map | Bin 348763 -> 0 bytes ...7a5.13b13757.js => chunk-0d8f.a85e3222.js} | Bin 33538 -> 33538 bytes ...3757.js.map => chunk-0d8f.a85e3222.js.map} | Bin 116201 -> 116201 bytes ...b8b.e3daf966.js => chunk-136a.142aa42a.js} | Bin 19553 -> 19553 bytes ...f966.js.map => chunk-136a.142aa42a.js.map} | Bin 69090 -> 69090 bytes ...5fa.15303f3a.js => chunk-15fa.34070731.js} | Bin 7919 -> 7919 bytes ...3f3a.js.map => chunk-15fa.34070731.js.map} | Bin 17438 -> 17438 bytes .../adminfe/static/js/chunk-293a.a728de01.js | Bin 23332 -> 0 bytes .../static/js/chunk-293a.a728de01.js.map | Bin 80400 -> 0 bytes ...6cf.104380a9.js => chunk-46cf.3bd3567a.js} | Bin 9526 -> 9526 bytes ...80a9.js.map => chunk-46cf.3bd3567a.js.map} | Bin 40123 -> 40123 bytes ...53a.2fcd7192.js => chunk-46ef.215af110.js} | Bin 7765 -> 7765 bytes ...7192.js.map => chunk-46ef.215af110.js.map} | Bin 26170 -> 26170 bytes .../adminfe/static/js/chunk-4e7d.a40ad735.js | Bin 0 -> 23331 bytes .../static/js/chunk-4e7d.a40ad735.js.map | Bin 0 -> 80396 bytes ...e46.d257e435.js => chunk-4ffb.0e8f3772.js} | Bin 2080 -> 2080 bytes ...e435.js.map => chunk-4ffb.0e8f3772.js.map} | Bin 9090 -> 9090 bytes ...dd6.6c139a9c.js => chunk-876c.e4ceccca.js} | Bin 5112 -> 5112 bytes ...9a9c.js.map => chunk-876c.e4ceccca.js.map} | Bin 19744 -> 19744 bytes .../adminfe/static/js/chunk-87b3.4704cadf.js | Bin 0 -> 103161 bytes .../static/js/chunk-87b3.4704cadf.js.map | Bin 0 -> 358274 bytes .../adminfe/static/js/chunk-cf57.42b96339.js | Bin 0 -> 29100 bytes .../static/js/chunk-cf57.42b96339.js.map | Bin 0 -> 88026 bytes .../adminfe/static/js/chunk-cf58.e52693b3.js | Bin 27673 -> 0 bytes .../static/js/chunk-cf58.e52693b3.js.map | Bin 84422 -> 0 bytes ...60d.a8bb8682.js => chunk-e5cf.501d7902.js} | Bin 24234 -> 24234 bytes ...8682.js.map => chunk-e5cf.501d7902.js.map} | Bin 92386 -> 92386 bytes .../adminfe/static/js/runtime.ae93ea9f.js | Bin 3969 -> 0 bytes .../adminfe/static/js/runtime.fa19e5d1.js | Bin 0 -> 3969 bytes ...e93ea9f.js.map => runtime.fa19e5d1.js.map} | Bin 16759 -> 16759 bytes 47 files changed, 1 insertion(+), 1 deletion(-) rename priv/static/adminfe/{chunk-17a5.edcdbe30.css => chunk-0d8f.650c8e81.css} (100%) rename priv/static/adminfe/{chunk-2b8b.0f1ee211.css => chunk-136a.3936457d.css} (100%) rename priv/static/adminfe/{chunk-15fa.dc3643e6.css => chunk-15fa.5a5f973d.css} (100%) rename priv/static/adminfe/{chunk-46cf.6dd5bbb7.css => chunk-46cf.a43e9415.css} (100%) rename priv/static/adminfe/{chunk-453a.bbab87da.css => chunk-46ef.d45db7be.css} (100%) rename priv/static/adminfe/{chunk-293a.a8b5ee5b.css => chunk-4e7d.7aace723.css} (57%) rename priv/static/adminfe/{chunk-4e46.ad5e9ff3.css => chunk-4ffb.dd09fe2e.css} (100%) rename priv/static/adminfe/{chunk-6dd6.85f319f7.css => chunk-876c.90dffac4.css} (100%) rename priv/static/adminfe/{chunk-03b0.49362218.css => chunk-87b3.2affd602.css} (57%) rename priv/static/adminfe/{chunk-cf58.80435fa1.css => chunk-cf57.4d39576f.css} (75%) rename priv/static/adminfe/{chunk-560d.802cfba1.css => chunk-e5cf.cba3ae06.css} (100%) delete mode 100644 priv/static/adminfe/static/js/app.55df3157.js delete mode 100644 priv/static/adminfe/static/js/app.55df3157.js.map create mode 100644 priv/static/adminfe/static/js/app.d2c3c6b3.js create mode 100644 priv/static/adminfe/static/js/app.d2c3c6b3.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-03b0.7a203856.js delete mode 100644 priv/static/adminfe/static/js/chunk-03b0.7a203856.js.map rename priv/static/adminfe/static/js/{chunk-17a5.13b13757.js => chunk-0d8f.a85e3222.js} (99%) rename priv/static/adminfe/static/js/{chunk-17a5.13b13757.js.map => chunk-0d8f.a85e3222.js.map} (99%) rename priv/static/adminfe/static/js/{chunk-2b8b.e3daf966.js => chunk-136a.142aa42a.js} (99%) rename priv/static/adminfe/static/js/{chunk-2b8b.e3daf966.js.map => chunk-136a.142aa42a.js.map} (99%) rename priv/static/adminfe/static/js/{chunk-15fa.15303f3a.js => chunk-15fa.34070731.js} (99%) rename priv/static/adminfe/static/js/{chunk-15fa.15303f3a.js.map => chunk-15fa.34070731.js.map} (99%) delete mode 100644 priv/static/adminfe/static/js/chunk-293a.a728de01.js delete mode 100644 priv/static/adminfe/static/js/chunk-293a.a728de01.js.map rename priv/static/adminfe/static/js/{chunk-46cf.104380a9.js => chunk-46cf.3bd3567a.js} (99%) rename priv/static/adminfe/static/js/{chunk-46cf.104380a9.js.map => chunk-46cf.3bd3567a.js.map} (99%) rename priv/static/adminfe/static/js/{chunk-453a.2fcd7192.js => chunk-46ef.215af110.js} (98%) rename priv/static/adminfe/static/js/{chunk-453a.2fcd7192.js.map => chunk-46ef.215af110.js.map} (99%) create mode 100644 priv/static/adminfe/static/js/chunk-4e7d.a40ad735.js create mode 100644 priv/static/adminfe/static/js/chunk-4e7d.a40ad735.js.map rename priv/static/adminfe/static/js/{chunk-4e46.d257e435.js => chunk-4ffb.0e8f3772.js} (85%) rename priv/static/adminfe/static/js/{chunk-4e46.d257e435.js.map => chunk-4ffb.0e8f3772.js.map} (98%) rename priv/static/adminfe/static/js/{chunk-6dd6.6c139a9c.js => chunk-876c.e4ceccca.js} (97%) rename priv/static/adminfe/static/js/{chunk-6dd6.6c139a9c.js.map => chunk-876c.e4ceccca.js.map} (99%) create mode 100644 priv/static/adminfe/static/js/chunk-87b3.4704cadf.js create mode 100644 priv/static/adminfe/static/js/chunk-87b3.4704cadf.js.map create mode 100644 priv/static/adminfe/static/js/chunk-cf57.42b96339.js create mode 100644 priv/static/adminfe/static/js/chunk-cf57.42b96339.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-cf58.e52693b3.js delete mode 100644 priv/static/adminfe/static/js/chunk-cf58.e52693b3.js.map rename priv/static/adminfe/static/js/{chunk-560d.a8bb8682.js => chunk-e5cf.501d7902.js} (99%) rename priv/static/adminfe/static/js/{chunk-560d.a8bb8682.js.map => chunk-e5cf.501d7902.js.map} (99%) delete mode 100644 priv/static/adminfe/static/js/runtime.ae93ea9f.js create mode 100644 priv/static/adminfe/static/js/runtime.fa19e5d1.js rename priv/static/adminfe/static/js/{runtime.ae93ea9f.js.map => runtime.fa19e5d1.js.map} (90%) diff --git a/priv/static/adminfe/chunk-17a5.edcdbe30.css b/priv/static/adminfe/chunk-0d8f.650c8e81.css similarity index 100% rename from priv/static/adminfe/chunk-17a5.edcdbe30.css rename to priv/static/adminfe/chunk-0d8f.650c8e81.css diff --git a/priv/static/adminfe/chunk-2b8b.0f1ee211.css b/priv/static/adminfe/chunk-136a.3936457d.css similarity index 100% rename from priv/static/adminfe/chunk-2b8b.0f1ee211.css rename to priv/static/adminfe/chunk-136a.3936457d.css diff --git a/priv/static/adminfe/chunk-15fa.dc3643e6.css b/priv/static/adminfe/chunk-15fa.5a5f973d.css similarity index 100% rename from priv/static/adminfe/chunk-15fa.dc3643e6.css rename to priv/static/adminfe/chunk-15fa.5a5f973d.css diff --git a/priv/static/adminfe/chunk-46cf.6dd5bbb7.css b/priv/static/adminfe/chunk-46cf.a43e9415.css similarity index 100% rename from priv/static/adminfe/chunk-46cf.6dd5bbb7.css rename to priv/static/adminfe/chunk-46cf.a43e9415.css diff --git a/priv/static/adminfe/chunk-453a.bbab87da.css b/priv/static/adminfe/chunk-46ef.d45db7be.css similarity index 100% rename from priv/static/adminfe/chunk-453a.bbab87da.css rename to priv/static/adminfe/chunk-46ef.d45db7be.css diff --git a/priv/static/adminfe/chunk-293a.a8b5ee5b.css b/priv/static/adminfe/chunk-4e7d.7aace723.css similarity index 57% rename from priv/static/adminfe/chunk-293a.a8b5ee5b.css rename to priv/static/adminfe/chunk-4e7d.7aace723.css index 924633a801083f1508233e0d69af60d671b65d9e..9a35b64a0052d370d56ef0e6a9d9ba499a473e6c 100644 GIT binary patch delta 651 zcmcbjc|~)>Vs=F{3v)|@WMhk1os87Pl+>c=l*E!m-7;Oc_~ePK=96=oxF_2&noYLh z;NyVFOb+5OMsRvL6cL)K}?~Ww^Bv zR`_u%BRG}FGOM}c5i&wNb_h-cj}C^h+>_7qVs=IIL`$>8G&7S}os87Pl+>c=l*E!m-7;Oc_~ePK=96=oxF_2&noYLh z;NyVFOb+5OMsRvL6cL)K}?~Ww^Bv zR`_u%BRG}FGOM}c5i&wNb_h-cj}C^h+>_7q#?l%bH5n3S4RU7ndzl3`_NV4zyNxlr^alMqZ^HyNZp WGcUDhvVesAuF;#NICQgpzjMU`pr2LA>x7j6`3{5w$Admin FE
    \ No newline at end of file +Admin FE
    \ No newline at end of file diff --git a/priv/static/adminfe/static/js/app.55df3157.js b/priv/static/adminfe/static/js/app.55df3157.js deleted file mode 100644 index d1a37af1c9697b23daabc12ea5def50b18c21dda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 179675 zcmeFa+ix4$zUTL^u!%+=>6oNgypwLV$CmAucgvTy)b8Ew?J;PoWU@_>GD%6c)dJ=v z!3>ZfKoDSnOn|&34|xcXr$LavCue~C7x{dDYptr{MUvfi>zq9^ySGKMYSp^@?zgpS z?Q}5OAD@;^<5x%B-kZNn$D^ao71w_M{rQXa($Vp>zxMojzI$|(e^EHg-{{`2=Q{_- zquy*V9<9ZNSz)w(mOq}xx#?^&=*{vsN2STyZ2jg*carOFj@I(EJMl01^}=LxR2r>y z*9&7^e9*l=ZNL}HmpQZsokjL+-LQ}VX;zel`2)PG)k3bXjZH>8>L#iRV=o-wp;#@I8?|CoF12elu0+);50)BH zk%7wk+h`Ue*QQ!(l(}1K6f1NY>07^8Yn57+=%iIIHEL1cepPx+o^1%KwNg|omg<^# zcqB)cjk@(&&qb9|y~Z3EHm70BZ5y|eV{S}KldI<1+^!Xyr5b(mK&9L&M%*bkbGlz= z){Sx{S7Tn>ZC9K2wNfdSqbOG{3eb#JuQDdk;$gM4+v>lmRcl=!00^{!gSrWWL?0ds zYD}mWm0IPxgL1jnZ`UiOYQ6Q$U*EP?K)G6tO0`Oqi`r2sYPNff!K{H`Gotfq4V(js zW?N9Ml-s#VyHtxbdaGH>wSY#Y7`15K%0+dXE$C}RO+5ut3|=*$1AL2>W~o_g=6IA9 z)>s9@+K+y*(I}OxjT5N4RIBr|0^*^ra(>lH9m1H;U+6}3M+Y}9I{cCJ+hn~{;RpepJCK2^aiS7p6X zt5~Vou(l-n1MbiaAZ&qd-D`mieu0cy8KhOUstU+}PNG_aRnc(~#71@24GIA>d|GBO z`_Ts-)l#M5;o1VO`q^XYH7GB~ll3~3&w>G>$f5;|TFejBw%~sLHjDxUN_eHwjB=1! z#n`+Cz!;~Y<`JN;8L&YYBA^CkgZ&D00W1JbL-g%n0K2fhoa9BT<$r+xGWW|ydr=WQ z^I(|=i;x9JHpo)`e@)?;KuTL&U- zWR&4utO!I!7@C7-)b=u~0YySSq60A40Ej%HU-Z}jb%Lc6r97bTBC=-P|1f|{BGISB z5R(xLBet1JC0!2#16+n}I+<6jAiX3OAb%c%ms(Nd!y#HLpWFEE+YP6szWays^_$aD zx76L=UmL97oR#+DgYNNgwmCR2VF|8=@s%R)q#Ae+(sgm z+ETa`dI60MG#~`pY}Iy`h!nWUM_r*@XgcNT2sLMYS-#O|z`@}~V5_SEE6s5!U}cGcq%2@RUM5F3PEW5(CdcpN;`k^YWu)-{BiHF+!2uEy<*YtTME?zlGI^LB&(8N^{2? zLI!#Z+fqD0VF;X)jWb`jauf|(jz zs}-4b9RpcUHHyqbz4K7L*kkIdUsE`b@~o%opMOZt-6dF^hLE= zeJfUO;6LEMF(6RE?qaB!Ruo}WOX$OQ2w{X+4JNHMiojYeSy>H*AUhTLDbu#a?D>FH z%pe7XEUawfbHaTNGj0K)qFF=@e_N zmct`T2Xk@e)*>%XwBg!0x@k*2N_etv5rJEauoPhzxS{2INcp>I6 zgyzw(<;GqEq{~JYAvYj8G?V*~GsGo-y9!VMUuaTAgF^snGIJfVDC^h8_O}yCslAnh zR+SF)WQbxdmn(ZU+2jnVe%vK-T(xSw)Z0L^TASGwDQ!LBFWM9Sn(NXY@%?@H1o_^& z^YjY&o`koyK0F!c*NJ*5xK-+6u~%C0>nooAVllG#-C$NcI35nCy-B>FB<63e))n6J zUXGl7z74-PwTv-eQ&0iH482s^!y-W<*c`(iqY48AvE4+5!D)C_lDmo%m9Y+x4rM|j zFh#v*jVjh0=?1l^xomWT3#j&T)EJty!4QcEM`5}}62J(|b{FzO$GE?;j}>XEVUCu$ zhS6hx>^fJF_?5;`wk+N!PbmsKy$lPByQHGIo zQmVFk*r-L6o6NXMSzs);Wo$)gHk%Cat`I1oH^5j1f{3W7rA0`p=?m!Si?2Q$GLyQj zX}XFkLoHnGn$@1xtq>lIs8k5MnX9c;3jG}r_A7TZ75xW zLs22Bz|v&fF^>j+Dv>54*R2u_HCxcffLc4+OMe{dSd*MTR<15uD4|wE-W(xNU|m*B z5FL-`WxYdudM?%)09gQLoQN=P8Ar^%M70R2!HJY2ZwnO;Z8%6CZB-m6;YvZvYP&h4 zSALco0*Ljf8wBWVwfbsnZHUG(HUS_pv794DbC7Bcqede}w;(94hVx~#h`-&nJH)^#tgOB{;hCxNk5rl|Q4-JmyKN1H7z>oo!L&MgkSwMCCg#?5$ z_?KBg6hdFUZ$d;(L{xHX2-=H`F)$6kCTt`_YLQ5^U2hn#h)^7_2wI+5AS=&MPz+cm zP5@<9AY(9u=_%$zz(IFDp7zipT{3>4A>(Ey00ef(d>diPa0D;0sA_cxu{bGE1^2l0Oo~JW4T-(0iTSB8vB794 zdTk(y#qowK9xxeLhujZK0NQNJ?Bu#0qO}LJIZ~)N?bE?5}y*FX@A=&O#;W8Zwndx-6Y!gXt8=Q97lG zi!2C&E&)#;2so31P8iUk04azQE|td)+W4EwjjH3ak2fHyPx|++%LEw3(^|X%O1?7w|eNn>hJ2=A}kjIW=15V4Zzx(d)69D3I0{T1- zAt5^O+tt**@7gd@@CYwAAgLdM448^#!hD1b+pXvsc&}Es-Sua>AxjkLjmx(@KZQ@i z{EVg+=GV&1555&2!4SBDC_`wd+2V?#jr1b+s$tVZq@q!^u&-_a0yDZR5@uCSvjGBi zk65?h87>oy)YtbGi6MBsQ71P@|CMLDqll1JYp!vCHEDzZ>#Nq1g|*gVs7hh27)gM2 zb!tBpJOoGnyWxoRna#ma60M$WRF*#s*e!Tot}TQ8Q^{fx>=I}m>yXE^Ej^q=5T)c1b;@wz0!tRElFMe*&zwaWuHjf^J|ZzrjlT&1VjQ{q^LU~loIx!tOIxpx?pnhB~u{ zu~mq0%GyEU5@lDS5^IATQ_1q8#2VCCmH>&=ZS+9Bj`quTDu!fa*C}^e)g8hk%_<2y z%?>Y|EViE=si3ymp5)h>SQ88L^UbU)_xKLV#IJda+@~Is@nYFR#kLj7z+#3MXsB#2 zj)*jwexg8`^A2eMK}0dA2+d1%05@w5S)x_EbaWyydPz53R@#6Z1nM1#qD|pBY!LU2 zzftQ1@2iHNA(>povQ-!aIgAUXEU@G?GLx`ElYFSmU_kHgfB*%&tQBnDAbUz;qzW4Y zzj|B4n*BzoNm?90Mns;mEm##xCNrybDOHZw$6cnn<4A&NK?9>4+|g4@8B+OUj8`%H zTi*oQG9oiNC5ciecXr?Ny>kkuEq25*l(5+BCuQRxO8x zp!sW#jne%rPW|y)Di+~`epI`P`P@on(ONt}hp+$^2JtehsL*Qtw`Ti!3kc<~opQG;smMkgz3b!dlezp>hAAwnCfb{~ho( zG2-Cv!|rmwX|5*#gTo_*OWJtRa!!;iQUwUkn1a@tq^649Ms!jwv=&lz2w<+Y(Cx~) z|l(v}b+tWe#S2T`SNg(VRzmDSe}G;4BItn3B3+Qloa*LcExIC$!5 znS@AVg{!NYw)=4RX=^@pRZck&P$h*&tW^lf`v%VZ#kee2o{=Xbdu)Oiw~HDJW?~Bm z;m}*9ahs_s8Sd*@z!J+MbpaC*A7JbzB8f;T4~6M5eP|5Ra7{U+s;&}S3So8A(we<= zpDas2L2uHsEE%p9Ql;OmvI<3Gp*tYRv+Y)&2s1g#6XXQZ{~hJ50Wsz7thKB=f$&v z%M3gcvPh;Tz+je2{o5)8X0bmE$65q^z#}8TBZ63`G(X_^OgvdY6CRZ*MS0@Tv6O%- z83a8raD+Il!BprP|KVsCx@?I=Dp*T(N~D@f-1yJHL1p+BlZpB~=$GY*D`i%p`zZK> zE-n4YQpGZjA+lA8GACPI&JN)vSXl%HwxKf%YBMz7D{z(A9TIjQHZw-4m=%alN99WF zE7T}j@spdry_$~{rJRV-$c(=e{SL3t1p0XE%4C#yTw zfif1Qpi#KlMKW{&KIn^yHORqVa;DYK;xQD|lt3aX-ec(H7lRqO>tu9B?^FLu9T5o z=se-SN}!vuM1%y1dlDa>V}Lel-D562UCdj z8U%esF+x2Kp#DOVFrMTpQE=T<=EuH6saUF#ggge1NRg_x%iUE6G8>vElN+YZC{eLN zB&rz11T>{&nt|BT9;KnXX)~}6WUOFETEDs(#*j3#UNi$O1jf*9KS4bk)}V)38~vC9 zN!Iz{5Vn&-!n`7o{SiPBnSL@*{Nx=}GeA*vKLAC>;)Os7H?<@W)kUBL0A+!Kgi&?X zrvl|+lW}kcDBf$ghN`TEI8^#Wl7~`1mI0Gfga9Xdn###6SwP+h>Xey)pDWRzme`IUu%*L zLPjkULkW}FHv?E>C>4h&UVw&zNL7E%Y-Ne)VftVtl+>Y53~utHG*ze5^e{FO z9g*j9fWld6P%;M)OtbL3-KCc@Y91;kLW{o!tcc+&i7UHW`2BEbbsq-U8WBrcS(WA- zcPk!q1$UbmFr1~_4TpN}{-T5|vk_MMXhL?}?I);b4W#Np+->1u&)t5Vs(}Nrus0vI zst1agVp-(Xx6sM5C@I$<%R&h`qJ=Ol=?^Htz9_<#0wooB4k+fwn}aOzVDd&Q^CHhN zuvEF>WnSc6FLxi}wjuQz)Pt|4lB^E+FGAeOGY(qON~^GXFzMiKrRFeER3vK|Y$YJ^ zaV2SR{K0Z0H<^lm+P5%9?fW*+Z4(cboreWOD`gHLE=fsRae*hc+z3QEtRR|lDLKLr zNSFzn-%uH`c}@5W2p9ZK1px`9maALXz^?R5dm<2!3T1*>TqVVEc$hGvsNc2((Zq;X zYl&22?1d9XWxc(H=%8dlEJd`St|&5w8h#O!uTM(DZ6)$NB`ui}OO*h!DOr5q1SG*{ zoLh~L*F_T>M8w7*OkOvTY>EKkVUkmoVJ_2K;+GX5ZQ(uOqEwLFN?Wf+#SwJh-P%9J z2pm)=R~mu)YqLVWn=d4r@Yc^J@$7gq%5~XC;Odex_jDoN>`q=EAF?%WS{lZq*R%e2 z)|vneo{P+XeMx|7-T=fSMMmiE=jumAr0{5?g#Aly>Kz4Ntr{Z`nUmoHJP+Uw4^ z*0F0-Htc0Co}U*~ft4BEZAm;Y+qP! zSooA3cKpRwz*r%XqtbeKj(*zO&IO<2KJpFiPh7{sh1gUiEMPR54aq%(j(=3zsfOhO zUn`4HH;X%J*Shc?i_o9oNw#cmEf^9m)l~e4yKl>4HpGuCXor=gpdC>*rchUDLDM3n z+*jYW`OSSK{p0*$xS@}k6N}eh)8uSz(A4miZGv=V6C}WaTtu$u!*#+6W%99lR2aq` z!^^y@Q*xi&(bBm`P?43($fQogpek<%s0uB01xMY7bqsjeHO24_Gv)|45Q2Ib2@W#B zh}NgCL_Q?vg=gq6lk@i=h47^CPZ5b$vVec)uYjQYNN^-j83_PBS8k%6 z2Q1y8-qD2r>mD+~U=#%w>8&Oy?>>@F@hcO=Q`VB;oq8Nw^E}>JjC=t1;rg(6g78kN zjmPoWER>*SyI6zKgci+Rw$(9NDqe+q3w@c@XEeoiMM~X=^$lcNHa}3W8``1iKiqw{ zK4J93p`yHO`K9EDN-{;_6o*0oiUXwK#L5N-;zDRWU}^Cng`DKFh`Spq=uzC={dHqN zcgEjVv}svKaR$^XqEG}Im2cFD*MI`0E2vUdM+5d_#UhDHqgyAtqzRLD0CL^^~ z)_o_|Wx^C1poU+OO=v*#(XMdPFR(r>gnuf&m>YX|fKr(~u~;pD$-ZxsIIa_p((1ez zi-=dU2JJ!AZddv@3%ijb+Ha|fgdAUBy@ z`pPy|b*F+3BqKzFRrNZu0LiE@KQ0p*#`Z+n(Fw{s zF*L16#?X^hDpQ(5Zzd09d+3x0sW5Q_IgTzRBy7Re1{Qs|=TWehMIWo^uwDL0;(+gaOIAMF!RN13f+EKS6QO6F8dtCWl6 zoar~oO!`FHVTrhWj@I4KrYmM3D+1})C3w?>&;3W3Hwuw?+94Ex3OAbBphg)^eGuVR z)Xw75^&MGmNG_dF0Z8F>VO0{*M55E|XaZJgN}BQso^DwsmA(T?^nq|xll&Hl>D&Na zHQ`;hjJ+ZYAWd_K0HPLR`7Ko%NM?g-#D>u*h44a<#aWwe%D&0E^=q?}LdaO@nS<;(TmROkwYyy1PQX-!M!s_FuRokQvbrEK;i^j^A zEsJ$i_5oL@e@!<#fCGyD8cj&x+q$^oGVz+ZlLQ!WK}fk}%K)@05u1*iWBmki({2V> zGC9MdjlF7UFWhW6CxgWEl1BzK3bk#o2Gy`wigqdo^wg}&Aga~mQz@uU?={(Qhy>gh zg{hbz4ba4h1YZ$EGg6q;29#067A|V(z#CA^@*p4K8hT4F(~i11(y| zW@;;H#2{8-(P)3X;>+J}-`Kk{#`A?vP#SO?(qz=tQBB=pHaIbNsMH7%qtFaw72=#g0hEIc(XA3G-Y0LUwhA%B zheWp-c@P=TazqdE2=N33K~(CBeP|GqP8fo}^tFz5vwQBVl!(8IMcAxYPcRXwCxT>o z#Qp~TV1;QAAPyFln%WuSKU@Pq!yXsWX_+EU#JQs6sJb9CRQ8Fa;#$3`trG0O6#zcy z>;*|voH7_42hDw0r*_VQY9HtXtIyyTKGcBzRa8i3=E=blX3lbl=}RRvTU}&>4jJK(8(E&1L}x`FvT9$^ zDDETNZ(G?p_|o<8S2uNt2(cJhV5Uk`R8R#k5RXb?m^eaH{xhDc;`s8ah{3Di42qN) z0ZvJ_iGV2s!863q_8)F=5sQZ%-d$T}pZjpbBZjdJu@I@#z&KFBu>QK6cmc2)lCIXc z4br8-{fG4}VJW)!@DK`D)fXFUqMCP(2;qaV?Zq7L^jMg1i_J-?M@?Mw~)*s?x!gRnA-C3Y(3(lNXP%gwd^O$)MCU_`)GJ zaGzv4=^RW^^$a^Hh=4r7LZkFu*SJYw%&swS3n6H)y7?V!`@&Xs*XYqw+v=Z*laEGl zisx7g5i&YZEw5yy`|f15hIPgC6~v?3NbXBM$)J%3K@}B$EZEbu2qC2f>V{wd?57s4 z_=;U|`VPp=t-xpz60A3+^hx8Jq5}YYAr3Ea>nV zt2(oWA(}fNmjDg|L&a?fSx~LM?0&`KSZtAkGB716n9!zP%o`eMY<5Y33#lh0jdYkj zh$NLsL^E(GLzKyb1IuI!L8jZ#vdA#41knZLwPIpF{L&@jcP^~az=V_B5|;R@Enxy7 zGogxunjIq@wj2O$4Ound>Ki9!s6C=Y-vtL9sg1UkKZBtK+9tmiuLjc){PAf zlsE{1OS0B=i*Tag#5ANdS#+&&0#aJi@cbm^e^Rm^bf!utpukfiA#uAYQp}}#Aox6hE5+u2R*idLv=%AqA-QWWXWqkD{PT>>i z+ZV#>prOWA-+E2f6^fGiV7EeX{DS$E00PGPCizReSgkaaqUMT}=jdP}3{ogr6pmYzSX0wB$?A7&Foa|&Q{t1bl?^9)tJ$hHxt3-vP!GeT3?ld!lq#E%%5@r0{NPe zvAuYJEx0DZ6^Mp^n{a}%>oV1hDs3F`1ZPeO&@eMI;u~N_VeySH1QHAW5c8;vNxXu> z;Wz*+Ymf$ALi5ItEQvKsM8OJD8+?jW3=Hxos_+J_$uv#A;p)i#1U|Gk(&5BN2wYl= z4emw+JkN48yn50^1V)Kb#sxYA&nfNT3x)5MsE_?uuX3UW%qg^s>JZo9EdmcPh*YpW z$o?sYl=*3gAGp2h!kH$-(aGfjL5P4AEMqYVzQS2_!Y`R$ z?k9B5Mg3GWnTo))(Z<*VXv$tf$LtSwK*?Vf2QdQt z$!R52^K( zV~eqBX0}U;h_8fpBhwTuBX=JJb99ZrHGgtI+Jl+NR4158gSNDJ`RQ$wTqQ5XZdpuZ zte%gLj%+{p`RB`Vo*ZNLaJ=<)?ijP}QGD`|&;IoG=aac-A8f@(mp}W(4nTW-JlKzO zl`OgWORFi4^ZkiXewm#tNnhz+mN1<;!b7G|9*W>;4=Wg~IwUZ{N0k?*or~a7VnCRZz@~VK$SB2q zB%SdApm*dk$F6WLAAYq)!(SM3dJ;V#=2!=t?8sNS-&$6=aZ3zGTcN6}N?ZdQ4aC$HU;(a*brUC7 zIDu$5N}Twv+GrmMx6n}p>QQLi@9W&jdW$UxC^u!iYh~@LCEXz+qLHG5`a+O)N%qO? zCI`Na5Tt5xONOEnYLMyP>>Z z_!>3X@=U6a=Lw<93&TS3%{Wp0A%!X^X;uUmS>DK^JgWa8&5RaRjT8{DZ%n~zglM7~ zGUcJ^)%_nR=o~;PbeZCQ>droI%hZ*9Z9O-^VfuGPq~ zZfl}csTn7X%XL3B)<{1#FNeeVxrX^9)6_CFmoOiF(}R`)^WPM1w?%T(rq94`%5KJt z#>VL)iFqs!oDCB9I)g#I@|VWeKs%(h3|qZ_MEBfi)Y=PXz9nE_b%JF2G&jSnjr;^J z*9{mpQ1NM43D@~dVBntRFNj5!geNtSO(3TU(49;qp)AbIVCxq~6&p!7?H8ZB>j!a= zw`HpXVbYCW`|wZ-UB^sN@+{$q{YDR>H&r<5L1PZ2wES(NTf!$SM`#vl2tpWDVGBEI zPq@)}_3A~__f#v=aP5p4w-n5PzjV@~_R;b}6Q-|oX0bv!KBmvGFs|&}O5d1OR3+Ri zaioa9=HRHK#i?-`6|6eoXjqADCD^o2K@tKo?c*S{IS$76t!fq?%Q+5%Ly0J8ZYqYU z=&%?)CK19l9qcY%HaO^ljtmq5vW!t-Pgn32cP|utyt#fao2mf6ZL{8c+S~U>f ztcfFQP&rXA(Y8E9*W;GH8p_F!D-qDng7zB4#UFNqZRa zVoyYD)WNEjrHP{9K_d7haFrBCUA(?IErZdAfUH##sMFxdCL5Zy!HDq6~u8bINDWL-Me$JJkU#7QAWvpjAlhBITI8qAY16LBV2yXkn&3DdIP+~?Ees+gn z3Lm=f)@lpUu2>TOAF8DVfJfeR*y@1ZG^=+iDjSi=7z2u=iI(dHwdq!ZNyNdhZjHLh=fZM+-S0Ls?9fv=G2!K;8{yXWKJRuHm}Cse&jPPGNM_frTHM zo>J0!)(*e{>VhW=!bp{GPnv?o%4l^z=mej~@B2YViP06evh-h@APY=KSWzWMl0g)U z`tK}@z z&E2u@twtaH#8#*sF&}mJ_?nHOazvm{!F~~}UeAVA=k$Y}f``?I>{k&QFUk~D{mkA#+hbqSDoypOeI4s6 z#-m%lXXPao1GK+37&qr8b4mBEyAOvV6p2md+-SH-MLom|`2y}hRuG4_O){n`3lz8{ zEN&l_ZS6$SdkfVPfr;oL`ysKV%sp~bk`WN8CSD&A;TmAXPX`7&O{=Y$oahRaQx=Z(&azLEf@~i?_9`)efWLt z^lg*K5I4F*;s~i?TRoJu0Z*o`Bt*;~Q*6+_qd(1L$9o|0BV%93ZrVA0cAqzH=y~Lt z5@Gg?0vEJ(-}SsoMC}?9QX3@Pce^jcSUMj?-_+|f+4?VcRNMFzS|KTgTdsO7_Z{M( zs>F;18SZ5Jtb&I?r_6WAIBjO}cAeSndvmww+wrUt+V;pZ_)Kn)`m*c_W`zyohIHRQ zJY<3#Jjx9PpR{Bu?HmvYwH`EtY%ERRmdf7%Hn~WlSw(4ztHD-zMyVffZxGys9_@0x z5Ns-tVAHq<+%m2CW&P=3LnfkEAwYxpC2-4i$Z8{Coa0vH9h3C zDJ!AMj~8-JT7w_2Nt#XL*^ILZ=Df0SZKYJJiI#Hq_m7?TW$%vZyi*2l)jO8iYS`Ye zy9@6i%MP5TRKi^I-~Da!x6!w+?c;B=6D+V7W9b|)-E8<@VPn432wJP3J6*oyKg_#qEOc4{x);}*|DKf z!Me23pb^Eb?K>lCY11TJ2TTdXY=Y=ce1oPE(BLcAhX zIxjj1`e8&fDbPi!ZiYOPnQC?p!<1+}%`Ve*O8O#E4W)s#IsuQ>dDsgvt7iMm2sJzE z7F=w|1u%LYjYoKf89(G6QUfjMQes)zrh-Wk66jnzQkq6J1iD&AX4)_4C=bue5q}XI zs1Cc#le*rCUa|Gr1L$DGk6xQ3^y&}U)Mh~mSL)F8+y3;_ukoe^w;xn{(TAH8oob^| zONcIN%z{SGuGa*DXhQBFtA$DYHy)D(Pt6WV(fY7lC_mF$?Nuo0hTqz~_rV8z9wJfXRRtn&uPj=>|>l8BzgzDDd?4C2Y3HQLYuX%6&#ibUul`!m=}i3g27XiA8K3*uD|**PGk z)QPQ2)asB9*c7m(bGoyIX$A?s7`KW(g)mQ-!rPF7F=1#(m<`h8k6WT4ac)#&NkL8h z8f0yxy1-@=(xi+~6Yjv3i%c@u#FBa1jO85bB?k!?X1|*ila8J$RJIZnsN5{En;M%M zHHAq=45dKQ3hKA7_IFwbrs+@jeQH;D`=tBn+V4v1!8Gc$kaYhc9sF;z+(TVAJ&7xZ-V&|BQcb ze9SA!pKi3e*R_T9FrH2qsxbVemA0$WF8uprcIy={B@a`{IOSxakYbJI3DtXby+%lC zU2bKa442))OCtKO%`$4(=qtoWajI?TB%QTGhJYEXPP{^}5SFcQGzemcusprlqey1k zaN&Nqe#qKsM50?(fMbq7B7j5&Pmwg#3)ss2B603keM{3(cRC~XmiVGN zPFC@5WzdLQGNyJAG9(!U_nnX_SpxN;6Z0*|?Y`=^l5O@lW{S3++BF_jQ}BjcE>teoH_tkLzMMAo8?TqK9 zkzqR(-6|wNIVL%tmYG>>R3v~&nWS#$)n1DJQ(L3}e7kXiK4$}!YoR+7JCV`mUJDa< zxrz3F(G7+w325@#dSaoSyY=ky&U~ZLo$^kqM_JLGyLI8L{B5I?#KZkB91b6&tag9> z`M1jhElCONUk|IZxiZ$hgJ;K=Kl{bXSb2|nHsY+;svkSO=ao_?Z^;O&NBBb8tx?My z!mbHqI82Pi&LS%pR23CkK-=52sa@u@PNlZ5XfNF%C2FBq?IEY~O+ih|1C962G+3N)=C-bR^D6N%$GrD7I~2 zi|qiaPqp{QR>}F1c(C1_R8&z#OO2b}pUWl(BsB>kENPT8#vtiMW?k8_;4IN>SFz{z zD{3r8k99|KMO$W7i)r7dw9~#6-T{MJB}QzLXDGW|K}_O0ttw_a zGmsXb17^&b5ZZ@mHiHhnlNrIT2?M6iYpQX0Bz3$b004w)Yk?sFX6mhM2qwiV75hRH zR=)zoDzR%Z3mcn(^vVZJ_0|rnApJ>bxvKQ1U=0)qIH0G6G)o|G5Rm_&PGnL5HtkDh zNz6>=1X*(f64mgbS_5tKaC=H^X9x4J;g}~I zrrg3nE(`O3P|-w0NuHYw2>{ZzV2*NXydsN)t7H{;8}NX)?HyIX8`KlY2Jk|Z()SK5 z4tDl^HPRq_$biK{20WwhN)kX>#p2w`54aSMdiVjmRqv*Xn@&G75v zqr`g$-Q`j^-Z1{t!Nz#m25^GxEMMB7vb;1s;U(nx@lTU?`2v?Xg3Mpi(Q9_8*XL|r zfARg+n-h0&I5@I*L--pfcb|RlZj29)C=wWjC!WoIxnVbw*Ejel>`fLLa(~>jGl%W` z8GrN5lVfW2b#u}U6FO|(a6QHQgV6iiH@{-U(=I;j4u)Zp&G<)m(WRmNW#zy9M^VjSZa(X;{^}4@v zxVyb;t^G~_e_1;s(#!thuj9v&T^w-6vA;I@b$jHlWzX*MH|^{o4IApg{dJYa+d~Su z`)k_oMPntidDzpTPFYy9@DzoOGV?8-FmPI`TR^|x`w z(Rj!d`0L8CxYqu7W`A7=L%ZvP4*|*jH%}d8bYQh_*L!f}5Rd&ChHAzRV`h^2i)N8) z{r193f6?CeX%{DP801Z>yk!HES`LFuo6ipH+Gz|w21LDnG=5}PKMan1hl97j-p!xi z3@3Z%CY*?UCYGOVj`;J8n1N=@p^b<6rbkqO(x^XT7IiL z8jWYUgTZJ&cR1cZ9>%$U$lr)>GUJc&2Qg}4;I zI~q@B)6EIj2m6~&yqy=ct!T;oLi(BHS-SD(lmhgE)XQ3D$;At8v_C5Zk&K+R)i4bE zl!b6A$BFycEFk#Gl0`g?39YjML}`*7$!wQZx)vRm##av%H8&V<2oYBjc$3&t9B z-7CN%6GCrp(^oY`J@O5mtG}+X0uclJ`(y)Y{6!zlTq?qnM84fGn_WokZ-&y$8y3fan*Wk%J{#}#$G8fk{riLA1?|74y}cMAI z|8QEL?D8qUrkHh((9Nx_&d9_JD%!I2VZ@4 zJp*c0*A2our>|Mg8YvwC5CEmfqGc zyLE_7b$pmVFB~2-A1%0Zw)gn^?~&rW5ANQ(bNgwAOV-cORBl~>N6Xs$YViu%+1<_;8>$RkwyTA3xI6b=Wo^|kywVtJ89p*I{)S3Ic2%|AK^ zZ>C&YgcY5+Y20VQOV1TYY0pt!6HXV*% zPdm?F%+detyLVsSd;0Y8)0aDszhCG#F6oKV(ebn|Ln?!W?*6_ROgrP(3(w3$a<2C< zp%clFad{jeLtdLKJy|cD>f-yqK9OK~WfxD^3-5LD;j>>yTzqF2->(<;Oaz@^{H_%B?j5;5>j*ceofN1ki7CG zV>#YVjJLJho3GZ^*IvWOhuz7WwSb6R%!_D7g|0IxCxtO;Bi&AhQ@`n>w$mi}}iDKd=N zpPugQnp5zkJLw)y*WNJj7`C|ce(fEzKLeA6vm-J|oe3IBcL&p-M*A^lZ@j<0UYd<} zXA{b#)+9tUlg(PeYp#3JTxYMC7lU;C#h1D`Lm^Ivo%}}kXs|(D^kjV4-RSNg4n`YT z((%M3?n@}@R`SLgl$BmVdE=@U~l#+oQMrau;T1)g_A7ztTCxT^Vslq?=xMixnrpGyE98I{jM zhWxGRE%ZFb!E|#leQ7w#UyX@M3fD_9QT+m#*qXz{)2lG?G{wZzzhg{zJ{e=^Udb2N zfDPHCcT)!zVHI9`b?fZ*F?>52d~k+Z{tn_Mp5zkF_JU!z^lE)=ef_+11`j?<6wyJ9 ztc&-hJ+Md-2@huR;dEZVXhydJsdVBA|<3}r%XS&KT$ovB@w+I{TE$C0v zxhXiplWuxTZQZm&Bj5ewr@KoHQ%KS{qsZ?I6Au&dEQ|z|21_}o_0ltB}!y(VQNPqz9uHP+LZCt#wOXh;@5Br59A&n|M^1B+6xsu9X&% zgD0w)M^U~oF70<`=tmK#d4bD~93GRVv+=AuOqya<1)|F1d--u&SBuDCG$iOq6%(3p zrM+^FzBYv3me!sAQA~!BKUZ5_fIY9os)d~unDOwb230c&=yXd#Sxi zvjTC`0ddrq-C1XZzDaJJ2Sf526lQ{xCrfDj?J6|>mZI^utI;UQJpNLQiqnvSbd~+? z5M|k?mkwRG?8j!J?|BR7Hwt@g=GVhJDo4Dda)mGO5q%|INA+q$*OS*cF_@MKClEj) zRIczOYF9J*m7c^;bN1_o zdyi7n$^r(`hOYC_(poK>;o0e}FvK62D)h4t$^^Rncsu+{se}t`H*X|+w+idX!puHG z^wY(b89|(2$oVcDfP!UKa)6nV2|dBWkMS9UL7t8kjOXUn@;QJZf#Js)&)*Z~Cr%Y- z@Q`BqN;D{3ddn4pDQcm|9Dqu!q@%5l)}eepMYQL7b$av-Sg8lCU-Sb z&PEo|ZEPbKRjdTBs9yN6Hf4U})H518&&Z1df@uL?$D2Jfo(o;)ASFCN-pNUkWA9YC zp*zQ#gN(9ZlHHzhEj7=GIC|?bs@{A>s_Tsl^kHC!@PeCeq+w{+uz&30&YgN(4Z1Fh zE*BsvH8eky7@o$5?fqZsv_+Mjb-onNol6_6_a^~>3te@Sf20~fu1qNTd{bqGo?#h{o7DE2F}Wil~0{j^Y^VZq9l(SF1+KOrH6vu1J-@1uOj- z^tOha8rsSCQ?T^x%2|FeEvjbYB+hrNp8zMw*i|dd@8!^bI1U4#M%Epek?+rDN1ct0 z)6>(^X|*(-yxypA#Q25{emdBn^*ed?Y>+Y@yzbBJyPuvm1f?}mq?RA*BtqUJ-)9Rx zZ>Cqpy*7$-On0%x)l*m&@f`>%@+o(bM*1j!W#4)1j|= z1>oHE&mHH(VVzfI)OeM9RIwL3YNz036Y28;o{w3W${-un$`|kUq27+Aw;Qr!bEneg zNHC7>ZS!7s4z#vE+O5NXw%=pNHLmInHQH~lT?A}o5e9humOk0Q(LP}_M5#&_+CQoz zL-n8C@f^1dAfE!kQpl`xS4r9dw0QLKKDJl^AD%;6GM*rT-EWc_S7#+Rb<4M%%O zE|uB{sw2*cjhNVZ{b)~RidnHUi&x7X$`a$nO2rYE(lW(a&@zZoSElF$j~5xE?g=G> z$eZWf&&ZDp1WS=!0kbakDS*}s5D%WB39CFdIqIH zSc;Tj9{Wkg!{REI(fGO z$*bts@7l3A{ULQ-cs9gdN>2`tVFOBb|`?DK*_-d0E0B88sUK_d8J_kAb-3i5n2r-CdU z4#^8PhA>eWRZ?H0ha3f|M$Kq_-GqYbT(kdpF0FaZbJ`NevbHLTkQglsiAP45sg&r( z)Kmp=P?+#*Enls^KY(z?{J_Sau;U=uVL5_VX`zJU&ROh>aZ79^Sij{jX0LVPbD66H z>%4dOpibvK*ZCmZ`7xcVLWx}r<;~5s!px^{)_nj)63|L_vGz7+6vdvsKQEpg@^Ak< zch)C8HsIg%JbyDkw#^86I_nX2W9qly;topQ3c$E7t7d$Vi@*71<6l2#%l3;KUvHFV z@pMMpZ@yWJH?2DS!AQT?k%CsSJ@;Issfs&8QJJ5up8m(pD6YbRRMrcY z2$OwXAfKoa?snfJ34^@qA{?Pd;IVPG_Wt+7-}is-|33JA`g?c%#`(tUR2~{CgtyV= z{1h`rnmFaef=|cWtzpFXwL%{2%@PJB7fRio~HOgjn$V#~ZQ3b{2|YeobI0 zjmD>I>&4Of1}&2%ep@Zy%Ky{9{h#@NzSI4dHgWH|HNB-5;223f*;wDlQ7PJ#^wNc1OF3y`D!(%?2}B&}Q~8-)3JAwTMXf z(6i%QM=mo0YvLf6Xv`3y+I!4p{z^_@Dy|Qf3>Y zj$Xc^$ecHAVc2x?qp?-oqblW~b;4E%^-dM?7;!7BhaFd0Q|I6UQ)j`<@jPVJ!WnZX zrsg~`CK*IKIbOa}t?km~WRb3q8D+3k2H{)TGiWyNJZjfQW!}t9wr(IKmvOP99goSr zjn@g8t6Cmo%TanVN$gQw=@9U}eRQN}JM@{@y@!K$Dk6GrCbAj8XMQh6f|>*~PNg~X zRaTyP)_%i)(5uQmaGiP3o;Vo1yL0^d!D#S!7x|k~LFRh)8S#$WBNH~od<_juOVi_5 zV&t1>MRa4}us+*>hVeu81I2jcC-Kc$Y4K7T9pbtC;rJDsU-Dl`S-F)Wh^_D)VtJ9s z1cA(vvLhZSE+yr{VEPE%=o_pG2B>uU36gf69YVMOxx$tg6}n_#<+S?Qcr<+fcoc8- z2Se1J1q@P<^}AEVc$ZfVcnI6M})pt<}zeJd$F@6%fLfkdHNCU5CCf?e&`tQb>&q zPD`P|H{a0D+?^%t=vnKuJ3Q^ap9*)HB6dCuXns=ROfs45i}7RP=a}1#6OzGcn4d^N z_4#rbm>*`>3@7@`ITVS?^5dc$G}_ z{UNU*;{kDT7}^IQrKVq!PkxS6F?PA-dg*W>H2>zC^m}65X#sxIn7M{5gnhytECN7L zn0JwpB;NH3E-z6vZ8y)$NYRQZe9Mc#i+Wln7eryu6&DCXfg-y1yLZ>V@ITuS=&VmTA=PM;VrB#L@+&7 z7i|W`(fDp~{ySn^lk-V1b2W+2@l!J5!b~m!(u$S_v4j(^ruVriTuAp0pn|5OVi+>@ znlm=XDKlEwDw?q^Qq$vAyh+nw2EI49_QSi{0Jrv?b8Fwn?BrUAzh%1Wyo#@iyb%dG z989MKvtGKLuH>BpXAyNP$PgIlh_1OpXs^58p(dmaw#LI@_lN@0M8^ab^Ob2ZU~ zD8k~r@Mc~O>|LM+7Oci#v83^_>^5G>(B6c(C?uA|S@M$d!M@KY4N~C`ny?l<`DUK; z*2H`O{%eDh6EnTAY+Gas1__hio7c1)?Gw!!vJ=Hc?-N+U)1%`%uM0@)ce6{H>~Wi5 zsAT|(SG08r6njGZ6gqigWn?J2Lm5LhLqxHCF5^5E$*Brgj>1{ISqV#$S`yaF2NRz0 zg78!W;dtE#kuG9`H_L3W5L&($Z_+3*z;0@Z881baM>7~K{Gx$V8`pv%GrGEgcP=tGxRmVCF(l- zRA=4Sa+I-rRjQ&G7Z!i8KovGwn{0NUmtSzV`#fAPM#zGRB2equg>9B^#zQt&(!+T9 zW@u@XF3zl}L7QEjn9ig92U2~h^_-Xkpo5VhpaD~uGB&3L@Z|bA|Xo}=W0&TcnkYUaeoF_SU=3DO3rvTgp)2Ml3Fb4j&?CFSv#Yy~_K#Y-j z$^aRlTfGv5a@S>DiR{TJmjAbFGjLv}mW&R+`KIfllFpN#bGb)wo>{sDxfhs|=4Ht64Ea;nlawrlo!-@Ts!Szi3kzrb z&?aHGwIh_cTL}rzD0~x-fDZs^)Fhut(WqAZDt90eDunQAoV=Vdy71bRM z-=Ar2u5$DH8C=ZghiKN%j2OglVf)b1+M9>uxP+XOuRzyGo$q|7{>}SUleur=KM9}p_ zFriBe!yOP&=aq8HiTSyhBpqTb@MS{9e;Xr@@VcL}_mJw(=iUqjr zoR1@JkVPPqKwkX@M?2Yn{-~GFn6xzHis^<5e{AHe`ee>Q&RbS=2AX%e)0ukF5pMv} zbkk$XF(y;#kkusbf#}!i&Rr4BXN&=_K$mC)K)c?}67WoyeQv1~Bv5C{hfqS8>Fwcg zewuT}{>8}VIX8htL|+WKME+9lcNnK!J!js*F2-=?p%KMg0pS$nDR7fnQviaY5U&-Z zbsU77GoOT?gY>#{bpx3h_|cPqM9C!C6`78eg?FUuq!NZbX{bALoE3l{4zN<5LX>Ok zXsZL;zi!eiaR8ia5tZxkDq%u<;IGBeji~ORtEV<6-*s~DL&U=*L=7Z6YT<^ADr3m(GbkM|HzkaNyKNEp!Zib5bH z`_v98%QylqAng4EvY3hqXB-OWKhOCRX)kGLoJlBzo8{CrT#%9%({gk$A08E{k%l0i zP2b8Z9y}Ov5}hKi7mVzpqb_`?2%(E>;CtNife_B1O?(_BL^VW+{JuSoCkg}GC)0E~ zf(zj*p%8q5ycN(;v6Xm5n5)zH_$Z~#>gz;dMBbyVUZ$>3|>6VlM#+f@3 zE0vS-lW;Cb*1el4(6ge`+-Q}v6*xqvAj#MC)jIjVjdJ!s3SV5|&YXd~1{*8CvQF|p zX^Dh0o735vA*>FP=1PaW3yIU|li-}~#b*Abb~Vvb2{cws9G@AkQo_0W+UnxWb6f=# zwBZZctaCX6CUwjitY=RD;H4^EYIbjaN_GUI>7G848Js^Ux(jAU0C>-T+9HL1RS=Ul z&K1m)Yn&sn3_j#QTiLF8iK-YOijb#l*PKVy*c{nUA%MvZ2TX1SOj6oc(0@_G!!j8; zlEJ1&p=z(hZW8q2an3~qKA0t;rf#l}#D~cAyBfzIGvg)n%#C%$eC^?-*)GG%^G2$% z6PjKH1?$zd{fNF3+RWm?y6|fElQ0k-bOTcu3ysbNF7t8J+ke%J($d)5%R!C3fY79T zU?<+BT?k=5NJ5y@3Ou9`Q|Ipc!dQ+e3nt#2VKe72ILlCk4Vi|)G7l=S{yvpA3*iS7 z(5WZ{8Cy zKj0*@qTR-^CR^zO+ZeD#J?x%-6URrz?oc)INz8vR>J5*Hftxzdg2z4L71fEEr!H|Q zvjWg58~!C=E=Lb| zwWAp!Yga_s>Ba@0)al%_Dicvo=V@FB*ID#tS&vlxQsP|nFcS36GSW#zoxgXmW zaGbz=D2vf=~-E;3<<N#4!YUyamT+(h|*=@ol)*1J7HBT0WCP+x&|nB<`6TF$mCF4DJ|4~Dp7s@ zD!bYXr71G0$y1<+kc7bUfM)>KPE92>+4*V(znOOwxO~<9C6=3O zw0IZhQ5jYzkoY{u%d||W){Fo&Kyh<}9d} zXBViK6{(U>bA+T@g7e}KVivkX3<5m^k+eV!+h|91wJxs=!0}y_cMSbWmXI;022UmD}QlQx<%vN>P{J=W3AsIH&w~2O}*KE9rWxvv> z@zwx&G~{bOENeEWpHjv4Fy5qwX(Hy$`Msn3S+H_*)VO$82Y@7tw(2~X(#hE2v z&$jp`R5?(sFdVKuXQMtDkN)xKO|f~<=1^-+Xiu(`tFOvbjQYR}o*uog2`YvBFTDpp zU>?eAlryp$CM{O#Qiy0ZZkfwgQkAZaLOl!OS*`Ai^TM4)AYjx1JdRr^Y-fNFuKsyI z*gt4g=7G>VsJAWv!u{EgoH(2UA(`Gh5R%Id2+n$20KokP01%PXI~$nFqjde@;`J>5 z*ZzY$^Ps3V%KI09qId5x#FK#Hsh|ElIV4vdAcCSbHyL)DCyDPDAmWc^U`D^B5D6DQ zA0ipDuhpyF{3@c~emG{2PO``cBONp6_NJ#apImi|wD;63E$-`u#bw0;*`{=XcH^a! z7hiOdl~=7-D;HU5{IZ)_wBDZ=*6R-s#mgzO*$SNg zkS;mBr=y*-vg`{Byk%amHF_X;rNG-d`~_K<;QnE9z-f-1k#;;mN&@K$?4E@utWd=WmxO(ir`YI7`u}4VR?Fs^QF!yi2M#fJk^4D;?rvZ;B8|h0 z06%KgAH02e=gUfEeETmtNGrovQDgribuxJR`mI4XS?$6sK7aJa{(j>E+>U0yaP&L4 zT`{^2dM&O(%>@Ad%e~pwN&veXs|VqQVwW#43&r}uYU#8Q|Fw3}Dr#lktCVchSYZ@o z2B^g~0JVZde){P#dwEx`Alo@y8sJ4y09t%9aYCy!CIcIJZdcjA8c zWIz;r383uOQ_-3QD5jlXRRi^7hF`4bg@Kn?*_xo-5{j>Z1 zL-_rh`~8!CC-TN&rZd>T3WiM8j67SKYV@-YEMYCTEDD@`xh!rgZ7JH7P_4bX&kGhL zj#e-wf-Xa0#ylzKzHrv|Ur{I)8tk082$vnKLJh)GOZi&WQ}(SbIxp$zCG-R9jU8eXLaY-Z~d5hsqU>2E@qLf#v$pmSdHV;jg!r=jfPNBf=!#Na~QsI z$rGFvpGC2$s9=0PyUKJ!V8BJ=O|4TbFbpZqOr=)^{QsFNIl+7dt0t%0WTdqo_C7cfi&K6p}xm z@ESNtL8Iv`-BFqf*!T~-TSEi7J9gNyJ{|`tYKy; z9BnqSce>rzDm=6TEB)P^qKg{5@R-fzV{{!Jv9R20DokFb@2R-XvvY4_tcgHVLJ7H; z3*R3ycVKQXu0Hn=Lmg7-%r&#b!eL4j=BDRWa$ICW*x1X8IWHb~ias1&c0R5@<1YGg z=bd_U%uj{GZ^MbUj*{`tVG3%Qc^O+&4hP#_@vfzCwq>Qz=}CjUL* zIV*E6Ks8GCzoL5X#do|&CTp2f#W-(;9A9aL9A~VM-Vy|BhSvmfXbnZI)bjlE_-*|R;QRmC0G?X-z%{%TCg5PvPctk8YQHAO>+lOY zUX;U{>*55)$)^7Z)wg?5eaiz2GPNp*D!gmpFaqiZ{obedn(D6{x z3GtEncST!-*f?O9qe2xg@P7QlgP-PB7I@FemoiUL*ekAt~3b|`W5&FiAI zlh8?v%U+f+WjBqzqH@3{MQtNrLgqkw;#7gMi0ANX$^knA?5&i3h7t+p4K2+$ykzH( zz3xz)miVz369xrHUMi;Kpbb?Q62wa-e((K@3~YO&&Z^LjCe+(J zQwFbd<__ZRj8$#M3o`7m=z>20_oZd-{4ITV#!(X43=f63nQC)dP{^65Ac9_R7KTti zeoqhP(6rDq=5D3MBv3HpeF$oG#Ub7liRxNDfa414_xvB&=4RqwY6uc7**;FxB>;Q?@2 zFcTOO&m0392Om`62`93!m1p-6r67gZI-X^;c7pwNYS({!uye@uSMK^LZ$l3xn|p8m zVZxB_E?Ro>fVMw-_Fcp8xioI)x%~QEg-hN;2}C+Kdnp3`rDZoAJESO>camX&B)#ccPXC#l z~ubTmzFlTDx9n1PuZk-fw`XOfZOwe zYUfIn>|{`~b16!KZn?(z(j`iRT}2!nO_VOgG3G*c*R3dIBR=?oL5wXk4*Jd7G4DW3 zkoy$mI`=6%lq_$yQiWFmV1M{m0faL{I65sD^|m(x>pC2yG8;dc3@D9a5914+#^%n% z(N1$Wjt)5IAV0R@7xW$iztw^{tr)Po9){_11j1x|nv;FH$NLBFO(tWqVgKr1WroWg z9#3aEKd)}?C`=<~TdYeY|J^&?0!c`KcY?le`px z4g9l?ds4+?!qwe0NTd)2$K!Qhb=YVRP4OBumE6fBxjUF@d!7zCFYLe4IbInXW&YJ+ zg;6Xg`4v8i&H@WNNc4<5{$)Sx+E{#l?bgn{+fQHq^yKdCy?b|GV!H1=c=UZXio-El zrG-8=&kzVt?>&6{>|S`{<^89RABJvLx>V#(DW*Hp*?ih?YUx$ZNagEP=zPP$%kU7! z|H5Ew?@@|^hy-T89hxEUYoQUZKzQC|<2y(3Jh!HpwA50LIj>$1S~TS83;}j?>Cw$8 zi;>H6du#6j3);JVXJP&XDFTv09O~}9-L0n&p6oq%y!;1S;5PsjwpOGjM>`NE!jI_rzJKl!06b0j?NMoVE1m3QiJD^ zfd8k!=X2LTPp-+N1I4hC`9;%S3(nk6SDLwK=c%PpXNy4Y`t5S*Y8}(Y7kkK_t0ndd z7`>nmUIwnZPWSqM5V*0bP%u8K{`V9eEnwV1q;(P!{l1!4#nKGy0;j$35X&mkjJ z*=K68%B1)=h{mK=$vYp`V2-Pk7UJfY6Vr(*ez9b1Wv<{Rq9V$UGs}gtnHmk}-xe))Uv(!J$S9IaR?Ul;qZ>&rUcB0xvwK|C% zeOc_?y?WS_?k7<0LayXbeTjJp%X-vS3wVSp^CnA~b0dX%0iwR}CByZUI(aWTHUzSG z>XmGdZ?a&H`Scb{h^eP!>gZSTRg{E_tS^wMZ!t%v1k^9FtJpqmYQ8hD(@uH?KYHi* z_uo0#HB@z7@y6oMO3FNSe$Q>PHXO07ipfu)jzq5g$ucdW2;9*N*jOFlbg?N1Y%SvZFURM zhCtdG?OkjItoi7#UIyxn(py9*&-E`n*T!*g*(^~K+s}YHBh|}Vz}H{Viu|01QQtt> zMuj223=4zkHvf$det+Ap!xh4bSZDJE*IS3{wT3i6VH0$2O|~(*ddmhov{$x2Bclx! zc*Ra?ZU0z#d^xknx!|^=ZPW0;l+^hmAs9y+jSGcQ=L?g=UW0gIz4Xr}K!toy#X}e$RX5{D^Vs^{ z>pNiIEn_15k z_FyB5d&YJ)qC3KC+%6H2DHQXg2g&oMb@x2_+^*m_mjK7P9l#OE;?glZ1gkdRK?#IG zk8R37#Z)+oovrD6O`bA&AhKMNNT8R%KKEcQy@T0rXgWcIGW-pYKA@HoCgA;TthXV~ zspmQ%0a{Xz1dqTJTPm@ql7}r30X>*k;L-Tp!P^ned!99NoWKEs|NPXoWByQr3!hoJ zb*D)51)EgOXVPl5Xg3eVK81&l9z6NXk&9DpNGIDcM35rY7n{f9?~slqj-8x8d+_9; zag8DBqltbZwv2W&=-{d0?78_P^B14`?DqAI&5FvNJ~w~#VkjK%xlHtp{0zf1PS`As zRp>S*97MPyhuzNwCTv(;OY@6_-ibT2>jrSDUgn{Qu%7NE#zxL^68vL9R#B3*FdkcC zONZpzpARb@Iu#cQpOAg5c_!F8^D7@!{w!#&^I)mM7;Yzgb~@`JtF`+wQbHG#3%ajhSsFU8`I~s z_HXEGc4zU?j<(`=w5G(rVRvSs7Y*#7p4>rJVyW6~hBF9SH-h}o*i*ZiAlx((T1Jy` z_~2w6Es*BVF}1>w>ekV1vjmBw#%9<~Cv51gs>XfdB;oE&S>ek9;J@}zLera4A5gl zkl6!HRNpt0aG|c94Zl-n$Cxlh6TY1^= z&Jg-(Htx^FvU2hY5DChO!0NhCFBwEbfdS4Rn?EM0_586TpBWq2=yf97232x*LZOCo zcQD_yi?@@#aB-5Hf;&ry-}a@h>@c_;-9=+f-hPb!uiKlGA%7YZJ{OeZ(7{7Tx9eeI zbg`chCh}BZSLd)ZYGN>NF&ME z^nwxD=tXI}-o|xOHY@&0F0L#_=xlSmCljPL_uScKT+zg9VHuhBzK*?GGmFH^_e;(D za&?wsom|`=Mth#P1ZW;`wvu<3*|ZoMm>Je02ONn8z!l>DonjhgDUnQgl#_Pq>`|h+ zjbPol9nL678N>*Lx7BiEBVd?s7fuQ96^1jy6hg=t(0uy|*o`;;(E{EWjNLU@iM z$cce&INR}SMH>^g_mhDo)D{F()Kx!Y3!$4B%<|gY%S40>oda6V#4Uv^!tU##Xx`nD zKbmo@=FwC1@%1>WAg*)Q@(GM^#nBD#VMr`%PC*r2)xKTiM@DH?PK=$eaX-~Ife%OT z?6o^fM`(Z*Et;-=!=)#_pjZ&E8f|ZE3IcE<*?8~c5EUiq!FPH}H$yk#0Y2Pbj6sc= zo1BZ#hs527+jJt}>HPvV)Et49c(D$zfq+YlC%~mWL=aE5zRmC+5Yp3D+ub2PG3@uH zQwjLv5pJZjJpzP_C%5UzgF@e`oeFwV-bc8PwVN1Kyl#4s%h%a-FkySY9h2hNF)Wcu z0w;dFC#}&-P1$KUHZwZXhuxJCD91fXGCb>uvo$&&UxJ(H2*@$!JMNF*lj9T$v0aS4 zVLm&4RQeaYp3QOfT~L|K{KXDubIw&tL^U7M=nMV=O+-5vs`hu)LZ3Zs=YS*)P?DGFv;7S7UtgllKisFO5l)AFc#xJ$p>vVAS(Y`a~LL^T{?TqY_d#DpF z0s{J~v6FOfD|UnnTxM=8VAg!@%<+3o7E_c_DjfR(9T zrvW}1AEyg978iBtS%2R~YrVaE?INRFVu#*tb`mJVSx{{Iu=~j+ZdLM9;&$<2*8;et zK9f8H|J1H@_NfG&eQF0fi-!H^%n_pb>vD$G?k@NC6|%YP^L^bF7${8B6;!^DC<4;6 zv9e1@r*K2yFMpl(T~CX2Sierc?Ga^Ou0weF_(T3cG4rArq<7&iMSRo>p4upHdUykw z;V|zOHe4z-Z- zI0xuYpPCp~024VA`#-KlX?LHxb}sO;K-5Y1wS@6#yd^Y?k-!4YXe4yS;&cR#xU<}% zwVjoEokL(Oh`B($8yhf^q&3!#u8E5iLYElAfV6o1EpoZ;MMIUyDI=OFH0ZOLhzzhW z9xo83;dVz>3t5j|jLmM`1^j6_DrP@xKB{(u%#BqVMVrl8kAK3VQvA=){vl zG?i{3d-Es?^YvbN-q@L*JIC`vMll1&j?ctmgBb~%EDzzdD#E4Uw6ph>Ees=?gT0+b zF@3^B<0etj$g4TR!#zsHirv5p>J7ycw&UZd=KY!1IkA=i^5pspF97%RS~I;HMQT|t zd`p=W(f;LCW%u!&zh%v!d#y2ZZ`VM*uRkdRy$DBVRyCp3_(Hs7(>_?t% zE$!Y)I33*Po{q^*M2$=~>B#K(H;k9M{_P}4YF zaAoq7JjUdB1*e2;2}U?B!LMNNmza9^-}D?f6Echd13}ari%xv38%KBPaxbdHkPj-p z%U^l;B;w<=aJQKnE--NN6w7-M>StQr?LBM-AspUsJaQOC+sNixd!J|N`;1-t+q2eM z>)hDcXCr*koLgdJa=;sbZ6t6lP>TZBe$Y@GbS2k~O>cKsxOEe7>+T9SMU{T!klVrV z->54=RO8TrX*zw0W#s1`UrDGPE$s%`}r{O#Km#S z8tw|UVFGHyU7!{r!-t2mVqQ1}E-ZnL8wM`;K}i^$ak#AQ3YWD6T-H8%aEXNpqO=5w7QoD zE1xJn{&*C^toK~;!U6rkKPFO(_?_bR#|*p4@ZN(doSm%Wz~n5J`U$tDJpuO-xGzP( zT^$GR@~*&LP5^g#SKvN!X>d0+V#|!zBXChj?3j#ltGGEne6aS_=tiE$jj<34!93_)R^0UfCmLj^~x7 z1h4ErSGsUOf9{J{?zWdc@VQ}Y{o0Oq0QIKw&%{o9d~tlF?w=9=yfz8+tGfdIY69q2 z_W<-q&O>*D-@ZZfQY=Z1IM8n+-e)M$wr?_-r*?r>RPBdn^SsXjL#?k~G}T~&0gm1zXgjqJ$xiyW z6&M}NsOgKjzMhLS#lyM3Ltl$vu=t*je!YOV{RwdF0;@m`8XYkgH_32YpR^oYD>vQp4rOn%sGhZE-k;e&mqf z4yvsBgE+J9?NHfWG?whidx7)&1ZDOy{+782uohU(2h;EqK2A~>XBRzQU*iKOg8CC)Jm?>Ys>%&o3O3oe zVH2uGJMHdee(CdS98gExQRFvi>z5o&;9}cxML$x@_L+mHr`D*oLN;Dgqt1I)qASHa z40ObxElRV?s0ateg@cO0knG^>lZv`B`pqi_?8d8P-m#cEh`RFQAchX^(_FqCBt>UW z7&RUa7qSMD)}h|&T64WMgCDcEYrqGIz zkz@qf4`j96F`|)1(6?|#7xie%WN*7WH$!;&%#AVn2L*9I-?ki!smE3F!J!p&`I-;@ ztHsWOnR@os#_O-sIk?t-`aS|OKm5%PMpcWl28@EfjjRt3ao-r0$pnr>q(9w8h+FAR z3O%;e=m)8w^8PI~nIyDg3iv=!+0V>Qf}iB%mLYT!txWif5X<6od32Y+5Um`#4;BV* zmj)EWVOJ&7p7G`>aS+#hG6YX84#8tZYZOfc-!H1pF+Uw&A)$^VXj{=d@gwMwF)d>r z(1&S%@w7>>G8bnDx19LSWk^e_Shw9@naKwcgo^MT7Hao0r}jlYMj;&l$yD@jc)*VSRM4F&5=M ztB>4=+rPl5loEwR0e^2j=Kw9zT)mz(~9_ ze@B6~s9~>wy@SMsDT^}^@8W%iTbx-;oigEpB9%uHEt11jOL3b@yTkfq>&ijrym}wg zsYO_uF}s=R;j86FrZiUBgeZy9*n};Cu@E~UFtIG`^$=#p?{2~*TMy=IXr3Tn(^25Q$cWek&kq=x_9%4)S*yLhP!z8{BfHj|Ej2sJXaPk(GP_nx-*=#n|EqSbhjd@n z`l_L_-7WZE5T8fR?AAENGo>#J4+#5T3$Zyo|1)9}wgve~D)#P@M{hf>eNX89o`tD| zPG*}`zq_42aYQdTx!!XaKXOVlen#WZ=<*M5t>1nv&hM;XORUB3c@>ub$DcJH3T{du zSTrB`|Du`?yrBN9`55#aS37KOPY*%x%nTp)Z4f@bm^OzIgVU{MVOyh%U!2xCg!k{; zP9}(^!~O01VSov}GyYb??QIX<4`p;`r@rd^liMo#BL|?{ADlkciK8yJ6l}qX(b2&# zm#&Q3KRgQ6^n2vaPwQrlhk~UKbKSi^4Arr1h<)JC)t!07Vu-D3_f!IAr=;28!n{ABVgf?_=1Yhw+^ZrW}lsATYkTAYHwT%he#BhlRj!(c_@i`I1t@=P2dCLBW48;9}i zM+q==Rv50D{QG%PEWJ4Gy(<#d4Uua1eh($yC%6Y@DuvK__MBEKA~Z+__<+wy%k8x_}7E#K>}Gn zu-5(k+bH7p_kZ6;F>ws>pdfX6e@V>m8Mu=d0FTJZ!73w`nk6fnVT0HDBa_IIF5Fcm z8)*|Fsy@2AU!CR?wqDp`jtY=O>bxQ8ror`9K?up0l9n3LNr10zME=>XiQ$M`)Z2?( z6lIzw7jdyJ&X342nSN|w)OQ9(+}7r0nnI%!gd&r97Big@nP->N0l52vpF@uxNF>p# zo%LoR*}k(9nTf*uJ(K0GsG0Oo7G=0~uM!l9OBOjZTN47i)1;+-sgqQk`Z@wPqe z7Y`?0Dir<59}+^V238_jb^$GMatUME#c1tersz4c_v$&NNeB2Gem?t@vR5>_wz z5H2hV86xXai$dv3#Z;LGUCwv+pXp|<%iOuW)LLk+$LNAk7@1FTOq%r>Ze}{o?xmqP zK~jDk$r^H?VqyT>=ToX~8Q9>uob7YD^qZOF?HAK?OElA+3nNz@+#I>;U}~SEa*2rG z+>>Oc)>8`zV2-C?mrAg-?dHw$xePZm7U*{S>XdvllMEy~eUMgXG8?mO&J8m`7c zxO!9j;oupH%zNJ!n9Pape(nibU9FvGZ8#l!F_=Z%X#%A8<@#KVBWmrF1r98;Sr*>ih=!{wfq}i$CnY3lnGLbZyKV-%% z8eQD)`%$jEXF(vbKMC3tEq4YRFN~H#7}gVA#S4!E0~2J~xiqYM@$LPT&YZ6Nqcc-P zto@*7s&}O;7on#KIQ(E(^)RoL7l9`+E3jOwKG(6e_9E5d<&)wDGLdTb!!9O?c*N#S zOs8FXAJ8vCzI5+$nk(I$Yocu%9)H}2Y>Fg!&ewcgZn$fR!{mkdur35Ce;M2EYlr=8 znoyrkr3r%E>kpMfi=K7CI!GvC}m@wa&S1h<@8n8HX(+m$S$eP70Lm-owK zJJ5+vQ-$e{>#fmgV}Vl^-S+Ivq^AICECScbVbu^9Q)`orB4+5RDtKxqsA>nq-Wz$H zbFOR9NRl7+N8ZM1jIbmE$noBl*7_kLo1?^i-H{F#u-VD9@(-{>e{~t24caNre6&G`Y}D;lb{T3(tci-aC1@61x-~GQ|=+ z*RS<_{s)@dz0qxJaK?5z4>Yyoq}zU2(>tani%GYOV|8+?Hce!?G?i?_T$URBpy_O5 zU64GC9NVUulE|xReUk0Pq)l@|#{1}VpK-)~u)7pkE85G1PUwEw>iZ0tDTx34R4{ZS z67Y;N91SNJrKHh_FMpJ|rfp(lwrM+2Tq}ih5#M|uX+?yWR4{HMuQ(hs)+w%ep1=zC zw=VlOE}?9{vff-vL=Bj|K`h9bC!d_{Uuo-Z9&b?T@L=|a+V zYi+Hu8Y$Nr%NbHy?r;HRTjM-htPL*H>o-1&(ocH6PbQRqjlXK4L+UiI^&9O0Puek{ zyBY*lV!v^iXQ!q-IgM;*%ev&+Luoxzfj9VmyA6 z6ZJM)nT=J#{OhVWqWtUbyotL|)?3RAn343kuFG|XU>3SA=cYmSDVvHD&sU`RJIYJ8 zJM$E{#*o&Q+7;&KC=;MvC4(6X^8J2{F1-)cDOUp#lUBQ_ zIf8p|(7i|iXrD6Y@1#gyh;O<)Y0G34>f)ga$c1+ZcSjRw*Tt zl~);7dc$>+Ln+Io?@P^V>BNPBpY>f9X**@xbNFp_hOI`vm9J&`*H#yLoksdd`er8n zUXH$>9{FB~zMoBguScJsNq(+GpHCz|7o*RY(>E`mwah)){CL;zOP_2$@x&}=!L3j1 z?>^VOkhQMPI-WJt+82L&BAe%GsovA=tF0wMpddr6b()K8`xC-oNwLf`ZgtruR%slkvh zZt(w|JMh;_Pwcyt2_*&jh?7VCnmMq4=D=frjn?n4mu_&3`NUt$QU-Jrc7L*!U1$NK z))8*3j%ms+5ShJUNDX5l#!Ix9cJ~b)2xx4&8iMt%9Du$#!yc?O()+Mmf=VEWB^U)4 zk2a(nfJ!}kaX#XH;tz~a?=$h`U}$M}yeuwU#}(hf72Udpo9>fi)k23*gPn-zu-!SW zvvQ8LF?)j<3FjgGnM9w%$94RDyECzCmY-)r7O@P=FlovKTO)mmF2I@?AeY2Pht$Rp z0Vr)HW>xja%dQ2Ai2&a2F144p)2Vm7&W4>!8KXL?U*TgRNx-?=>I5l?sH zwh+h$E3Pgr3XUNj2#hd}7K|LAjH9>&Y6(QkoQ93!tVdUea^#BF3@y@&*=B}gSPo1v zT@WI&dhwQ?ec^@lDlB-oiYT71B|^6(XV>k7^@Ra>t&&^bni!e(#(E+$j20{eHHBy5 zlmv?kz?bNkTQ2Yx4fk=LZLjSoMwNt!sDharE!gD$g8;!)+ksx=lc*acUo@WYx<1oA zzyK(xpD@p0!3=COe^pvOID$PA{bcr#>cfOsf}Ug{of%}(3+b4rM99*n?>>EbTDnYE zd^#8BMm#AhKaA6x)6zR}I@e=OmS2n07pJ9T<86yBF* z(%EBzWhBy^xuH99C4$bit}eDZc42e!!`x68d8RRMXkB@#ceRnurE;l!p{D=3P`5ik zkZpYI<8EC}U0v;TL5-EcV6Cxl-<2y@vR8`ShJ9%t8_e83#NBkt*R+46rc`@rf3h2& z__#P%ijOC%wQM0rdzEatTG^;&i?z~9v6ihCHp8QOSRe@Z@JoBf%JM|h4gZvQ_JS-g%k<(Y9&?6 zmP(e37p2p?jw_U+5c7Gw%uH5lu8E_iO14ra2a{N#SUFp%xgPCvaQ(!`;0@yYEaEdL zT=0i+$J6`UH#5)nPURmFj$RsK2b9E7`|!wd)O8~PuRK60q^ddQIo~N(nBbBSHeX9s z>e+g+Fjpy)qdZs1gTBgFu9s3wdZ}1W<(c$ic@Y@p>)BGFoGN6C_3B(UU*)OF6J;vJ zjX3~bKAtaBv-Nr{n=4gQd@mGI)Lg5U7Ri+tHYj^JUnj9%uJcSsirIWwdHJ3PHR+Ga zf-2d3u6lj7R07v3sd6=2E98$?N?9r;AzKi}&{Cx^M^dr00aBI=)xG1z;<2^iBf<-b z*h#d|`yqG)60CT01g7^zHQEx7vD%7);dJ$0PYBhjcs0%jLDt~LB4^)>clD|;y z6oD5IhU6=Y6>hXN8n9Y?_)jF6{7AjOhE7-*xFfV~< z*ILIqtcexNxO;;{tyYt=ZH zk(}rNI?_U0T7j-E<;EqQi_tgAhZHqs$bJ0 ziPIKYW>M>OVKFzyx}2+)wHW3Rx{9oqg`%WRiCF#(6e_c<(nXlGKw4Iso?QlJl3FNk zpkbEnGb}jDQV>fbJ(r@jGD4VEXeLlHpQfS%$V&N=xNfD&Oq6m!5eCN0Q2=I1LJg?{ zloN|(qVrVX-Vho})jYs2=P2OjbJ2hbh=sC6wZTGfkFZMc4qQUKwq*(g&^L%hIo%^2 zKu$}{KektC2Pt^@9#OYJ_=GWr%sk(t9 zar)G~|HU|cs_x-ba_@iU*ym19Y#|p9FPF!*kTdDd{K74D=j2cM3$)y@ls>_C)iX)S-G3SO-H+pyCJ!=|=17 z#s0NE$5xkGoczQP8H`1Tkjc6S&Oie?FjiDSU~dW#4Q%0(-IR&Od+uf%zby_`@1ujw z1L;u6x)%C=GjlDP-vG&wHbHboI4(8a&>6$UPMafYO=JiX*8i8EU42Hd98v*y0O~qFZKpDqaihSVXX0xkiRf7goLI$j&Hhd z_>g&Skzd69K{SpG8qDOE{Dq8Z;g%y)TpcIoTebgo0v%X(okQIo6u@Nx(IHm8qN9>o z)Zv-uF3d6jV=H6*M=?(bLpxj0WQP9dF1Q3V)~&G;@7su>B^_LbIbg(6JKpzz{-eU77$MH9VA*0+H)O)IFwskA9igq2-7h^G3f?Y1&1A!-Q+ny z9RnCc&{otZh$3XC>h1e9RzWC}zrYl<*LIw-IIruAi`tUTVvP_d?J(xqv9_-!I*+|W z1Xl6txI!<6+{|2!7KOwa%^2>Ei)6_NA_KgX_P6*G)p&NKMpnkw1W2?bj{#AiieO|| zsoia^WNY+$n7@}bhdxbUG(ljnpJO8ot8B%{6oXK8!n*3ec}Z86k87Pa2dNF@-KI6r%@ii8&bz8joU*E$G;-4mG*Jh9E(^tP zx%9-5*5$+$#qz~tk|BnJ9&2s)GD3+)tO@9?xmA#J+RTxtq+7$~ab|G?hHgsM_De3~ zG=FLT8uNrmza(KCDSpX>Q$%?Da)a!5Y5#MSzYrWpv^w@-!yDLYVd1d-s!ff^$F7=O zW{!|>vq*3AK^P?<(52Y}enq_!e?|V4Slhp!iZgzxUSXbQ8;KXh85;DrN?`vy0G=2@ z3`d)uOn;D02!W!epIgp!FC_coK6t4Yxuw18Sg>Fx^s<$t4>SY`oJSV+x7CU8IuE%mu z?r~rA*kgVl#I|H)OSFjDbl4Itx-HSgk@;{W-W?OU2XIapW7~OZWP-Fiog#jrh!_#lPzr>d z4}_S#X!v^IhRN#~R!SyodYLQ$U+5U7UyIIwHG$=!D;|qI2Dl~^N;e0+Rn20PTV4W4 z@*YE2Q6ffb2sbvfuKM2o^__Q(qZ?P+V*d%A6Y}1;dRVEabOCPy9 zA8mRwV>?+w#%1~*0h2gzAjxLdca+no@RW792h-#EM`Qga0pp|>@zXwgvoUqz4?-Hv z3z;tVMf^HbpMw#w11;_i*dokzTgt6YAjd2Va!gx*96-O)TQ{M&_reRkgr*>*$f#;$ z@8;ne=C(-?4FwpvaB~C|fI)OOeQu&i0x9~wWU_4oTN(nNNq+UzLx+-I4fj}y-JDF2 zSpPuW(LT&Ke)CWLo37`{I-~ZJA3JT#i$XhbHB@Q?htxy@Xzeub?Ga5~e`oB`4rsr2 z{-Ybo1AV?X^hZ3MkQdU54X0%^bJTHhHK7TZz|SOV4`ziE{)_R1K07tO`Nh<&fDN_b z7yEedek~os!o7qUy0aQYlwg_^H+_8=O{gB7PKOlyWhD9?PSn z`cf&MuX2J|Djv<9clocc>bQ%7bIMxHAFXq!R4iKwj73W2%K2nfrSrKJauj79lFebS z;1c(K8s7fgvxgrMoUXGSPJoHmM-C-*Aps<;!t8ih$8#8hVElnuMUDgF62BaYk*MDl zPMizH;v$gD=QYh`*?w^5h^0=|9!s1m7XWLGQ#33-$Wr2Ltc;}*r_!8GlFAnqvYfg4 zQH(=!W$c7#v(BL!2jkdB)Tn5Dw0< z9jMB#DqpIRhZ(P6u~jK;Xd?3+&i~6yA&0yh7}-=x$C>L2XY3pbV}?kflBy}I4DYg` zs$E~jfQw_^0%j*1G71jLcb-$%N;QwEOZc);praKIWMzw2=46>67YjNH&10IPL)`-9 zfe?qN9KHg%f}V;%tD0j%b@U5>3XET;!#Oh-E9N(_|H&2Abltv`YHGS#lEqOC3m`N4 z;p~(nP^MUjRN!!38JOZ)Q2=CsObZxUF4jrMe23m(NI?Y{3NURs&g;PfPOf#fZBNWV zzR;gD6D3$sQHRMD4xeRFlwT2KOV>|;ZAHumXkF-9D>Iaw3~;J4hEj=gi4*T?d83N0 z1&CF~ydsZr00t8nYvi$3Vw|)jvn6_ARDmH7l?q=t7q>@j8+g>wHHLJmy=nwu_CGa_ z@QaWgu(27meytYk7)*d@&|RgnQU{=dH}(uGOuJsJOqsKDy~+lnQj4vbWz zgCaNpAOy84NW@>P3&0|vk1-nNWO~Lj5ezSY|Db_ceikZ4OdhbjGB}p%1v&(LF)yex zrS7>5ip%gr-)J*et-D7{wYmTk#HhM7*%-?}7_wZH;TqPCt{+TQ3G+1d&w42XgM3YP zF_lW4ME6u%p$04l07SkBQ2Ab7kpb8S77^g?3PUJV&tnH!-uo)_e6{tcY*q(NPW)rr zDPAA>Dx@xqu0o)RnDo(-Y)51PiXT7nsEmI!AeZh^l7B_b z@bF@dnPlno-IO7gRWey@Z`B@kv!vL@Ov{|q4Le`2 zuH>-@t5<3XxM+w0F7a?#k!tnYuH)Uaxs3cECd9m(ndf_js}rBcJZK)0*<#7 z$9=X`s_<`!N;F0xM4bvttE2#xtC-ZORZRA`&@bk+SdC>R_2C53k+~oC%lau6nzE^t zt+D%YqoDgHi!fAYeyg4#G03Q0Hp-Sxdos9Wsi@{z$@19yx@W4Yv*_gV;5R?j;t3mZ zK1E-O#o8e(f$5Te!wyfj`7#L&19O&sd1Nh$lhbVbDp;6FxC<|`;-husS-w)6WrfAL zH_NJ2GxkH2wTdjYlD8_DMrEqx_e@EbYq>*@h-3!qt&xqb-XA%+SSLn^MYfE^R9y^~ zJyQ+K-MTFv1(Ox>SZoXFjPI4I8>~a+vQQZjSrT{&GkYij6mogkN4Yv=J6m^BEtaNW z6}IF^xb^bUx_lv=PX%V65+_O1{z2KC^ey95RRc+?9;#NZoG?|-@aW?CI*b^zVe+vP zF{@oR5Nv>Ci(jhPvVru?4x*k@_hgn1iXxw}=Hx0(WOA*r{1J6XhO%COb;yMXW9Px& zT&_BnkLI9Bt3fLTf|UDf6vlV<{%VT}MS4P5c7H>vTF1DufL9g58I!4Lsk`=AL)gvG z6?Fu%#P-)_LlX^?*)v?24kj~?WiV@6oktp_&QgV@v4>?sm{hjBHY4;^$x~;<;38|^ z%2Ad^Xn$-fWPVCdVW4Gk)e5dn%#E4Wvhfv5;ZKDOu6BfHiKqOr`PLpuyDRz?LDD8p z^J@U4R|WhNfHGpZ!v*-FAr)0JC(MsPX3vu4*;>niD~~YGhNDiO>g@3(V^n1@E<5X@ zZIW~9Di~the+*ig1Wk&j35SjixJ$XHZAi6H0V)OdYJv*8)Bu&LXu_aEcO0?6=a>#h zEIeDUCy9l5_rzj(j993`FiOr!q7M;)H6U7H>C!+-Sek=+WRwPh5n*_bfK{qAVa{UE zeo_sG1QBuV3P$Dbub**iC&wxUy)c#TZv+iPTrB1#8Y)qLePRKZ5lvDADS*V*h%JpH zzJT;#2o=~LE142|pk%&`E<>&%__^qdP=?tDcj`4p$l6L0vkvXDJuWzC{}JA%GDyNk`Z|YZ494N`yhkj@zb_ zx?vH(*j5Wnohv&|H`R@1(7g`7cHFuOLWDkK)46fs21z=0M8}+ z<3JEmc&)amNi&w{h`x*mM&J&GB(lQ4M9b7FwqYGKj267hI78s@z+8$L>7Vdb4taXk zEWtaa9xc}DD--^Pd~vrbK0x@hfwhoX7|!)ohAWJQ`xFc3(VLc0KiM}^gw$8; z8J(iI8zKh$v39;v^8NIA>TL7!g}v)Z(yP@cc5_mDXddWG>&;6esL1=H$3S%=A%5z( z1kWCrf6ml>czNlidR*uwy#*WriM}Wq#`4IGtiB;dezAO%pF**=A>CK0wxUfEyoaBB zvACj>lWOTaOMk9XR9Y#29u5O;uV^R7n^kBk!2)t9ZojocwL?-FwJrPy>A=o+>iSlc zj#%HWp8%u4Y9yIjK`D788h&zx`gw=|b?Wg%29AI5KriFnNMT6pyc95b6_fXlm2#c& z>3c_=)x#&&c4lVIhy|CAv&+rWhj66hgx7J zXpiI&B^7SvxVQ-#VPrV|>4k6su6DUO5{lqBQV>dUMSWON%~j4ree;PL#)YkS0F@A;GnngViv>Pi`> z59~j9{@nbzGdvT1BZ{yx>O0jReD*jQMt!GhCog}RkE6a*rw7mCsuB54JxL@(e`5ex zjA^A0skglUMyuP9wIx0XL~SB8S>wi1v%j*?Yp!F1e>gl6y{9{f0_il;m1_gw~;>KpVFD<`@QawrS_n=-bjCP z{?y5od!cXLrQ_J5(4X#7N>7Bqk@4>7W*0B4`S8fq26o}fIb3O)Oz%N^OSxBSS?{e~ zC!`nE_$R6v5RLB|b=a>u%_Tc$dm4#7B!+RKeYLIb>L;C%yVBX-+1^?stzc31s3w}1 z4!4^)8>x}zrIdSd3G>{wyQo1Z!M(VIPc^#>O{%s8_d>fX1TNE*+HYFMYKArT-bjDS zzozUxoxzm2<&%!{7(TDIulld_JA7S60#CO3+!@tKpS0i98DT0mw#cZ% zJVWLuMXW4BP4PaL$Ck-`;xT+I#2*Xs$7Eh4zC=PY|6E+67*|tt?H7DweKexM@C!v*3JZ2vZ(Hy8dt*bssxXfE9D$STrIm=`C zyf`G7)vCUPV)h{weMuA1a=|)H*e8r68)*z?3D?D{he74Rbr4U3a@TOA{Che>P_IFh zs1CB&b0 zBoc;LT%y)hS+(1XzUd|9llq3$qe%|A@jS`kjHfv9t};i6p&-^7jr6m!5?NbXh9Mn( zTCJ*TK1Qur1|sPZZ99EdKg?XGy=J6vnD|u8TzK^%(9*{Ugr=$pT-I%NpoYt;&Al;C z-ODCx5T!JIPIsnR3^-cl{frAv3S01I<}ilH;CwXZY5GJI0ySk5mbPG|bBfm62Z@g; zG-=Ac>3*jTU7*E%W|vM4VF8p#@cJY&CL(8%~L4qW<~PH&;v zNzKa~ii@`tl@n9H_CUx)_3duT;Tb8%;4NxVe9K3eP z3Z(F^YpP#{g#L~@2N>0rT7{ZY3Uo!L6l*IGZN9HDz zqL8Bcx-``jb!}ffC!;mn)qbj)b}ljg@k)W==rVs}^mSs@A3>3nFt@Lj^BNVc#VfcE z6`B4lVNX*Q5)tIKIK^i$xCZ6t?Ik1)+G~6})=eqqVoITl`zc@voVvY1sy}SQ%Q`L1 z3$RFS%*8EbNL^e>HTx;}0qev-43fF_GC&Ge)auMx@wvrbXSmumDE2^Yg17arbr)CG zd)?l!pK_r9aq>?*L>E?2($uOWRW)Q;4q>iN)q4*ZF zwb)$i59KT%%n@|1u|zCqQ^yRC(I9^sF(sId6d>DWeqd01nXU2$!4?=vb1F zdyR@C`A&7SZhmIB%56TE+q4$y&sd|94%GofMF1ecrig~g@i>&_y4%r>6sh(0r4_Nd zbz;4aHIE1skjsf1@O-PgKez^xdW>?F114e3zSIiA+A^t?mQ^cwS);ntE%{Z`;_S!) z(6RwU2qEaQfC@`j*9I}5gh(+UjG}SefNO-WkiTDPJ8)h@A-C!~5Y4w~A6&TtrU@A@ zIeJzvHioUg%%|j$251~U#+*lZfU;D)$Jp&Eu7c0^Q%z`(qN^|~uEwNZ*66mufT9lX zd`)g>&2{L$r)GubjD>*Yp1&=y&I1Miy|=vFHtO*G5hF3)1?_pU)1ie0Qf8K$IK7BM z*0r{Q^zI8>kMFUlN*Q2z5>~pb0L)X$A=`j1p_qhN*Unjn>9?5Usnn=ciz%DZ280ER zd37y&f#9s<0>MM#mAlyQ*ea5j3uAE>-!;4EkDZu=Anna012xwdiHAJzO*x1@$d+me zq0UKZK}1tYg_R_HYdSp96q%15mVm>a7ukl;6?TE5_I8S0it9M#noIQ61TXg_f`(6O zU9w-wuqVYwn?mk^))9tjo4>5y!v&la=;UelKzr*E7*1r8BI%@2QwTJ9i z5`PyKj!b^YJpkrN8b+8B|2*y1dIuO`+rkh@ih_ZBhY;2{`HrkMC!t3}(`FaOWNRY< zDR8Mu2_ND6Z7y_S!B8IU5CTE>*P4c%t?r7t3o9lASj$R!l_uM;T4))v7Ya8!#-6pS z(4wuZ@S%Qtq0K(|nyNIe2th+ct77=6{wmGF|1WjgmsA()=(QB1jrb)*#s1$FT;e;j z+vTjp%E7P;KSbj4CwGZB2io+ z+%n^ynbkDgB5LSlnBxQtL0WnhZM!u2)Mm`fb`UofnQ?CCl~RG<)LFNo8k=GJN+ly_ zRE`*oi~E%vPPWF^$&N%m7WBU z780Ff?r|>GEcl_FlBN2Kt!{I@-Rox&c4=5MpmC4%(*!doMf4rdcDMdE@B>^%`hrtX zSP$ApBClxs$d7+JMTOcI+P9Q@^GOFgVjIev;t?>35D~@PPuNLnchfnilvBv5683pF zngzZ2GRai~pL9E?li=7S542hvY5NiPbz9j+Iy{NbiBF>L(hei>;`rwV!?bkj9^#;B zJWn|jFVi?}sM9fD)t+n#CL6U%X@za48H?T??gtUG_uFRi`*;V9PAA$v+xo$M$_HHy zXIdM)r0k8IFacNwKIIubhXhs{viVxnjxQH|@nxfTcGFfq`rzk=Q5{?~wO>5Ul>ai3 z#jit}ijW895l?tjQ#>^x-;`|BHkr@r)2fxIORvd#63JFaGM(})B8#n){pBmBYl5+} z5)4d|N7y|%|MX{sVr+&^u#`3@@@S{G!X(Yiq$q#1=)S)rC2k ztcgyX7)m&mN@SZ#`;K{STkFB@wY_x?QIt+cy%{UFx2}Xb)?1$&vhbevik36lwFtrO zE6cIdzpz+pH-jLeTV~2`B!eW-_mI8P$t$#Mga%ouBXA4-nB+;Ec}b6oH}47ej5aP_EVC5= z^7NuZpN+K15Ld(y7>03))D7E0-E=!CLw6v8PC#>juq_1NIP-qy|g3-^I)wQYw~d4ek|-Q&NeS4sN}w;%&>ZZ zmR~479ppkRwSc7ZAmebDnNl0ugMAv=?h)#~=s0aJBN9&?kYB_764BpdE51~U4PLtg z=i9D!F_tcuyOL{hVwLn#DWP5Nm$L2(?AM20L zh;|O96Gj-6Ho(=wE}_TH_0(l9aWX}junzg=5NZQ^p(&)zMQe@d#ng?^IAkhatU z+lD#)CY9fTJ5#kWI9jt`SnzNG1vtpOdPEx=+n&R)wAtgHv}UKhlyXkzVFw8yR zib|Y}m$78V$3`S;n{{`J!JcQS`!PGi5NS9VIb8D8;)z(;YSa_>u`t;_1S+4%fvD#Q z&zJPeq{hhsi7%ZXMs_Vuy`nq)q9?CvVFQoeD#%qRaJ+=YWt+^q4h5XV1%m2>PLi5S zCv=YwyBu@|t2}a$+za`T5Ki_|PQM@O63-cSm;CPO^m40F*3~1Hu3q&?a zN!{%Y{H9>{{gCB2C&i4;F#JA@-g%UzYyL?y$i{LoZ|Wu;h@7$aRQLcnqfb4(Zkwy{ z(W~a!i`!0si9u(j19LlJp$?s`_{~Er(T#)Jq%ljj$kQ$No7@?UZ{NDTxJ{-rw4{|z z{R;a~52PE$2Tmt-8D0boKwG#bvXjD(lYwhau9gx|;AE>B%~aT0N@3!*o9LvkxxQFQ z*OPcCVQ8^j!BFDejbQGSJD^^8V6LTZR-y+{38mQIpRr$R9#Nk7{B$QO;&wx>nE$RC+-ECo zUk{l^g-(M>OH1>uE{B+q2oPIZLP_UeQ_CG0r#dMt`l_}h*G=Rzt?(iYq=_QIA!^Ef zV=-_vJC^7^iid}7ZnA9n3XX*yX>a8_7cfQuljhn(+qJ7s3&&5{-06u8Tlk2QNMkS> zu+Kpm4B&_HV`bjr2DE+}-%{kpzFsb@t1 z?wg$sE?|<$frCeAB$JJnqqpOFMAa*_Wj0YPxbI8{tEDU9zPP$$y5BR8a+r1sZ1H$T z{|rxPz+)O;JZg%ucwFY3fW!5MuB}a6P8*gH9r%$;h6KAlcG4%~=>{T|}rs+x9 zixLJ+UA(N%?ujjTZw=KaQk4B-iq~8C^sq(L;PfYZ3n^|!90Khtt%cJ==}F6d)I6t;^Ycd~$M6l9!Lg1jsx;yoaN{jyL8f^{ZkNgx(Oo|R zuiX<2aP=$QlQclmC+Ro5TWa`diUlhd%Pi-(B{vpW&Yd}~)xmlrx^)pGB~zjUhay~x zEPt;d#*G}^9FDC9)H!h*ElY==ig=Glp;!lg_^GHdtMaf{#%rr#Mj-vZeUG&CGLuU7 zfrB~N#6)&?cBjnTiOZ^I*B+nkx!fjeu_ht`V}7L7#AP3?*@tNy>xYk=8R5losp$@x z*c32@i3iaEp8bqg>UgjQ=O&ibN7FPhublB8?k!TsJcs=}B^_W8+gq}~I%vPzUtm0i z=%F9_#c|{ii!d3A4%dQ_hq1l&{^`z;_5IMPGxJ#Ga5867FKb00<1;Nn3!*&>Vkw6Q z`e2+ySHm9UYHK@Pw_PRdMITK{4>g!p!Bh+$)VK`JP zOk*3gp7<2zQ082|l>G?RYal77wy+ z(SfsUBQ!}aF`iBIDiWQfXN8w|UCZCNTz!Ikn!4t1Y=iFA@2S5@PIUjG7V*Dfv2~<; z!+wlqd9rq#$~Zo|N1>8YJK)m3a)N-MI6JmWX7nG%8MxzoR@+%;oC2~LPt{3+_7+Co z#>(6=yx9TuL1IgAhVVTF=(hp7`bg5*pR7+Xj<()sWv_1M?`D%?L(C3P#iLgX`MNC% z_?3-uS+7I~8YC!a{LMAsC&%^F^YN&&pgNW)+IqK~>44v_`9jC-n9+E@X4EFezWH>2 z=E)g>jw5|O$7`Z&Fj1pg$#yIxaaPr+Gnc3?JAjEA(_3J!2<cn9L1AMBqp;Z}-~FkYFs&Jd}c6{gvljq@6&E{%c8 zT9b{W8}9Zk#idX5Fxg=YQG20qm>*>3s8iu7Y*C)JqHdc4A&Xy-Bx%@$U^8Mm+jHsK zR7rFI1Kj>5R6=Ik@35|$FM}#|3*#Ysw`x6TMlwiRrq(!<0dQot)FWub`eiJ}O_*^Z zkA$|_JR(JRM1$Bq*tXNu)Yf5Gu@I!sN-ag0qgqH>*{p2b>79zIJ8mRS3f9n_AmyZ3 z$H}AsamR=WZs`?k>P5^e)i?AjvVx+BENqvPqnLMJfQ*~jWY>M}xZe|*1?x|BbEx{Oa8|zU>lWFQ;Sc^fVvu z{rIOY8*Lz-nuau)W$SBSy8ZUgLKg3Ow)w+9ZvEs-8oakJXU)0u_TS!q`*(?N-tX-8 z+h4u&_SfAuLJZ1nB=j`tadzjO@7(?4KWu*gH98Y~&Ng5E?%h{k+xqHvw!ZqVfI8}T zwuTED3>H0blQ8&N)|ri;{dV(3(pI_Vj`FE+rd7|BYZ96n@gH2v-RDdZ~h51a7LaVF0Zt$umAM!7k{|Y0asm_SM*q#4)`_9|{92z1`9%JEE z`)s%T-~H{sY`*e)K`wZJZNBk$cYgAD_@A7u?)~7E+kgDi=Bxi^LSp1{b?4vz{q~z* z-uliTLWQwEmYI{Lw|?~Rbp7_5U%&gsceh@9b?dAD{D0p0+TAyPf9LIQ!WV?RzyI~^ zH-E7Cl~?cn>4$e;{aVQL`Tz5d{xO=-#cA9fFa2)w^)EUO zv-Re0w*L8>XcJdeeDgX%IV|_i-~WotOyM*)sc2Q-eB%$d-}?L8Z@$P74V*7-{kwp%`O?oF>e2G;x8B

    %ehpDYEzevxwe)B40iO`p<_g?(+)>r@M?YI8UA?4<;K7aRryrju~ z`+qX8e~(aN^B=$F z{EVRF&6nQUeEkn3In;3Q-}=@c?!EXE&_R@8G*8F3{^>{JvakP67-f`C|L(r=W6Ius z=X;wky?pmi-viTK(&p>m6JxY?9UZbXBT|fc8EyXXEdZod`e(n*q=3Mkm;Tkg-Fx+| z&9`3J`ul(6Z!Cd0KO=Q}O2Io{_`~Lx-X1F$dm7z)`+Ij^|Cvj6!l94`smBQ}o8S2R zkmh}h5XA5O^n03dom*%J4}yK^>kNPE#eZcfFk6E=zkYl3pOEg@R+*C#ow)nP-)dXs zoQ$@<@uNGx{i$gxXzBNFZ+=A*hxgfFYXQ!vaPTwQeErp}pS%gnak^v%Zwr&QZWRCS z@9ur~x71~BMve6CcYk#E#g|k(xEtO3-b;7iczf&H>}%YaC#&68euub5A!i&*E#Le) z1?hyj8wGjHj*)c)w)x6Gu%y}q$=~Si*S~z{HRNTd%OUyYpWOStgaSMBSN89uwtn|A zADm4O4Ziw%3ZTN(Ohn@7FnE1v=`$D^6@CRO#N~)(8^PH&JtHSnP$yIYirsnr)jQw# zJHWH~%CBIfAo4wy)9<~37ZIt5hY%vDDnerE_~}1wz5}cMI~d&7%VJXZe(^P7=)2VL zH74-(AO2a4glu2=iQ^VP;NEv%2hyqyTBq_C|5dfwvr)h=)JgU!bmlv+Z2sxp&0qZ* z(B1vgZ@0ev+uLvc!qr3S)-T___pQ%^@0&k=m(H6P5tMM}-G6}CZ~yU!fueo^ZEXF= zw^^ySzW(jIfB*ZnlGO znT-hyemCnkiW9Cx0sWi`Aa*5k-#35%8@DX5vLPBJZEpbN-9LVlMG{WN)^zh5U!QT* zwS)IiXl!p4NhL7mGvwA5FLR{OgN?WiMXSEp`@8Sr9VBqtQVb?K$uaU03KHa~c;yAl zAXN>_OU@Q2@cE0IUwdow<$q(*Mfana2PtNqCZw(Lu0b#`*x#E!yYsJK7l-=B*Mt!! zt2%HCr@ryGa9G%KlwtF&Z-J^#x={vOSef6=SH5uf#V@+P1O__c^0WE!@3+1Kt((~i zwD{70-23h?{i}2G2_Kzc<&(@n#bt);(;MkgmZV2vGqZhA9|fi9U^`)!n;3+RYgCST z{mNqd)_!2JjPIYvY4 z>+J0l@+mCI?F4cz)Q z&CBE)Jve^}m4H_x?T+eR4)H1!8{GQk;!qHh91^Kt)Y;MG5fzLP&k`nuvO4!Q0L!E< z=Cor91Yq(0w~meMu8{FZ>{hu|C>F4*UPhe_fq&dOdgQ7PFeItbdFd?bOhshv&Leyd zN4@rvHc#7Kg9ce+BY}gEI+@{J6f{%g26eTda|kAdO^+pVTt_pHJwJv^3FXWr{|{=K z@o+xYE2o}0+gt@P@MN;6KD0BcpotO1U@B84rpYU6FcP%aSW$DXKXgbHYieW}2nnWOZ!xk7r01x3UmI3aV0eABLtzXJcjIyf> zP!^~1iXO3srG?9EtATot!#N>(M=$-x&V&v>nGMjuf&l-qXoQa_fmddr)qM} z5>aN&R!!sloXan+n0YqCZPHT)5hpuY=Fj18n34}c_?z>b0$(z@XGhJ?IEI9E-(l^a z!{0>V9M&+w_;dK1KZn1;mP2R$F!PU0_?uHiv+3f-zRm)&%o@ewC#hQ`j%MmCXYf0R zzq$3xfeBGb9hI}RLO&xXcc-PIK57m+DvXray2LuFr-s;wvDRZvgFwkb%3evLj|3RZ z*X&p6v6?D8IW1Kj4(I6>=c-iX6;v|$wsJu~bzDQ_OHfW7;lZ{$K{>^ekE~>p7m^^2 zn>>gJHxduWddoab35at<7TCnoKq~SYszdf`UxJDXdBEH`*IA{nNn^^ouU zJ{z2w5*A1J^Ti44bQD#vj5kx zCsbyKz4GkgD9fG-7E5l~4mL`r`f@KW!Rg1aeUuh$&&skxW}|dJnlkJz_8cKw3Nkc5 zk%Fuz1z8(r2M)07Pin|+z4ZY{$)*E^5vwF&AV51O(@8A z2$dbkSO^b2UqzmZ17aUsU%)lIzj*6^I#FvvSvHzM+i~pbjSrcELW;9f-AlM4PN>bc z(VDRwd3D&B+6<3!!b2h!r8>fTXSIYJmNZ{-if5w=_ZbkI@Z*h`_+jQ8-Pm`>PBADX20nrJsH|3RtxG=6-lR2?sHD6_9plJw??dp7 zY_88i#nxFNtSR)1{&!mio0g(Pt6s%6?>|aA=6dfeqE2FUv-H3I5s(g}p$H#>eH3m! zD^W1&&#}0gimczh^=eYT27XJmX3d)Z4KXB{ZlXo{^sV1mgp1Uz*Km^~PO<6W1_>S9 zQjg%feIhaFW~u9`ja$Ew${RJ@t>57G94q3$DO7Lwr+l*&HrBo2J;Zw5-cj{--rC2D zhV}_R3fM;sse<~A33nM7vYjO!#?qa6YIaa3_K6Onc$WNQR!vTz_sbx@=*UkxZeSyk zs9{6+cR8~v%hxmZA=RW`g9$e{7K|FXSu9})<8mRWheS-1Q5E4iadKrvSD(;--ueyG zY@N~TGE*+XL%qGaAUT+D$k?RI1&#QME-N1IDXuC@i%Iyoo2%Te|fcXkcm|Fv7I8^bpUSREN0? zT}PaOZ_r6Rg9vYJ^`V@lS4+5js4L{uaS$rqek!)jI)U%E4piu+L@&{lCYWt0GS2of zsx@6&bb59dhePc@E9w5Mq*LHh;=yt1B}K%aV^lvpCEZiV`r-@n2f9i~VM0HOnvt_o z@NgXSjID-DvApP%YjJ5Y;2mm^D#%Ndl*G#LxGjvn3AJXybCKduNd?v2NS-Du)oW`h zLU1(K=a%s6W52|58)8_a#4~G(gq<1U4e4fK1}8yGAJN_E3^*8=DXb5{fT+%?@1Dm< zUT-_8@8qF?tY@mY;IiyY%nsx3K}|d9?}nVVjcV`)EGZNk)#33)C%ZVM1lbh%VOuTU zk#1jsP=b}&{Lpwl!w6cugI&THXj}*rBayB7HmSrr!Ia$kpSG+DdSd1*g8%Cu+pHkk z5wj#LG#8pf7B^%HPH zYO90OR@0gT0tB;my8|`pmOdvI(je9dE>r*~_uU0Ma3sQGe^J%5UP*tTOj?_?UfM#M zVI}4ngAbNw9XJx<+Bd-ZO=x(WzH}f>cNXL(rL|gX7j+F(Z&Xn>bq*Z|3p%4$tZR<# zyS8bzwcUuyju#o|I;JODRwdN0X^|tQF{%uiK-p&>;~GIvB4PL%WzGWCVEW^{*H7!X zhIQK{ae*SF3y_qX2zD*7Z>!MLKEaDb;~Rz%eOv-1m_Kz>A&QW4onp`Aieg3U8(d#rE1im2ew} zFylU+z~KO(01_f?81@Cp^&pu47Uv16hA^JJri+g);KlZD9>G8i2!PJ8?O}hUDv9io6tQ155NjdW(jja(`_nS#~@bw*{rAyDg@IqRFCsuJD=e^%pzB&8kAlux(fp&4V zc7Erp0lV3ynL#GasJ9E)~g{neHr*us#&xJ~S8Bhiedu>l7kHR!fD?S8OOdbhDI>OK?lw z+RQ9U`4;uf&h9p|s{%zdY0JB1UycE>yb~az8b4q#G!;*BpNlne(FZmNAW%%R4;}y; zBLKLx8vriF0J!u~06>xs+@btM#bda*%);^E0>qUurnImdC>CN+EbIh|j}B8Jw(ti} z5ZA=0JrlmV8w{?-Fu1xs45IozU~ZHoh8X>kgwh`b>pwe&@RxU+?91_FU*3MQKdh@T z+J=Ggo_OXbR{&jO)oc*E4W$9vsf*|kka7DX>wS~e^#fR`k2r8;nwj+opXYZ2*?bJL z`JF+Q>1Fy+z3-7u(&_#C_xG~spH^pPAAfwNw_n-{+-XeG+FyJKGL;-n6og|Ji1xYA zdv}O-5mJ%VF4|d7Gt=xhdiY3j*&`9a8o2=MVk~6sD3+BTf>?Gj06PN7sog+*DhBnb z-9R1H`W^wzwXBkzAF`C3N$+z}dM+X%put217YokY(yRv_d(Ddm@@XJ^l#=y9tz+6* z4Q&4_%l^X*|uE9&fXXx;tyuJCckF}@gC^7Lx~7<$96-AV=+P; z+ZiETKVblw#Y`u&mRZWIWL7iFp{oy!B#!PO`$2AhGwT$hfeCPf)RNeQ5)YKPqRPc% z3SVl`EivIsKHtcfp2YU7dD(4~N`W5xR362KVoTc0N|nSO)IFR;9hB+qKR9#4^+1S<$q|P5FZtD=7CN*>%Um&M z4KMKwPTKQ|dw$vnqZ;j$pMXTa))f8xV%*Oqd)L_1nZ^qJ-M%wSm&deKwK_2eDA0-C znw$n{LUyPxn(k%l6T7q_c?OWLJ_Mc~?UR>d4xrKQf%^ff!6A%w->NYxGdk75s z&?t4Lqp>741uYQFYX;J@K`DUZYG3?@7C=1X%>I}uL=@jTaO0qzM4$|Hs^h0PMMJ|k z-q>PxVRmMA_GaV8O%ws$XODJw)1R%Q#fa5FrKeh-Wzt90K*yR-5BQi=1EJP~H_Y77 znTc%T=JdzRCKB&+9PXgu%gCkwDcu>COXX7eLQVhe_(Flq8z1|4|Jv$8uammE+UfT9 zr!Ca=zI|7&T*+Q3W_#OQlw$5asEw`*DQ} z*=i;4axG>nRZ^*hKe>E0Rm>Jj;klg6)hZkLTp?Rr%#l*5<>&I*e65oUd8!)K#fe-D5plDwWSuNi|nm%<-*Qr_cFHajudr(&u8fRIAZotx`NxrilWn z#X^c6R12CvV3w`9sa&x!T|ZIFXG_IWDxa%obEPVCSuK^A;9@qHE6!z0d5xl4t*M{I zGEe2XY^^|kYc-#(=fg9zR%Jvso@|kBFdv1oCX4Pd_4VqUFJ3K>mMfM>E9NvZS3l23 z>1MgQn9V739`NQ%v{$ML0n~7{aJ~Q{)K}=vK1?CmaX2JplU@NRQXsh z3d}&*btmp@De7yKI7jii6{ldW?y97m0IF`+j+QDwuC~ZqwL~wdf?lW-mdj>7M+fMI z`c1Z6Ud_`9|7>$2TB07H$1s>L>tKy>Fkhi8-)c1hnr3Z;zVek?Heal52(;BQ*hv|{ zRw!kQMdqgpG88KSwZ??;OSVEuS;5d^Mcpq~T$iDqwBq% zYz#5sZ(#q znf~{E-b2}On#>@B?F3i^ktK>cBo7bY^FdOI^9ra7ZXg0}Gfa&cl;CGG7B$x^i@c;Y zB|}Zp&LHEb;$F)SLqKgr&=4dqWYn0_0Af@E9f9!!=En*Q45kZWFj}7WRMZ}+7aU0y zNXX%wYx@;sT+esXjm%bUj}a8XiGmD%s{C~kmemE03R*{lLS?LI0Xu3Kqj*sijWMGt z(LdMRaC7>p@;>nc+~@J4oKp^o(>@gA;=B)70Iq;^iLO)O45;*DYO4<$0`;wZJ*@bt z8nl3;`f$US3|(h^-enoVnE(MhuGF3r8UtJmRD<9G1;Nl2TN{{#;uIWKL0yejgO3`u zrK4>1MBX-~oleDfbQxj^?vtg0gTC;V1zwU_s+jI5tVM#h3N}LkG2v8f0+Xm9_)+*d zr8X_vI&Xy{98~3k3Iu79+Mzb-cqt`mCNV;!)%@;EITNTm*jPwWhroYP-9=Q01W`s( z5#0gY^t3(#x)DMgiih!x=0a`~7sB#fD&`Y~Zqfrf8*s-pwPCn4uk`@cfIG@#9VYvt z>qH7@3$>u~B2rXNMaB_*pn~m~T1q!I;w#))Nh=I>aZX1iO@mP6Whp%*!$Ewv?DP+G zKn!|EwI(|uzb1Rm%_`o`{92Y%T%J&{YL3qhKAfEt0HKtbc1F;E71SS`{VT3LA| zR)~hQOeGZxP*4MIv<5UX2kDGC!?AE~Djd7N*7@n}zncfrI$B_~u6A_&;Cja@v95Rk ze&>BJ#Jn6;c{I`1F%3_2INSw?Fh3gcR|83C2FE*G8#=~gH9$^==ogIHXC69V3tCZd zx?Lz4!d)h;a3CHn($za)hiY6qv{ZqSj+RryI3Y(@Lw~!5NwTiJ7c_%V9E6Vkz`>px z=K-jb6A&vie>t{!Lvy7nrEH9WXZ;!h4m~@<`O?r?$JAzF0J84GfoGWD7B!7(21-V= z!y+{;fmd`FmX@HDj?ev)NkaJU>^oMr0atgU_=v9kATqt@+SNZ z2W!tR&MB5Qsob)RXQ=rS6wr~9^h`QhO0c_#B(+e#8Ju@V)3-|e9XRkGzRhy&)o-(0 zVe2Z>?rfGbkxPhG)LMf9cwgMs{lvpM8fN?{dWi0UsBGAV#7bc+@5wvuH*;2+inlBLD zK{a*#pb?^3z_n(W2tpcyHf4o4tATJi8^1lRT~4p6y++rt{+qCYyG$jn^mR*T=2q zaW6iyaFH*fbT_J^efB+{#N+cyhhN@1srakYL*MZ!u?>$2TZfb|AQsNo={9f#iIA-b zSUz`B9(zp~B%;f#Md4~iFlW5%_Pf!Cf1Doci+`>u-ha}bXf2St)tnn}zgl%SfBMtu zt#IoPH#N0;da_B(mXCiY*+?l%b zMe6S5`qSO&x0e}pP#CyxpQt^`yRSsgHZv^@WO=f}-H-JdrfloUb!>f-ck4yBvtGZq z9z@@{Bh%Jr)|~S>k-f=sVylbIzsnEPvQrlLbp&@VyWIxXG?r|$%HKln^~5tx!Uq=5 zPvP{3iLDLPQoC)fR-Lih_37|0zuoU&P0g>~bG8nxtHitU^$(1@uS#AII}y@sz%W_x z{7yrqAc(8{pdsj8p{l{dLU4waNn9WVv(zmbUKEOYdP0inh|(kl4QzJ3&P5wFDqTBB z1)rZ$yG-;A{|3O$jcuE0h%Bq@gl-riaqL>?g=mn-G&$jKsnE15rqb$(<@_8l8D0V}q+iS(*ns&FwSUA|pIHnlg%S4^XwJCARDbciqN=#GG28cpcbmdp=MqZbLNWvP*L~~ zT}*iE0^_p$DEh$oP7d{f_->)tCEwv0eK5e1CgM)k7N`8W8xOradR$qr_=`DEV3LMK?YD}{dJU{QF($!%!PI*nd zQjJq_`-*tMI`dpwrf!I5>cL(yC597SMO?}R)40;*qne3E;z@T@jcoF$(oHF;c}W#U*?r zXH4g^8;KHXp5XpQ;LmaLw+DhN-l1~JOLEapom*lFzY0?=s<~4O|J&>!gszRiREc#f zkVzseBjzC5SRXh>ykkhVuKcq?S{knD$FKn$t8-+9h;CunRpZe}=U>PSvGV9KI8|cMP{joe%-h((3dFXjybh)$&q&KWx_!(!ydq?PScKK7=9f7* zKc?bcKE<=`0gGi5FLoB3LsrxTG70$GU2UoRs4p2CMa#SjbSzgdWE-|ZV6{|1dp+`11=uFU)PAbV ztw-5YMc!3Ko@8XTr3St1(al>st6Nm-GYc3t*@kld^zc2FG-rPu96!piXy5I1mLInD z*5>HiEKUaQWY4I6o2>3<>HPxRmnroXJMT{NSL0uPS(%aS&(OLAy<&Bizk0KIWmVm= z%Vn#!N0*mu^SH`*U+bvP2Qqli9bd1y{J{6N!i1+Wy9EYoXc+s?%FC^?&P!!IziW@bZ|wQ`raJ{>`FJ&c@S^7jOIB zHp#y~U!|O#zMf@AM@L(`e|r3Qqx+AJcX|&W*Sf0rZ!bIb-rRBP-R9zO&}{Ypxa+?^;r?x> z+ikXQJ|6UP_=!r*_8{%G>jT)O-uk%D$H$NBw3_Yvd9`MfzS`(@TCJmcJI&L6ZE15B zDxR9@bAPc_f9(thi@E+z>o@&xd^5(`s9!G4YCNXz&9Hfu{@2PpK*tqZ_4duMe)9=l z=+^uF=gxd0kNfP}O6q?+_ygCMJ zFFGbjdSH|_DqbyLZ@f=MM9qiO`LF(KyYXM`PP>;jh6u)|^uPYs|5B^p-tT+FTk}SU zx?KZvJH5J`>`KBle4YH@a`)p;|6ESjXEJbq(CMX%51p%FEA21Vfnc`4n5>ufdY#@u z=Vnf2NgujQdnO0w*PHD0GcD9}v;EW@r1K=(WS{TXRd3xCr*IxpE}bW z{L0`5(#*1}>f77blO23!-dXd>mY)OrU(xoTS6f?+Ul}~$E~DE&ua_>T%*xM9f2x;X z0c5B7(yUZt46|vz`y3Yk1t3gy@z3qe<)4xgp?%I!TfLlCRiDA;$jJW{JpOAMBSEgV zW(}Vcia7^mrj=S>-JX&In*8TG_-n)ewbuXKTwPn6)x|skIALO6FD*|QXYY|c-}G09 z{MB~Ijg8fxX^eeVt~c8zD%aLFrnJm^)6BK}#Q}e@**{k|Hb2%U56GYP;z`q-}%oecI7Gm$A*;lA! z4i;o^_R?1U^>Y^IyH&lfZl^c+Y-jJ)Rc|9V?K5-zptAKhs1Jsp1Frx1s)NigpADr3 zZgp;0ZS%jk@l8tY>&~1ilSCc%=Q6d%(&x|3Q8kZxo$Dr!>ulJ59Qgg7Z}6)>J9_%i zxoaK~u6w^r*Mzgd^LWF1H9qhQyXgn(1Kemr!5Ekg#e*d2>`!ip*N21KITb#oh_32u z>mN#*oJP*c=zp#>R_n9cjzcx+OC$Y6V9dfv^8y9q-dH!id9~(EHs_aJucuSihD*Nj z0MBXTwmvt+Gs&=RVwd$6s$lMd822lUF%R%@t+!_P<4S~rKCaYH>%GS9$Mpt8eLt>s z)oBbBXSVppC#v`Ab3>;&M#|fUPd6ok={c)-du6t-GDg%50c=t3|NZihcKyNkz-qOy3sw`^n!U7E1BM>| zjlZNXW_j5c*rRq?zfXI$-mpDrKBTqFUgsIhpw@qVxa_oQwew<*W1^w((C%LH2}Ic{|T0Uw?{Cw*6P#=*Zv5Tf4v+wTUYZTTOL97FX}A^ zr(*?RrB<7A6iey6J~w|?kIAN1egwVOIP;lom`c~#rkZfrEx8*8&qb`{^M|fX8MV8>HRHUhBOZ(~OdDt+ahJxaDC;X!N?-PSy0GK4_>+ zmdl`j-n0tM{z0cvZ{30zK0(n&T@RC3_*as0dHH{cmJBpS%s#t%KXOgRW7J=roOX`L z6P%JO{~6-1Z!ceMY;JDB(Jg0<{%x8LYAZXEZO#VvlxL2Qcw!Rb|4XQk2$e;zY?01i zsOfU!>T+{=?f=qhQb7KXE-H^HeLyK}o~xCOwd;Q=r5@^Cgy0{GV?*|j#c>kGAB%Z& zjkhUtkdiDfCYZs0EKcW*P%^`lp>*u9{IQrbk$K5klDtji3Fv+7-0E#sE)C_A`8-xI z0x_2W^T*;3F3lf{lX({5ebibe@Azd#F3KNddQ9YBP%zFf(aR{IftRt_|HmQ*UoJI) z_gpH)ABz)dKz}SwrEX9#N{He)pZAo#X2}tMERJ$w@Wu09t6nGaC@7dr-l1UZR`W6r z4*an=_LA*+lsv&(E;EU@u><^v1-C}%oCpQ{vG{{;p2!e;k>17>=HFl#hhl=noz{~q zh7SMfZ&67E{B2;b@#j8$mC;bSyh|J2N!Gw^Ilcu$nkRz9HR3K~(b?!VyBKsbVlcO; zAq$;9qcxC!J{$!qD6PpF8QeBC_-`bA)dy+Ov&i&O`-8Q9+LB9B9}Ie`71XW1)D+F* z0a#SQ{srv=mGymlmnS8!nemf!I`>T+hcEvibjQEXm_G!WqjnRsNEvot{&AH)H5+Q4 z@hOj@Dt{h^9&k|m)5`8HPC>O)D8|u=>R5-#L8Y264b=+8Z>6M2t!nYKb`Txzo<+Oa zYqj*J9FgjZn0#nd%&&|(4=ROrBy7gZDK>&2gNaD{ZymfVy zeW_vYszxupfx@J_bb2}8<+*19yoO8)3!~dhpVX5tle3e9sx9zwe|+qK(xC zcTbAAp7_}?7F}V_sY&fcbgaFIZjQ|HC&!~9JIq$k*1qhCKph*l_97bBUKAnlJ|;hV zKdbs0`q6U)aXP)`Tee16Jg0$3$cAy65mjx-^vZbv2GSmVKk49W^Rb>yCA4yws3k|u zk?2*%@3vqnyRcqVC>@IZw7iGHhdxDxa_O*|`>H}5_Tou&TB;ldj{UK)mQ>CTPwAu) zKpS@!dJdgbPO22_h_3^tSfzo(XO`BLop##UoxzU}_1-<5g!`D(L^AL=hP_wy0?1XcPq z^9Mbf>szy4lobM8wHNKTV;ri_T)9~(e33TK4X7#AUbgMuW$nV&W>jE_!doK%Uo4%W z_Ok4fWO-H1rs_D?YIT_GIPIKRAF3I+Sl=8_ggQ_E=%k+F_6#_qEKte6>$uwDJ7ZWt;2KOBbommHgK#|thKsfdB z`Pj=5q&Bm(>?ReBf^o%@@~qUA{v&$9pR1wg5%PFKkoYM*9AimZL$bsXp5d-G^-3l2 z$+cF|k@Ed`ap{x2S#DoeCB4duh32DTal8hz4>iMT_BQAwte@e7VuVqzSAWfk>pmN_ zbR1&)wPCY1N=$#pdg=B$T|wFGN9biUo8nHVmDb%v$f9_zs*_f;p@qFOY?~72aWn=D z7?R^DmFQ|VstQH()x7q()7J@oy>-M5NAA1+ycdm0hM7FEyStlja zWQBYaOlH0cE=2ndXBRH8DtOa8DJf%q$W-DnGz)Kg9OW=33-y|9@U8;=N9}5g^tuW! z1)F#;1p{WDB9uLnjNMN4r+Uj07=039rfw+n^{RO-6SCcT=HGJNz-go=!khi#iyaw| zJV&|+wvZE6lk$Ftg{}j?n~IX>r>0|T2rEfg2ojj5Oc?)`t*$49iVXu)f)||SHR=S% zSV1oh{n?u@k`pW2vBjX`9O%kVd53N^U?{^Ay}7v-ku5K?tiW{HWIn z&}z!jsi{JSv+!5oyfZ%jiu$6RrSJ#`53)pT*m(79Je>5~#b5;D4Ck&{M=GB!|Zg=t>c`bNVzGoQS(t5)4nEL`c#DUqCO!GP~$a9FD zAVIzLOg@hW1q`Q}1?YK0XvvzYX}Q-#KbsVPk){e5@FcAKu0eSQoop%(e>c(bd%IJB zA+9>l#@6mj1|}QJ&RT%#9a4`ruNYzUoD(ih%;Q$8-ZgEWanWc58p(|rEsn5*G*$+` z89J%f8255Xn#m32?W^A@H78#ai{+)r6PhSJb!W)6%kMvYcKsJB$+D!6Y{&pDB{}sv zPOE$eDXsKc*mt(|7V6~*J}aaxquMiJ@mBV7rNRrU{MNU#Md#pzY!(XqJwe8jmy#pB{PpiE55X z9lV-YA{d+49VVuAH0&v@h^Gy~nwFuR2SH>6$jqA&7xrtwXQ#2=q3L*V`g&^in^ z4XH?lrYB;`IituTDM6<8%(R`Z!S1CH4W??H0F(-CIuMjQvIg84T>W>NlTS0-*2cE08XU#*_IgeMwMfpPVqO#Rm2GZ}p~7iS-e+TKRGXLHZpt z16x>fqi*`x;DT5I!@w7)|5%b#s-)$&8Nla-lfi|%WQBI}y zj)`L;MLjVkO23~DMb^vVVBmg$N2?^TDOMy%hDrSBLjn)xwVY(QAX^&vy>aK@pwsw`V(_$wi~UXDTDBpA+XJH2x`7z^G@%X1L1NE zqs2^Wj1QEp=9Euo=Dpc3cP>55+l0DE6$xV@fj?$6x&e*}VqsB^^zcC>9P6ittOwUP zP_l7f84mh1p>Eh>8lh-Z#KSi;t52Aii41%YL2$K0oO%eP)WDkMK(?OH8g|aI5Gqj= z@Ra{bDHJ79Swj>5w*W?!~wYLD%xfP0tEg4T3$oZi--n#SH11k1NV`1-35A+P?V za(3!|eHQAl@A_PC2%TxJ0jG%#(?J9)!sfkE+i5U(&>B)b{$S4@jWS(%x46TJrG4|} zfrrtnAk!y($vxx228WBkH4o!rgrtLXRD1H(8VGzF4d}D+Y_`RN=%UI;#nMmo!gHZ` zP&_S;Ui0nvY5|T>Cm!z@y+%2pbI+I;O^eDGCLx`CW!tiSlUBnnNp12q;L7+5C^BSa z?+V3-4FhMnQh{3S&LFji(Hye+7Qj4lcFm%|!6rKLsJ_@b-Lbr5dJ?h@qAL7RRQ6TP zdTm}+t|jZepHS;)yo2I(gV>yVq!S(4o8@nRp_(qJmAn@{@dQQMbgDPUr< zwiWSWr#J9uv^qkYi5K-vePfP7im+g-&^Ozhy(Y`78kjLjs$r%JoCx_EKa?Q2uA5s1PaL5Pm&nr$g*~QY=@#v7P88wVjhnIjhE3d3XXDw>9kDl1~JC z{x&%RHGC&axn2lzSrhUH294l$@3r{xpi|gZ*p%RdJzMO-@Qj z+FuDJIgp8<=Vpeo7@9!^^HJ9VXjk7tsLDcCd-eY55KCCRXP*x{?a6}Co9D1)VCH~l z%)(`E)UfG#Qj9B=Q~C|>%tJ_ZJZDH%NweLxIOY#{%c`D280ux;gXFZuog3CycfLRi zW`%~LKdUD&fCte-J-!kx&+(`V)NWM|fscRV`DDbU*e;nZNYjj4^QCI970MXFeK+D+ zzvAC2fsW?D#hnme`=LNR$Vd)T4&o2nq;Y@5T4~l>XC?}%qVR;jT;=BOCAk6{d{cv| zZ*2#JDw!UddR47V>Z~kL-9^fkv#0s#Uc#rb3LEyT%Bj>avkKW>?KeXd-8|H0t2vPB z_pYkIVq1$rn2p|D1liZLAHSB&EHcIp(rMPGEHjhtt$ljs{TZ)AHinw&EOU}`{U>{5 z3lwWX+l99(mO~<0!QSU-bU4Z^;N|qJTCCRMi~Ogfv+7lcn4;Lu33c+WJ#uJ zvtP~O&edLUxPu~>*0HKLovRQhDws;}f#{<)_3jI379zq@n)PX-k%pcm$NQi6EKKS; z*Cz~EUZ}1{MKaUAm8vD-_*QxOpfh9+7KoZxby|Na`FvUGsIa{m?}3 zHzI7~>985sY{#!>EW%*#k>fmFq$L`@W(jh_M7P^N@iQ;Hc{C$48hb35j`meDw+~my zGPOttp?5x5nzCjU8rm6w@Z3)^R0n*!p`U?e!|Np-2uvLS^|wQ!%HfTC1kBFSD{sm@ z35h}6ZPnwVoNSM^?=~WRNFy~`=@GFU?SE0Cjiu0~OqPU<5K8r!-@z{(o}O^H12bpZ zlnIqxXoi==sGM~YeTz;l>q|YJR1S*OP{;fg6-H;Ld$qkv^)zb?1jyu_3h$(kOwp>B zhNL_9eX+(o1&S$Vd+I#qP=N(tfkvWy=5X&$pshCf%3BWM7+XXiEX#zn4rSyxNQ6Up zaO*f5)ZcRB?pSJ%R}IrR^8R*&o7#-BThHn;n;Gj=kceGs(*i8<19u`$UUBsG`pPvEnvTSC{iy19A zJ!X|m*73K|)LzVBA^A-!m_M=sgGn)}|*w!L+u4k5RgFV7r_uiLb?`73-v1+)OUDdmF7d7;2FP_Can(fP~ zA@XI_@v`c8S+%)NRSiAbY=jTbMaueC@t}S)BG;Z&vnh_`m*Nm+HEgmj>tC%y&KN6 z-u1#Wn_mx_`8XBK?$sha(do6Gf%HSg(5X!+>r^lJ;q2!p-nDr+xxRe~tUDgo4G-_s z4?|eBYzVy!0T>4oE|-ho=55N^r!4;~_kCO_ah|iD^RH7GV%Xj%^s5?rtR0OmMKkvV^`-t2}LH6&lskD|Ob_1TDMuIP5i8EY3biIl6vbpO;v#=9pL>;Qnm>HJcFyxCu2{w4D zeK67s)D)~`S($p4%R_#(B}e;vwR&DTQT$Y|c|Bewev<6fj?aoG7ojKNzgqKnp8+;S z6RJhpMagTV9||-9b7|b-8sSy!6?L0C<_aj4*&PR=Tv_q?cSUD2MUs2wk-ZtKpFMd5 z(Jx@ohpG4ENBvftQC9dto8fg$E4Z-6j2A|0Y*ry_N6Xh1V~q`!kgq1|)O4HJVa}3$ z=aX8S#1nrU=G1;+Qt?i`s@bYAqX&}*dXPl$iZ&g6pLz|#z(k)~1mSUQ3h~N@bjp@2 z*D6!TruB25>3Ou|jM9^DVHA_B8fElgLN*gZD!uEN$)yar3}h^{3B{M$Uozxoe+a`_ z=M3k1R}ZFjvf=dEqf1#Yqn=e)MDWjn87E$R2)AKWJHx%J;4p-J);bA2sGHMI1zCXkrS6NtP7X&2ra`A@#!vw9P6 zzJSB00?Axjb~`vg>T&8l!y&@C=Nr0RhCcLzOmzXIY_MDhB~p8P1NF8euxnHA&B@Q~ z-Z6Gj_W-H#!CWSRjU6XTVw+Lc|FK$?V?NUAHjKIu*er?}rOLg}YEVII zpQBf!Yl=d<8O3Vo#aVNY-A3)J3`lreI=wdUgN$juyIxf=OwTA#N;WPQr$JyR+Mgp#vW+H7)c}9G%bWjsh~L%`UY#``55D zb|0Ns|6Ik~CXXpD5B_KI2!E3tvxL|GdkCH_J*C}9%y5~=Uq1xDjH&hhB?TXm`QJyA z|Afmx@CB4;^}*To>#g;b#@hADuU!Z7@5Au_nRj%A6De9J`fv6PEz8^6tD75JDdH^vHIX!BI|NHZNx~H3n2-ZF;E6a|*lNqR&TAoKovVycA){-9ZqpVr zyifSp_m<|~Wwc(aU$TD)^COUJx;dZmsB_Yi1 zY~?eZO`I^E+tunsS32q}yT_6xFh5>i&+Lq`y^{#(X_4MD)oQlu_*;h2IkTGFlAH_4 z+5{whZfCFzTN%OQp*W*u$JDdG0RnrK7l+B9{63vZ@k3@x2f@BkY)eEpfO&UQ5KbFq z0x3|`7F((pdlql77F3*n{Egy{l7Wv#pTc6Z1vEWa@dblc?WZdJ2!X9Tb4A7VdN629 z&W@F9l2@Dr>WyT2%kd5FUN%3+$9S}ilUd~kT}adMX)3wGN^N>UPV|2Z4|KvECHCDP zgP6#MGT_wGY_or#rua|L#kPh1fhJzwT;ANezHa=QCdMHt`Qh1cn%0;=#{B5o7d_`M zeBs^hQ{k_?{iA%=>L|~9qSWpbr1~gVxfG6;=y18*Q@-8xJMAL(D%#HARr@Nq9<~BQ zO1>YhJnPC-q{S6EVQ+qJKzW4sT4A=cb}FxAG*(&HO;d1*OTeKTyQ4r?)fC7Fvil$O z;<^3SY5iuu-oCUmvkhHsD1@+Z?9xO+rY1EAA}jC0o;eGbJ%*cYXrO~yCMc8A9}u(qrNY0U3b28u8wPFP#g{QPN6jdih){gtpS zhBwa_%HB|#iZ6t3To0oju9P(5_qP0LC3f2kWo*&=?Ue=mC#&Oo0O1@Sg?CL3#(4mt zUDr>EPzy5q<}c3Y?a;&co(I5}l6b@Za`HUCEvzdww1=v5c|-WI!YT%&h| zD7SPhkgYxHo#KJE%6GVIz&K#C*aCJ|-sc{d#%<=b4w`n1GvjF-Q(@KlI=qY+SntC0 zM1k0Z3t?_jUmu~(AP_%eoX|4&s&-ldS*O{v_b0;XqCP6&viemJzClBYQ^-C&kyK)y zf8B1~urk}9(k#S8{OJxa3Bu^iB4gM|x`w(IhgcZG7%j@CX7+L6}RsBa>8YagPVhNVd!;2haV6nNsG6cN4A` z{N6>>ex2QZ>ceE?5D1$Nx;uwtq4Piy%g|uJ{HvxNTS;!0h>A|JSzj`; zWYe;dr&^C`@_sE5<-HmNw%C%$p<&iw9y?M!#LauX%|hT5;9(&I#^F%!cNlrczIsy|-fz6ps5&Dd2*_Ix$0eMQ6a~bT z`-(>U-bLakMUC>I#ALk{v^!a6F3u6tmRi@MWex+01kOs{Mb`zmLq0k+$V4e>yUOTt(Kq z*cB$)d)h;=xXkTfAj|5ZC(G(VqTjr5dl~K4+;qCQpOoEoF3G>hep~FwFHOV!y)`)T zcn>(c6ZZ4WWu?tEC;9Cpq(87xWTB3X`Q6`l(IvT6(D970S*fG8ui3ViaY+7y?@tBB zc_vrR_HRx*UOv@J@O~-PH45xP9dxiu5?@XEUZvHZe5^DrbQ)r99(PYoQ&sUVie99$3 zXl(Uhn}3F2YiZR-TUf%8i}6vB)ynVDj<+F$h34i1_4@crwm-=8EytMUC-A8Balb|k zDbpK-dmXRqt(MH&s@|pn!R7!qj=Fw;4Zs z)3NLGW~-GkVXl+YPKZxrf|_rrwK(M6i)hcZjGTs9&)oW40Zb!36wdB~Jl5 zE*u>i_FHhQsY?UohR}rvBsbQLe7&0|r;ou;nJmyLHELaZW798TY?D$&Y2IoM@8i;3 zmmXoF8#lf3#P4_01~;DMy9hAgvujlHVi#e{l) za!X^VXR0V`?)QMJc}J6NWt9kw%HRqUo{A#oZ(p1nJDiHHPVRTte6@^s(tT}Y!^xu7 zvsR5=8Rsh@E6U`EQiTyARx4u?bxEr(t+SkVOm-D%*Oi@5bva`n9o<|DJMW^n3qx62 zh#o0~+#~5#Lbc^6IDG|}h(@UzhqykDS9p@*cUPFF9Cc-U*c|{GJ->$auATy8hIv-5 zZPAt58jrECBFe-_BI&g>yRn6WJ;bwgQTT4xz=Y?~C%$sb2s9DUyA&b zgnw1?r8Fwov*Itu6qM}otNpug|L%NI?Ol65w12H)#U79BU$2-@u^1J-3Y|COtef2^ zxr_CgQO_gJ-`Ln+nz21=*e^G^=Z9V)D%O20>#n?S4G!rc`5ItYNRrd*9UJU#FssIR zR-N2Tyx1nl*~b60Nd=mSPrmeXv&z$I`109l842q~uxsZL;l=sV8R$$LvQ+;vOp<81 z?v?%qv>p1?4GkMPG~|f8_4k^jSF%f)z7_p>L}-$w5K zX9>(8`)yH$1!iJ0x2V?Mer{Gbv5A!{7cS5_ic(iJrD(Tqg616c6Wy5lSwtbB5*PS$ zM$P0)MZ!ohxOUpHV$)gC=Wm zBUQ7#_pyZhdrj(Nu|GWh?soHl)H z(+O?5H*u)=H;_rlZ1;7o!^Up3O}Tz0}9mbC0uP}1uN|p7_S&@`EstB@1mWDBs#XXiIse}68CBDip*3a z7n4>o`W9+woG_oUbMrsB`2mY)xoU8sP0v3>y}o@c6(7!%(~bSdoiEREa=di z63B)r+(?lyumO+FxTuoKz&DD-m=_&)nSifwzNuyqb$|3v8UD2p807PSn|dVdm3X|) z$^4ByhvG0HX0{tW6@U>@S4`+$|I+N(@aX@RzTo6V{n+5liiGR|ku{0FdkM{Am&q~I zv4%QPSJBy5%`9tCSHuuFdr3*NFCXMTQu~~S#l={GI{^W@aDBB9ztMYgWvo}ufCI!c zL&KFDTXhkuScA8daSr^yV#j(1$1qA}| zScIxr7ne@1(1Yl_$H(0l>p}EtL*{3z9->7HnrFR0 z+Do4>{BRL{8#g{{#%*gnh%eOmCb4xbB+~f6+3-)beTzFWWtZc&^?KKuw&HU&eU3%l z=r%r8(}D!S#rH2|82+2Zf37b#wY-z!miW53(~FJOiSvxOYS9d^3PE(^t6N*FW$HtR z{ayQ4LVLFx6D-$a1s4|?;wggG&A1_4445!5tvCg)jRFUO%j;jZ_L74qTV1JPci@5v zS~x{ayi0Cm1(H)JEx?H0ieI)8sT8fvl_0jP-kJSdF(vTkf;emM-Rj@Fl7-;K$I-oS zh_Cyf4U-F*@-+~oy=~zGh{kqpRy46GtThY9>vsiLtG6ZV7W6w?u>)c7*@i*r2+T&$ z>L+$yZ$}z|X+9WB)e(R1>5a^F6MrZgH2flUu~Wd9=4{ONE_z!AH0I z#}(K!`R>d8_Ny$(>eikJ+KPRwUF>KozZtZ{WXV1*Cx|<(D<&uT$sDSNy$kqcKRSiQ zg$>s_sn1gsuw^SzDH5R*_}3W0#F;7(lVoGHZg@?xZy~~%+*5>x|K?G8$&8Sdc-wk_ z)HLlGf+F8wo)JXZV!#I_k}bBEf`FVx#r>wB+SFvjbUaX?qZEJd)C9 zf++JH2`^@KNp2^9n#GXPws;pW7EQ9=>||;k=#n>6x|JnaQmBe$*TKUb$?5v88unQH zsF(;juLi662FqE4X92`|lMP&a;ZLr`y0>ugy&MPe%@j05TOI>rY!tq{G6*iW2HEr` zTL#r_vh7e|;@KePQoI0`bcxkp^!k{mCQy1_pI4BW*#)){>c&%YYx2F)xB7z^I}*B- zc@;=7;id4&)+oSl~Bh=QdG%)xV%hjAtQyXn1eqjCxV? z7q9!Tl>bR5z7wE@uBka*T@={ph4r{CD3Y}p3Ls9;t?CnU-=^~^Wuc|!*7!)kqjNDu zm{IlK1j8{%02gN~*6s0fL93&}+itY8F0L=4D2mZe0Y^)CfH3<}>0r??TnVZB>)v_BMverxmEV!`1q7|7?0VXrdD4#Iw=1 z!|<21$8p`*;N(&SIj-AcIJN;#dhxBsKYomn2FDTa0m8AOsXoWh1b{A@Hpd?cT8T~p zvw=c3lAjh_phrix5?pPJ`#W2UUBBJLE7tF<4!^PJD$!YDk%Dgpl!!2*4lhQmQAdIz zuK181K)|G%_;Rn9$8aC7*tZA#Q9iDZJ}RmM0K3uEOKhSh>N}1z%{JzRwyAL^#L&1o z>bi9`Huj7e_LEh72zV)NmIcrtUiIn@xBm;%nHUgRSG?$Dy0>Z5@gaG(j)JX2eR_B6 zf_RI1nyV=T>@ptSvk?RTjn?m{bR-*nBOS?7V+l;yV`iNr$XTsrORQ@c1m|t{&xD5Z zeOZPs^Gaok6~5G2okfZDXwk{>^G4j6+apVcq(QvtICcd2^K*`%Ua~o#0&dc6#UA>p zw9O1J4-L9t;CXBUF9WJLn_SxCUs|q7VvbUI+2*ki$3bT+y85z^Fwi~)$uI$f$o41C zA!-S=$zx$bf>pBAYJy4xtX18O_O_FNlJR0JePBhha*%9{f2{7O>AWAE{1yB0!s$hr z&z6i61aicSKN4wxB+>?%2y9gljZ;44akSw;&|8S{xt*Ak+v~~G%?^0s6zqH zL*2avOjfW6XqmdEbOd(I6rC?uu!BsDjRgz#qzvo@2(D3V-IzZFA~@lbH7ZLbHE6}lF3^Cn zPM2jFyKnLG9?g?u0vCiXCydmBLL0KQTJW+obCsxhZ3# z6v#3uBGc5PLmU3b`72uU_2vKiWFl-wwKUQOKV}ALKOgR>JUV%gdN`29h*!5+4U(hH zcvTQpHe$FGUf1F%Mn_MHnX;8(Vy0|VH%O1V1+U#HP+N-3Dz6vTt&g?B+8%bu(RzWn zo#qfCF!tAIvEBf2*j@KB$Z)_Ig zNZB+c$-^`;oAl;ZhYL=(8$(bCdC)j;8DEuKM#`}q_2|hFms!u6=9hb!DOk^X$!Vom z=ia9|+VO@)_nj&xqk1SV{qbp7lUD}~H%}kjfO9T&jAlN2KD7EX z0VW*BA`WYZXn2LNVmFqra}(p>aY6XdLqj>h(U}#Qgk=cm0X)7Ug9JJ8w;giF!~+d9FEbnTbx zA?QPNA1ijIUxKD#jIf;@tm+TklF98ksGh^43{SBX9j(hvZjxPpWP>4~=v` zX*d&6s!qCo~#G>z#b}^$14_SqM zMInnIB;8!CG2!GipU%=X6kM84tDENdm3)@h12He{4G?TPxi+vJiWAKC_O@-Fk9*89 zjiT3h;K?lFsS6878xEx8DA55ku=RFuXPXkQ2ag9@Jg@PcFcckk!+b69eSc!|OpdUc zGn(>dLQi%&ZDofjlTaYw>gHfqTpL7R7On~xZSC`C**2_InOdc&ubhdf+Ecp0s5i)j z!%0%ieR+Y@iDVexVFGyO@Y&wi^zzHaZu#0oNrYo8a-JD&4U7{lIJ&pXMh!PWY$4(o z2p4Ws=&>1R#K+rzj5h;U)RlddeKGyJYLtIX0E&S^#sG^)if6blZqW{mi2PIz1Y_#W zV&6dt#h*KlLh zQT~en^=_I;^Ux%+=l@A_(8~fX*T_r}Y%`$6IVQ8rc^4khL^Nr*z@O{FWiL!kmHBSd zgn2tjSe+P5wme~hd!0KPtxt&=)wX5U@kE;dg_|9=7T9K2C8;%fUrP!XJ@dhSSUO7W zimIy_A#8qiTRAj*v=f`r_b(3#ah?qqWmy#K=h5OtA6Tre^VNqNSJ0#%V)!PLjc}dSOH)mT*tN@qsqtqOQV$8H0N)&ILd7811O%6y{01Bk;sbkCG4*o8cTXEJ)G+6zD^?VUgC}t zx+{}ZrZNHaV1n$(W)11oQgA4Ys=l_YC}Vv8@-t{kX5v@P#XXtz=f%#SABtcN#T)3r9!cH z*t2{^zXb)QY-W@o!atLK#pPq_5g8G55j7QLN+Pz$Owxf~PJ~+#+u<`RkaeeMe$wXZjyG~ zFT?oBMemTnknqnQwm4yZf+!aTCm)!ZIV%ya`I*cdnLMZqO;#X9=qsWBP1&i%XiZ;? z7_wh9;Y}efi7*CK+nQ^|^H>LL1u%F-^o7SaRtmX6CDb=xE*9rtWq1PnO{97+T3$om zHbzu4QL9p{2 zEhr|3Q1RT=b@T3{S0kHyVI-5uFC*;pN4TM62rFhoVSOk=Y^0`&Db*bGh{_#bs%bHY+`|)hly2oMI@^ znLMR8^iUN;%w`gnJJ?sNo6}(13uHmd|E<@qaslyxoN0ZrJldrp(V0mIxvf&?RhAl( zdNgGMdV=IeY+8)q!iTtcw_9S{mEsHZl*Gpm((xXCnUDjK=db(=JfGkN67bN3{ZtNH zo|;`KV-fN)FfKJ=IP`Qa5Q4E!MkRKXsxt?}uBVetw)Smtm1%yyi5L;puJfiGN)(5W z8Ol7>Ok9@(1b2#>OX;0yb4d@vnD?G2Ch-;B8u}jNrpyVpG2E+zXQj?O0Wt%tZEojt zvTbImc`nTuN5FW6zQR2`0R>ik7chXB+Q96U09x?GEIi;Gm@GZ@La0c8(3pb7>{k+3 z<#ajkhOR?xl13SIsp$3k9)LGi8Tc%m@2pEl%1}-Qc=OL0prVK;Wkr31(b$&LL4x=B zGEmn0f3Y++3ls}`VRMJDvFLsK-AH8X1d}^KVTIc(^AJJIB zna4o6nJkiT_L#gnRc(4tf#sn6(j@KsUQ35m-lI8yX?kAGiQSnZHYj1s4C&T=N_t^D z%9{JmYcnSrm^!Q)cn#8ROig&pLIoAJXV=6WtB!R~RUIa`Rnhr)CO&5MepB#-(roqG zyu0J2@fbn^lS?B2Xi-l?lh(v9Et|=n9FtqSlgkDqECqWxtR5KttZ=@#?zS*6XQqpv zVq+myH(mAxqmp%`xEfFnPi>ur=)kaiferH*4-T<_#Kglr53sXIrDgMRh)o-IvI>jQ z^BHUYKq?MrPjb;!X6T2NKblb5*Uz+J4G-Tb58z=NCA z8}&tFti1jBld)ATqR|qNKg9yq9_M`md?qDW^U7M6e8jg^OyTWo*I(=Ti2#Fq6l+QvkJ}; zi3;Km)MEK{#&7z%mbbIQ?Mc=LeX`UPC#{PF+ zbbk&!a&|PCfsg72aYP=cg}q5dD(j<0O`^5zxRO+W3qM!%A)qQ2F9Xb>Y$PmO<__3$ z(KJROTAewD!V{{Y__k~A&%n}g@wDw-E_^ctv&kuNNnevC<`Xl>M~}xctPjGW=t12H zqVZqrh6Xe2RVOjOTgRs*Fvc}^RZ~R*Rc^VE=U6aoir!`~$||0&;W(=sCFHY#9q#&Y za`}8bwpy{pVf;@bvwAc@y&|*`z;3NNMS_Pfs9h7dsu>j*!(?2qeK+{)FjYoHFA)j2 zG!&>;5{uqthsVE-8m!vqjZvV&7lKb;BlDBcIfU4nke#9rseK?m

    jZ=UE7Yvyv=k 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�#3Z4cJlIP^w{HeE{6mD%E~t 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

    nq=e(jh-@K3Q5T?MwVE)k+9%vu(dn|N`oq2 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

    %88?sk~un(C#3wDekwFwO~&02?+~Z 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 gH-=At9y`TP63M@lIn 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 z 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%0AZLY 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

    S-yc7*K1a$^Q&#uC!N!Q(vM|DOh}Cph-sba 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 At^fc4 literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/chunk-03b0.7a203856.js b/priv/static/adminfe/static/js/chunk-03b0.7a203856.js deleted file mode 100644 index 43ca0e4e66d14ce4210ebed4e495b4bfb720813f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100666 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 diff --git a/priv/static/adminfe/static/js/chunk-03b0.7a203856.js.map b/priv/static/adminfe/static/js/chunk-03b0.7a203856.js.map deleted file mode 100644 index 697a106aca54b4b2b4e41923a99a21287ebd5066..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 348763 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

    ### Added diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 47afdfba5..cb8201f11 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -414,6 +414,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `nicknames` - Response: none (code `204`) +## `PATCH /api/pleroma/admin/users/:nickname/change_password` + +### Change the user password + +- Params: + - `new_password` +- Response: none (code `200`) + ## `GET /api/pleroma/admin/reports` ### Get a list of reports diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index e32895f70..b5435a553 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -605,6 +605,17 @@ def get_log_entry_message(%ModerationLog{ }" end + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "change_password", + "subject" => subjects + } + }) do + "@#{actor_nickname} changed password for users: #{users_to_nicknames_string(subjects)}" + end + defp nicknames_to_string(nicknames) do nicknames |> Enum.map(&"@#{&1}") diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 175260bc2..2aa2c6ac2 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -658,6 +658,39 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic json_response(conn, :no_content, "") end + @doc "Changes password for a given user" + def change_password(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do + with {_, user} <- {:user, User.get_cached_by_nickname(nickname)}, + {:ok, _user} <- + User.reset_password(user, %{ + password: params["new_password"], + password_confirmation: params["new_password"] + }) do + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "change_password" + }) + + User.force_password_reset_async(user) + + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "force_password_reset" + }) + + json(conn, %{status: "success"}) + else + {:error, changeset} -> + {_, {error, _}} = Enum.at(changeset.errors, 0) + json(conn, %{error: "New password #{error}."}) + + _ -> + json(conn, %{error: "Unable to change password."}) + end + end + def list_reports(conn, params) do {page, page_size} = page_params(params) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e4e3ee704..c03ad101e 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -173,6 +173,7 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset) + patch("/users/:nickname/change_password", AdminAPIController, :change_password) get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index e4c152fb7..0c1214f05 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -3389,6 +3389,32 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do end end + describe "PATCH /users/:nickname/change_password" do + test "changes password", %{conn: conn, admin: admin} do + user = insert(:user) + assert user.password_reset_pending == false + + conn = + patch(conn, "/api/pleroma/admin/users/#{user.nickname}/change_password", %{ + "new_password" => "password" + }) + + assert json_response(conn, 200) == %{"status" => "success"} + + ObanHelpers.perform_all() + + assert User.get_by_id(user.id).password_reset_pending == true + + [log_entry1, log_entry2] = ModerationLog |> Repo.all() |> Enum.sort() + + assert ModerationLog.get_log_entry_message(log_entry1) == + "@#{admin.nickname} changed password for users: @#{user.nickname}" + + assert ModerationLog.get_log_entry_message(log_entry2) == + "@#{admin.nickname} forced password reset for users: @#{user.nickname}" + end + end + describe "PATCH /users/:nickname/force_password_reset" do test "sets password_reset_pending to true", %{conn: conn} do user = insert(:user) From 13cce9c0debbf9a80ed5da26cb34ca563e5e1417 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Fri, 31 Jan 2020 21:07:46 +0300 Subject: [PATCH 55/79] Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials`, `GET /api/pleroma/admin/users/:nickname/credentials`. --- CHANGELOG.md | 2 +- docs/API/admin_api.md | 75 +++++++++++++++- lib/pleroma/moderation_log.ex | 4 +- lib/pleroma/user.ex | 86 ++++++++++++++++++- .../web/admin_api/admin_api_controller.ex | 34 +++++--- .../web/admin_api/views/account_view.ex | 40 +++++++++ .../controllers/account_controller.ex | 60 +++---------- lib/pleroma/web/router.ex | 3 +- .../admin_api/admin_api_controller_test.exs | 57 ++++++++++-- 9 files changed, 286 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f8091c8c..ec04c26e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. - Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default. -- Admin API: `PATCH /api/pleroma/admin/users/:nickname/change_password` +- Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials` ### Added diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index cb8201f11..edcf73e14 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -414,12 +414,81 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `nicknames` - Response: none (code `204`) -## `PATCH /api/pleroma/admin/users/:nickname/change_password` +## `GET /api/pleroma/admin/users/:nickname/credentials` -### Change the user password +### Get the user's email, password, display and settings-related fields - Params: - - `new_password` + - `nickname` + +- Response: + +```json +{ + "actor_type": "Person", + "allow_following_move": true, + "avatar": "https://pleroma.social/media/7e8e7508fd545ef580549b6881d80ec0ff2c81ed9ad37b9bdbbdf0e0d030159d.jpg", + "background": "https://pleroma.social/media/4de34c0bd10970d02cbdef8972bef0ebbf55f43cadc449554d4396156162fe9a.jpg", + "banner": "https://pleroma.social/media/8d92ba2bd244b613520abf557dd448adcd30f5587022813ee9dd068945986946.jpg", + "bio": "bio", + "default_scope": "public", + "discoverable": false, + "email": "user@example.com", + "fields": [ + { + "name": "example", + "value": "https://example.com" + } + ], + "hide_favorites": false, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "id": "9oouHaEEUR54hls968", + "locked": true, + "name": "user", + "no_rich_text": true, + "pleroma_settings_store": {}, + "raw_fields": [ + { + "id": 1, + "name": "example", + "value": "https://example.com" + }, + ], + "show_role": true, + "skip_thread_containment": false +} +``` + +## `PATCH /api/pleroma/admin/users/:nickname/credentials` + +### Change the user's email, password, display and settings-related fields + +- Params: + - `email` + - `password` + - `name` + - `bio` + - `avatar` + - `locked` + - `no_rich_text` + - `default_scope` + - `banner` + - `hide_follows` + - `hide_followers` + - `hide_followers_count` + - `hide_follows_count` + - `hide_favorites` + - `allow_following_move` + - `background` + - `show_role` + - `skip_thread_containment` + - `fields` + - `discoverable` + - `actor_type` + - Response: none (code `200`) ## `GET /api/pleroma/admin/reports` diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index b5435a553..7aacd9d80 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -609,11 +609,11 @@ def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, - "action" => "change_password", + "action" => "updated_users", "subject" => subjects } }) do - "@#{actor_nickname} changed password for users: #{users_to_nicknames_string(subjects)}" + "@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}" end defp nicknames_to_string(nicknames) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 911dde6e2..44de64345 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -417,9 +417,55 @@ def update_changeset(struct, params \\ %{}) do |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, min: 1, max: name_limit) + |> put_fields() + |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) + |> put_change_if_present(:avatar, &put_upload(&1, :avatar)) + |> put_change_if_present(:banner, &put_upload(&1, :banner)) + |> put_change_if_present(:background, &put_upload(&1, :background)) + |> put_change_if_present( + :pleroma_settings_store, + &{:ok, Map.merge(struct.pleroma_settings_store, &1)} + ) |> validate_fields(false) end + defp put_fields(changeset) do + if raw_fields = get_change(changeset, :raw_fields) do + raw_fields = + raw_fields + |> Enum.filter(fn %{"name" => n} -> n != "" end) + + fields = + raw_fields + |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) + + changeset + |> put_change(:raw_fields, raw_fields) + |> put_change(:fields, fields) + else + changeset + end + end + + defp put_change_if_present(changeset, map_field, value_function) do + if value = get_change(changeset, map_field) do + with {:ok, new_value} <- value_function.(value) do + put_change(changeset, map_field, new_value) + else + _ -> changeset + end + else + changeset + end + end + + defp put_upload(value, type) do + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: type) do + {:ok, object.data} + end + end + def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) @@ -463,6 +509,27 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do |> validate_fields(remote?) end + def update_as_admin_changeset(struct, params) do + struct + |> update_changeset(params) + |> cast(params, [:email]) + |> delete_change(:also_known_as) + |> unique_constraint(:email) + |> validate_format(:email, @email_regex) + end + + @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} + def update_as_admin(user, params) do + params = Map.put(params, "password_confirmation", params["password"]) + changeset = update_as_admin_changeset(user, params) + + if params["password"] do + reset_password(user, changeset, params) + else + User.update_and_set_cache(changeset) + end + end + def password_update_changeset(struct, params) do struct |> cast(params, [:password, :password_confirmation]) @@ -473,10 +540,14 @@ def password_update_changeset(struct, params) do end @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} - def reset_password(%User{id: user_id} = user, data) do + def reset_password(%User{} = user, params) do + reset_password(user, user, params) + end + + def reset_password(%User{id: user_id} = user, struct, params) do multi = Multi.new() - |> Multi.update(:user, password_update_changeset(user, data)) + |> Multi.update(:user, password_update_changeset(struct, params)) |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id)) |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user)) @@ -1856,6 +1927,17 @@ def fields(%{fields: nil}), do: [] def fields(%{fields: fields}), do: fields + def sanitized_fields(%User{} = user) do + user + |> User.fields() + |> Enum.map(fn %{"name" => name, "value" => value} -> + %{ + "name" => name, + "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) + } + end) + end + def validate_fields(changeset, remote? \\ false) do limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit = Pleroma.Config.get([:instance, limit_name], 0) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2aa2c6ac2..0368df1e9 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["read:accounts"], admin: true} - when action in [:list_users, :user_show, :right_get] + when action in [:list_users, :user_show, :right_get, :show_user_credentials] ) plug( @@ -54,7 +54,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do :tag_users, :untag_users, :right_add, - :right_delete + :right_delete, + :update_user_credentials ] ) @@ -658,21 +659,34 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic json_response(conn, :no_content, "") end - @doc "Changes password for a given user" - def change_password(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do + @doc "Show a given user's credentials" + def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do + conn + |> put_view(AccountView) + |> render("credentials.json", %{user: user, for: admin}) + else + _ -> {:error, :not_found} + end + end + + @doc "Updates a given user" + def update_user_credentials( + %{assigns: %{user: admin}} = conn, + %{"nickname" => nickname} = params + ) do with {_, user} <- {:user, User.get_cached_by_nickname(nickname)}, {:ok, _user} <- - User.reset_password(user, %{ - password: params["new_password"], - password_confirmation: params["new_password"] - }) do + User.update_as_admin(user, params) do ModerationLog.insert_log(%{ actor: admin, subject: [user], - action: "change_password" + action: "updated_users" }) - User.force_password_reset_async(user) + if params["password"] do + User.force_password_reset_async(user) + end ModerationLog.insert_log(%{ actor: admin, diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 1e03849de..a16a3ebf0 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -23,6 +23,43 @@ def render("index.json", %{users: users}) do } end + def render("credentials.json", %{user: user, for: for_user}) do + user = User.sanitize_html(user, User.html_filter_policy(for_user)) + avatar = User.avatar_url(user) |> MediaProxy.url() + banner = User.banner_url(user) |> MediaProxy.url() + background = image_url(user.background) |> MediaProxy.url() + + user + |> Map.take([ + :id, + :bio, + :email, + :fields, + :name, + :nickname, + :locked, + :no_rich_text, + :default_scope, + :hide_follows, + :hide_followers_count, + :hide_follows_count, + :hide_followers, + :hide_favorites, + :allow_following_move, + :show_role, + :skip_thread_containment, + :pleroma_settings_store, + :raw_fields, + :discoverable, + :actor_type + ]) + |> Map.merge(%{ + "avatar" => avatar, + "banner" => banner, + "background" => background + }) + end + def render("show.json", %{user: user}) do avatar = User.avatar_url(user) |> MediaProxy.url() display_name = Pleroma.HTML.strip_tags(user.name || user.nickname) @@ -104,4 +141,7 @@ defp parse_error(errors) do "" end end + + defp image_url(%{"url" => [%{"href" => href} | _]}), do: href + defp image_url(_), do: nil end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 88c997b9f..56e6214c5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] - alias Pleroma.Emoji alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.User @@ -140,17 +139,6 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do def update_credentials(%{assigns: %{user: original_user}} = conn, params) do user = original_user - params = - if Map.has_key?(params, "fields_attributes") do - Map.update!(params, "fields_attributes", fn fields -> - fields - |> normalize_fields_attributes() - |> Enum.filter(fn %{"name" => n} -> n != "" end) - end) - else - params - end - user_params = [ :no_rich_text, @@ -169,46 +157,20 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) end) |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) - |> add_if_present(params, "avatar", :avatar, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :avatar) do - {:ok, object.data} - end - end) - |> add_if_present(params, "header", :banner, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :banner) do - {:ok, object.data} - end - end) - |> add_if_present(params, "pleroma_background_image", :background, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :background) do - {:ok, object.data} - end - end) - |> add_if_present(params, "fields_attributes", :fields, fn fields -> - fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) - - {:ok, fields} - end) - |> add_if_present(params, "fields_attributes", :raw_fields) - |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> - {:ok, Map.merge(user.pleroma_settings_store, value)} - end) + |> add_if_present(params, "note", :bio) + |> add_if_present(params, "avatar", :avatar) + |> add_if_present(params, "header", :banner) + |> add_if_present(params, "pleroma_background_image", :background) + |> add_if_present( + params, + "fields_attributes", + :raw_fields, + &{:ok, normalize_fields_attributes(&1)} + ) + |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store) |> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "actor_type", :actor_type) - emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") - - user_emojis = - user - |> Map.get(:emoji, []) - |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) - |> Enum.dedup() - - user_params = Map.put(user_params, :emoji, user_emojis) changeset = User.update_changeset(user, user_params) with {:ok, user} <- User.update_and_set_cache(changeset) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c03ad101e..2927775eb 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -173,7 +173,8 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset) - patch("/users/:nickname/change_password", AdminAPIController, :change_password) + get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials) + patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 0c1214f05..0a317cf88 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -3389,30 +3389,73 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do end end - describe "PATCH /users/:nickname/change_password" do - test "changes password", %{conn: conn, admin: admin} do + describe "GET /users/:nickname/credentials" do + test "gets the user credentials", %{conn: conn} do + user = insert(:user) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") + + response = assert json_response(conn, 200) + assert response["email"] == user.email + end + + test "returns 403 if requested by a non-admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> get("/api/pleroma/admin/users/#{user.nickname}/credentials") + + assert json_response(conn, :forbidden) + end + end + + describe "PATCH /users/:nickname/credentials" do + test "changes password and email", %{conn: conn, admin: admin} do user = insert(:user) assert user.password_reset_pending == false conn = - patch(conn, "/api/pleroma/admin/users/#{user.nickname}/change_password", %{ - "new_password" => "password" + patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "password" => "new_password", + "email" => "new_email@example.com", + "name" => "new_name" }) assert json_response(conn, 200) == %{"status" => "success"} ObanHelpers.perform_all() - assert User.get_by_id(user.id).password_reset_pending == true + updated_user = User.get_by_id(user.id) - [log_entry1, log_entry2] = ModerationLog |> Repo.all() |> Enum.sort() + assert updated_user.email == "new_email@example.com" + assert updated_user.name == "new_name" + assert updated_user.password_hash != user.password_hash + assert updated_user.password_reset_pending == true + + [log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort() assert ModerationLog.get_log_entry_message(log_entry1) == - "@#{admin.nickname} changed password for users: @#{user.nickname}" + "@#{admin.nickname} updated users: @#{user.nickname}" assert ModerationLog.get_log_entry_message(log_entry2) == "@#{admin.nickname} forced password reset for users: @#{user.nickname}" end + + test "returns 403 if requested by a non-admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> patch("/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "password" => "new_password", + "email" => "new_email@example.com", + "name" => "new_name" + }) + + assert json_response(conn, :forbidden) + end end describe "PATCH /users/:nickname/force_password_reset" do From 74388336852b18d5d5f108a8305f1a038301f7a1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 16 Mar 2020 21:58:10 +0300 Subject: [PATCH 56/79] [#1364] Improved notification-related tests. --- lib/pleroma/notification.ex | 1 + test/notification_test.exs | 121 +++++++++++++++++++++++------------- 2 files changed, 79 insertions(+), 43 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 0d7a6610a..104368fd1 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -344,6 +344,7 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo |> Utils.maybe_notify_followers(activity) |> Enum.uniq() + # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs notification_enabled_ap_ids = potential_receiver_ap_ids |> exclude_relation_restricting_ap_ids(activity) diff --git a/test/notification_test.exs b/test/notification_test.exs index bc2d80f05..a7282c929 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -82,6 +82,80 @@ test "does not create a notification for subscribed users if status is a reply" end end + describe "CommonApi.post/2 notification-related functionality" do + test_with_mock "creates but does NOT send notification to blocker user", + Push, + [:passthrough], + [] do + user = insert(:user) + blocker = insert(:user) + {:ok, _user_relationship} = User.block(blocker, user) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => "hey @#{blocker.nickname}!"}) + + blocker_id = blocker.id + assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification) + refute called(Push.send(:_)) + end + + test_with_mock "creates but does NOT send notification to notification-muter user", + Push, + [:passthrough], + [] do + user = insert(:user) + muter = insert(:user) + {:ok, _user_relationships} = User.mute(muter, user) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => "hey @#{muter.nickname}!"}) + + muter_id = muter.id + assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification) + refute called(Push.send(:_)) + end + + test_with_mock "creates but does NOT send notification to thread-muter user", + Push, + [:passthrough], + [] do + user = insert(:user) + thread_muter = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{thread_muter.nickname}!"}) + + {:ok, _} = CommonAPI.add_mute(thread_muter, activity) + + {:ok, _same_context_activity} = + CommonAPI.post(user, %{ + "status" => "hey-hey-hey @#{thread_muter.nickname}!", + "in_reply_to_status_id" => activity.id + }) + + [pre_mute_notification, post_mute_notification] = + Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id)) + + pre_mute_notification_id = pre_mute_notification.id + post_mute_notification_id = post_mute_notification.id + + assert called( + Push.send( + :meck.is(fn + %Notification{id: ^pre_mute_notification_id} -> true + _ -> false + end) + ) + ) + + refute called( + Push.send( + :meck.is(fn + %Notification{id: ^post_mute_notification_id} -> true + _ -> false + end) + ) + ) + end + end + describe "create_notification" do @tag needs_streamer: true test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do @@ -489,10 +563,7 @@ test "it does not send notification to mentioned users in announces" do assert other_user not in enabled_receivers end - test_with_mock "it returns blocking recipient in disabled recipients list", - Push, - [:passthrough], - [] do + test "it returns blocking recipient in disabled recipients list" do user = insert(:user) other_user = insert(:user) {:ok, _user_relationship} = User.block(other_user, user) @@ -503,15 +574,9 @@ test "it does not send notification to mentioned users in announces" do assert [] == enabled_receivers assert [other_user] == disabled_receivers - - assert 1 == length(Repo.all(Notification)) - refute called(Push.send(:_)) end - test_with_mock "it returns notification-muting recipient in disabled recipients list", - Push, - [:passthrough], - [] do + test "it returns notification-muting recipient in disabled recipients list" do user = insert(:user) other_user = insert(:user) {:ok, _user_relationships} = User.mute(other_user, user) @@ -522,15 +587,9 @@ test "it does not send notification to mentioned users in announces" do assert [] == enabled_receivers assert [other_user] == disabled_receivers - - assert 1 == length(Repo.all(Notification)) - refute called(Push.send(:_)) end - test_with_mock "it returns thread-muting recipient in disabled recipients list", - Push, - [:passthrough], - [] do + test "it returns thread-muting recipient in disabled recipients list" do user = insert(:user) other_user = insert(:user) @@ -549,30 +608,6 @@ test "it does not send notification to mentioned users in announces" do assert [other_user] == disabled_receivers refute other_user in enabled_receivers - - [pre_mute_notification, post_mute_notification] = - Repo.all(from(n in Notification, where: n.user_id == ^other_user.id, order_by: n.id)) - - pre_mute_notification_id = pre_mute_notification.id - post_mute_notification_id = post_mute_notification.id - - assert called( - Push.send( - :meck.is(fn - %Notification{id: ^pre_mute_notification_id} -> true - _ -> false - end) - ) - ) - - refute called( - Push.send( - :meck.is(fn - %Notification{id: ^post_mute_notification_id} -> true - _ -> false - end) - ) - ) end end @@ -820,7 +855,7 @@ test "it doesn't return notifications for blocked user" do assert Notification.for_user(user) == [] end - test "it doesn't return notificatitons for blocked domain" do + test "it doesn't return notifications for blocked domain" do user = insert(:user) blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") From 4705590f76f6a875aa99f32c8b08c20d793470a8 Mon Sep 17 00:00:00 2001 From: Cevado Date: Mon, 16 Mar 2020 22:02:01 -0300 Subject: [PATCH 57/79] Fix ssl option on Ecto config breaking release To use `:ssl` option on Ecto config it's required to include Erlang ssl application, this prevents releases to start when `:ssl` option is set to true. --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index dd598345c..890979f8b 100644 --- a/mix.exs +++ b/mix.exs @@ -63,7 +63,7 @@ def copy_nginx_config(%{path: target_path} = release) do def application do [ mod: {Pleroma.Application, []}, - extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize], + extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize, :ssl], included_applications: [:ex_syslogger] ] end From ec3719f5391d6f9945cec2e36287049d72743cd4 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 18 Mar 2020 20:30:31 +0300 Subject: [PATCH 58/79] Improved in-test config management functions. --- test/config/transfer_task_test.exs | 4 +-- test/conversation_test.exs | 4 +-- test/plugs/instance_static_test.exs | 4 +-- test/plugs/user_is_admin_plug_test.exs | 8 ++--- test/support/helpers.ex | 30 ++++++++++++++++++- test/tasks/config_test.exs | 4 +-- test/upload_test.exs | 4 +-- test/uploaders/s3_test.exs | 10 +++---- test/user_test.exs | 4 +-- .../activity_pub_controller_test.exs | 4 +-- .../mrf/object_age_policy_test.exs | 10 +++---- .../activity_pub/mrf/simple_policy_test.exs | 22 +++++++------- test/web/activity_pub/publisher_test.exs | 4 +-- test/web/activity_pub/transmogrifier_test.exs | 12 ++------ .../activity_pub/views/object_view_test.exs | 4 +-- .../admin_api/admin_api_controller_test.exs | 29 +++++------------- test/web/federator_test.exs | 4 +-- test/web/feed/user_controller_test.exs | 4 +-- test/web/instances/instance_test.exs | 4 +-- test/web/instances/instances_test.exs | 4 +-- .../controllers/account_controller_test.exs | 4 +-- .../controllers/status_controller_test.exs | 4 +-- test/web/oauth/ldap_authorization_test.exs | 8 ++--- test/web/oauth/oauth_controller_test.exs | 7 +---- test/web/ostatus/ostatus_controller_test.exs | 4 +-- .../controllers/account_controller_test.exs | 4 +-- .../controllers/emoji_api_controller_test.exs | 4 +-- .../static_fe/static_fe_controller_test.exs | 8 ++--- .../remote_follow_controller_test.exs | 4 +-- test/web/twitter_api/twitter_api_test.exs | 16 +++------- test/web/twitter_api/util_controller_test.exs | 4 +-- .../web_finger/web_finger_controller_test.exs | 4 +-- 32 files changed, 89 insertions(+), 155 deletions(-) diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 01d04761d..7bfae67bf 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -10,9 +10,7 @@ defmodule Pleroma.Config.TransferTaskTest do alias Pleroma.Config.TransferTask alias Pleroma.ConfigDB - clear_config(:configurable_from_database) do - Pleroma.Config.put(:configurable_from_database, true) - end + clear_config(:configurable_from_database, true) test "transfer config values from db to env" do refute Application.get_env(:pleroma, :test_key) diff --git a/test/conversation_test.exs b/test/conversation_test.exs index dc0027d04..3c54253e3 100644 --- a/test/conversation_test.exs +++ b/test/conversation_test.exs @@ -11,9 +11,7 @@ defmodule Pleroma.ConversationTest do import Pleroma.Factory - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) - end + clear_config_all([:instance, :federating], true) test "it goes through old direct conversations" do user = insert(:user) diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs index 8cd9b5712..2e9d2dc46 100644 --- a/test/plugs/instance_static_test.exs +++ b/test/plugs/instance_static_test.exs @@ -12,9 +12,7 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do on_exit(fn -> File.rm_rf(@dir) end) end - clear_config([:instance, :static_dir]) do - Pleroma.Config.put([:instance, :static_dir], @dir) - end + clear_config([:instance, :static_dir], @dir) test "overrides index" do bundled_index = get(build_conn(), "/") diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 015d51018..1062d6e70 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -9,9 +9,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do import Pleroma.Factory describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage]) do - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) - end + clear_config([:auth, :enforce_oauth_admin_scope_usage], false) test "accepts a user that is an admin" do user = insert(:user, is_admin: true) @@ -42,9 +40,7 @@ test "denies when a user isn't set" do end describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage]) do - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) - end + clear_config([:auth, :enforce_oauth_admin_scope_usage], true) setup do admin_user = insert(:user, is_admin: true) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 6bf4b019e..c6f7fa5e2 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -26,6 +26,25 @@ defmacro clear_config(config_path, do: yield) do end end + defmacro clear_config(config_path, temp_setting) do + quote do + clear_config(unquote(config_path)) do + Config.put(unquote(config_path), unquote(temp_setting)) + end + end + end + + @doc """ + From _within a test case_, sets config to provided value and restores initial value on exit. + For multi-case setup use `clear_config/2` instead. + """ + def set_config(config_path, temp_setting) do + initial_setting = Config.get(config_path) + Config.put(config_path, temp_setting) + + ExUnit.Callbacks.on_exit(fn -> Config.put(config_path, initial_setting) end) + end + @doc "Stores initial config value and restores it after *all* test examples are executed." defmacro clear_config_all(config_path) do quote do @@ -50,6 +69,14 @@ defmacro clear_config_all(config_path, do: yield) do end end + defmacro clear_config_all(config_path, temp_setting) do + quote do + clear_config_all(unquote(config_path)) do + Config.put(unquote(config_path), unquote(temp_setting)) + end + end + end + defmacro __using__(_opts) do quote do import Pleroma.Tests.Helpers, @@ -57,7 +84,8 @@ defmacro __using__(_opts) do clear_config: 1, clear_config: 2, clear_config_all: 1, - clear_config_all: 2 + clear_config_all: 2, + set_config: 2 ] def to_datetime(naive_datetime) do diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index a6c0de351..b0c2efc98 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -20,9 +20,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do :ok end - clear_config_all(:configurable_from_database) do - Pleroma.Config.put(:configurable_from_database, true) - end + clear_config_all(:configurable_from_database, true) test "error if file with custom settings doesn't exist" do Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs") diff --git a/test/upload_test.exs b/test/upload_test.exs index 6ce42b630..6bf7f2417 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -250,9 +250,7 @@ test "escapes reserved uri characters" do end describe "Setting a custom base_url for uploaded media" do - clear_config([Pleroma.Upload, :base_url]) do - Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social") - end + clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") test "returns a media url with configured base_url" do base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs index fdc7eff41..96c21c0e5 100644 --- a/test/uploaders/s3_test.exs +++ b/test/uploaders/s3_test.exs @@ -11,12 +11,10 @@ defmodule Pleroma.Uploaders.S3Test do import Mock import ExUnit.CaptureLog - clear_config([Pleroma.Uploaders.S3]) do - Config.put([Pleroma.Uploaders.S3], - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com" - ) - end + clear_config(Pleroma.Uploaders.S3, + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com" + ) describe "get_file/1" do test "it returns path to local folder for files" do diff --git a/test/user_test.exs b/test/user_test.exs index b07fed42b..e0e7a26b8 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -476,9 +476,7 @@ test "it sets the password_hash and ap_id" do email: "email@example.com" } - clear_config([:instance, :account_activation_required]) do - Pleroma.Config.put([:instance, :account_activation_required], true) - end + clear_config([:instance, :account_activation_required], true) test "it creates unconfirmed user" do changeset = User.register_changeset(%User{}, @full_user_data) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index bd8e0b5cc..df0c53458 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -26,9 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do :ok end - clear_config([:instance, :federating]) do - Config.put([:instance, :federating], true) - end + clear_config([:instance, :federating], true) describe "/relay" do clear_config([:instance, :allow_relay]) diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs index 643609da4..bdbbb1fc4 100644 --- a/test/web/activity_pub/mrf/object_age_policy_test.exs +++ b/test/web/activity_pub/mrf/object_age_policy_test.exs @@ -9,12 +9,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy alias Pleroma.Web.ActivityPub.Visibility - clear_config([:mrf_object_age]) do - Config.put(:mrf_object_age, - threshold: 172_800, - actions: [:delist, :strip_followers] - ) - end + clear_config(:mrf_object_age, + threshold: 172_800, + actions: [:delist, :strip_followers] + ) setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index df0f223f8..97aec6622 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -8,18 +8,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do alias Pleroma.Config alias Pleroma.Web.ActivityPub.MRF.SimplePolicy - clear_config([:mrf_simple]) do - Config.put(:mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - report_removal: [], - reject: [], - accept: [], - avatar_removal: [], - banner_removal: [] - ) - end + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + accept: [], + avatar_removal: [], + banner_removal: [] + ) describe "when :media_removal" do test "is empty" do diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs index da26b13f7..ed9c951dd 100644 --- a/test/web/activity_pub/publisher_test.exs +++ b/test/web/activity_pub/publisher_test.exs @@ -23,9 +23,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do :ok end - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) - end + clear_config_all([:instance, :federating], true) describe "gather_webfinger_links/1" do test "it returns links" do diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index efbca82f6..c025b6b78 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1351,9 +1351,7 @@ test "it accepts Move activities" do end describe "`handle_incoming/2`, Mastodon format `replies` handling" do - clear_config([:activitypub, :note_replies_output_limit]) do - Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5) - end + clear_config([:activitypub, :note_replies_output_limit], 5) clear_config([:instance, :federation_incoming_replies_max_depth]) @@ -1394,9 +1392,7 @@ test "does NOT schedule background fetching of `replies` beyond max thread depth end describe "`handle_incoming/2`, Pleroma format `replies` handling" do - clear_config([:activitypub, :note_replies_output_limit]) do - Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5) - end + clear_config([:activitypub, :note_replies_output_limit], 5) clear_config([:instance, :federation_incoming_replies_max_depth]) @@ -2145,9 +2141,7 @@ test "returns object with emoji when object contains map tag" do end describe "set_replies/1" do - clear_config([:activitypub, :note_replies_output_limit]) do - Pleroma.Config.put([:activitypub, :note_replies_output_limit], 2) - end + clear_config([:activitypub, :note_replies_output_limit], 2) test "returns unmodified object if activity doesn't have self-replies" do data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs index 09866e99b..7dda20ec3 100644 --- a/test/web/activity_pub/views/object_view_test.exs +++ b/test/web/activity_pub/views/object_view_test.exs @@ -37,9 +37,7 @@ test "renders a note activity" do end describe "note activity's `replies` collection rendering" do - clear_config([:activitypub, :note_replies_output_limit]) do - Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5) - end + clear_config([:activitypub, :note_replies_output_limit], 5) test "renders `replies` collection for a note activity" do user = insert(:user) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index e4c152fb7..5f3064941 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -43,9 +43,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage]) do - Config.put([:auth, :enforce_oauth_admin_scope_usage], true) - end + clear_config([:auth, :enforce_oauth_admin_scope_usage], true) test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", %{admin: admin} do @@ -93,9 +91,7 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro end describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage]) do - Config.put([:auth, :enforce_oauth_admin_scope_usage], false) - end + clear_config([:auth, :enforce_oauth_admin_scope_usage], false) test "GET /api/pleroma/admin/users/:nickname requires " <> "read:accounts or admin:read:accounts or broader scope", @@ -581,13 +577,8 @@ test "/:right DELETE, can remove from a permission group (multiple)", %{ end describe "POST /api/pleroma/admin/email_invite, with valid config" do - clear_config([:instance, :registrations_open]) do - Config.put([:instance, :registrations_open], false) - end - - clear_config([:instance, :invites_enabled]) do - Config.put([:instance, :invites_enabled], true) - end + clear_config([:instance, :registrations_open], false) + clear_config([:instance, :invites_enabled], true) test "sends invitation and returns 204", %{admin: admin, conn: conn} do recipient_email = "foo@bar.com" @@ -1888,9 +1879,7 @@ test "returns 404 when the status does not exist", %{conn: conn} do end describe "GET /api/pleroma/admin/config" do - clear_config(:configurable_from_database) do - Config.put(:configurable_from_database, true) - end + clear_config(:configurable_from_database, true) test "when configuration from database is off", %{conn: conn} do Config.put(:configurable_from_database, false) @@ -2041,9 +2030,7 @@ test "POST /api/pleroma/admin/config error", %{conn: conn} do end) end - clear_config(:configurable_from_database) do - Config.put(:configurable_from_database, true) - end + clear_config(:configurable_from_database, true) @tag capture_log: true test "create new config setting in db", %{conn: conn} do @@ -3052,9 +3039,7 @@ test "proxy tuple ip", %{conn: conn} do end describe "GET /api/pleroma/admin/restart" do - clear_config(:configurable_from_database) do - Config.put(:configurable_from_database, true) - end + clear_config(:configurable_from_database, true) test "pleroma restarts", %{conn: conn} do capture_log(fn -> diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index d2ee2267c..2b321d263 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -21,9 +21,7 @@ defmodule Pleroma.Web.FederatorTest do :ok end - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) - end + clear_config_all([:instance, :federating], true) clear_config([:instance, :allow_relay]) clear_config([:instance, :rewrite_policy]) diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs index 00c50f003..49cfecde3 100644 --- a/test/web/feed/user_controller_test.exs +++ b/test/web/feed/user_controller_test.exs @@ -12,9 +12,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do alias Pleroma.Object alias Pleroma.User - clear_config([:instance, :federating]) do - Config.put([:instance, :federating], true) - end + clear_config([:instance, :federating], true) describe "feed" do clear_config([:feed]) diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index a3c93b986..ab8e5643b 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -10,9 +10,7 @@ defmodule Pleroma.Instances.InstanceTest do import Pleroma.Factory - clear_config_all([:instance, :federation_reachability_timeout_days]) do - Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1) - end + clear_config_all([:instance, :federation_reachability_timeout_days], 1) describe "set_reachable/1" do test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs index c5d6abc9c..1d83c1a1c 100644 --- a/test/web/instances/instances_test.exs +++ b/test/web/instances/instances_test.exs @@ -7,9 +7,7 @@ defmodule Pleroma.InstancesTest do use Pleroma.DataCase - clear_config_all([:instance, :federation_reachability_timeout_days]) do - Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1) - end + clear_config_all([:instance, :federation_reachability_timeout_days], 1) describe "reachable?/1" do test "returns `true` for host / url with unknown reachability status" do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 7efccd9c4..5a78f2968 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -756,9 +756,7 @@ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_ end describe "create account by app / rate limit" do - clear_config([:rate_limit, :app_account_creation]) do - Pleroma.Config.put([:rate_limit, :app_account_creation], {10_000, 2}) - end + clear_config([:rate_limit, :app_account_creation], {10_000, 2}) test "respects rate limit setting", %{conn: conn} do app_token = insert(:oauth_token, user: nil) diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index fbf63f608..5259abdcd 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -739,9 +739,7 @@ test "returns 404 error for a wrong id", %{conn: conn} do %{activity: activity} end - clear_config([:instance, :max_pinned_statuses]) do - Config.put([:instance, :max_pinned_statuses], 1) - end + clear_config([:instance, :max_pinned_statuses], 1) test "pin status", %{conn: conn, user: user, activity: activity} do id_str = to_string(activity.id) diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs index c55b0ffc5..b348281c5 100644 --- a/test/web/oauth/ldap_authorization_test.exs +++ b/test/web/oauth/ldap_authorization_test.exs @@ -12,13 +12,9 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do @skip if !Code.ensure_loaded?(:eldap), do: :skip - clear_config_all([:ldap, :enabled]) do - Pleroma.Config.put([:ldap, :enabled], true) - end + clear_config_all([:ldap, :enabled], true) - clear_config_all(Pleroma.Web.Auth.Authenticator) do - Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) - end + clear_config_all(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) @tag @skip test "authorizes the existing user using LDAP credentials" do diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index cff469c28..592612ddf 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -31,12 +31,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do ] end - clear_config([:auth, :oauth_consumer_strategies]) do - Pleroma.Config.put( - [:auth, :oauth_consumer_strategies], - ~w(twitter facebook) - ) - end + clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ app: app, diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 3b84358e4..6a3dcf2cd 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -17,9 +17,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do :ok end - clear_config([:instance, :federating]) do - Config.put([:instance, :federating], true) - end + clear_config([:instance, :federating], true) # Note: see ActivityPubControllerTest for JSON format tests describe "GET /objects/:uuid (text/html)" do diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs index 245cc1579..bc359707d 100644 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -27,9 +27,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do [user: user] end - clear_config([:instance, :account_activation_required]) do - Config.put([:instance, :account_activation_required], true) - end + clear_config([:instance, :account_activation_required], true) test "resend account confirmation email", %{conn: conn, user: user} do conn diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs index 4b9f5cf9a..146f3f4fe 100644 --- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -13,9 +13,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do "emoji" ) - clear_config([:auth, :enforce_oauth_admin_scope_usage]) do - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) - end + clear_config([:auth, :enforce_oauth_admin_scope_usage], false) test "shared & non-shared pack information in list_packs is ok" do conn = build_conn() diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index c3d2ae3b4..aabbedb17 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -8,13 +8,9 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do import Pleroma.Factory - clear_config_all([:static_fe, :enabled]) do - Config.put([:static_fe, :enabled], true) - end + clear_config_all([:static_fe, :enabled], true) - clear_config([:instance, :federating]) do - Config.put([:instance, :federating], true) - end + clear_config([:instance, :federating], true) setup %{conn: conn} do conn = put_req_header(conn, "accept", "text/html") diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs index 73062f18f..5c6087527 100644 --- a/test/web/twitter_api/remote_follow_controller_test.exs +++ b/test/web/twitter_api/remote_follow_controller_test.exs @@ -17,9 +17,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do :ok end - clear_config_all([:instance, :federating]) do - Config.put([:instance, :federating], true) - end + clear_config_all([:instance, :federating], true) clear_config([:instance]) clear_config([:frontend_configurations, :pleroma_fe]) diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index 14eed5f27..0e787715a 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -117,9 +117,7 @@ test "it registers a new user and parses mentions in the bio" do end describe "register with one time token" do - clear_config([:instance, :registrations_open]) do - Pleroma.Config.put([:instance, :registrations_open], false) - end + clear_config([:instance, :registrations_open], false) test "returns user on success" do {:ok, invite} = UserInviteToken.create_invite() @@ -184,9 +182,7 @@ test "returns error on expired token" do end describe "registers with date limited token" do - clear_config([:instance, :registrations_open]) do - Pleroma.Config.put([:instance, :registrations_open], false) - end + clear_config([:instance, :registrations_open], false) setup do data = %{ @@ -246,9 +242,7 @@ test "returns an error on overdue date", %{data: data} do end describe "registers with reusable token" do - clear_config([:instance, :registrations_open]) do - Pleroma.Config.put([:instance, :registrations_open], false) - end + clear_config([:instance, :registrations_open], false) test "returns user on success, after him registration fails" do {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) @@ -292,9 +286,7 @@ test "returns user on success, after him registration fails" do end describe "registers with reusable date limited token" do - clear_config([:instance, :registrations_open]) do - Pleroma.Config.put([:instance, :registrations_open], false) - end + clear_config([:instance, :registrations_open], false) test "returns user on success" do {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 9d757b5ef..71ecd1aa7 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -427,9 +427,7 @@ test "it returns version in json format", %{conn: conn} do end describe "POST /main/ostatus - remote_subscribe/2" do - clear_config([:instance, :federating]) do - Config.put([:instance, :federating], true) - end + clear_config([:instance, :federating], true) test "renders subscribe form", %{conn: conn} do user = insert(:user) diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs index b65bf5904..fcf14dc1e 100644 --- a/test/web/web_finger/web_finger_controller_test.exs +++ b/test/web/web_finger/web_finger_controller_test.exs @@ -14,9 +14,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do :ok end - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) - end + clear_config_all([:instance, :federating], true) test "GET host-meta" do response = From a8c6933ca023d7487910a0f99aed62fa8c2d45e2 Mon Sep 17 00:00:00 2001 From: stwf Date: Thu, 19 Mar 2020 12:25:36 -0400 Subject: [PATCH 59/79] remove federated testing --- .gitlab-ci.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5d0d3316a..1b7c03ebb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,19 +62,21 @@ unit-testing: - mix ecto.migrate - mix coveralls --preload-modules -federated-testing: - stage: test - cache: *testing_cache_policy - services: - - name: minibikini/postgres-with-rum:12 - alias: postgres - command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - script: - - mix deps.get - - mix ecto.create - - mix ecto.migrate - - epmd -daemon - - mix test --trace --only federated +# Removed to fix CI issue. In this early state it wasn't adding much value anyway. +# TODO Fix and reinstate federated testing +# federated-testing: +# stage: test +# cache: *testing_cache_policy +# services: +# - name: minibikini/postgres-with-rum:12 +# alias: postgres +# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] +# script: +# - mix deps.get +# - mix ecto.create +# - mix ecto.migrate +# - epmd -daemon +# - mix test --trace --only federated unit-testing-rum: stage: test From 98a60df41f8a053005a2a413b552a582a879ecaa Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 18 Mar 2020 17:37:54 +0300 Subject: [PATCH 60/79] include_types parameter in /api/v1/notifications --- CHANGELOG.md | 7 +++ docs/API/differences_in_mastoapi_responses.md | 1 + lib/pleroma/web/mastodon_api/mastodon_api.ex | 24 +++++++--- .../web/nodeinfo/nodeinfo_controller.ex | 1 + .../notification_controller_test.exs | 45 +++++++++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3be2ea08..a27200895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - **Breaking:** removed `with_move` parameter from notifications timeline. +### Added +- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list. +
    + API Changes +- Mastodon API: Support for `include_types` in `/api/v1/notifications`. +
    + ## [2.0.0] - 2019-03-08 ### Security - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request. diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index b12d3092c..dc8f54d2a 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -117,6 +117,7 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields: Accepts additional parameters: - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`. +- `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`. ## POST `/api/v1/statuses` diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index a2dc9bc71..70da64a7a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -55,6 +55,7 @@ def get_notifications(user, params \\ %{}) do user |> Notification.for_user_query(options) + |> restrict(:include_types, options) |> restrict(:exclude_types, options) |> restrict(:account_ap_id, options) |> Pagination.fetch_paginated(params) @@ -69,6 +70,7 @@ def get_scheduled_activities(user, params \\ %{}) do defp cast_params(params) do param_types = %{ exclude_types: {:array, :string}, + include_types: {:array, :string}, exclude_visibilities: {:array, :string}, reblogs: :boolean, with_muted: :boolean, @@ -79,14 +81,16 @@ defp cast_params(params) do changeset.changes end - defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do - ap_types = - mastodon_types - |> Enum.map(&Activity.from_mastodon_notification_type/1) - |> Enum.filter(& &1) + defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do + ap_types = convert_and_filter_mastodon_types(mastodon_types) - query - |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) + where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) + end + + defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do + ap_types = convert_and_filter_mastodon_types(mastodon_types) + + where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) end defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do @@ -94,4 +98,10 @@ defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do end defp restrict(query, _, _), do: query + + defp convert_and_filter_mastodon_types(types) do + types + |> Enum.map(&Activity.from_mastodon_notification_type/1) + |> Enum.filter(& &1) + end end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 18eb41333..30838b1eb 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -60,6 +60,7 @@ def raw_nodeinfo do "pleroma_explicit_addressing", "shareable_emoji_packs", "multifetch", + "pleroma:api/v1/notifications:include_types_filter", if Config.get([:media_proxy, :enabled]) do "media_proxy" end, diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index dbe9a7fd7..7a0011646 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -304,6 +304,51 @@ test "filters notifications using exclude_types" do assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200) end + test "filters notifications using include_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user) + {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + mention_notification_id = get_notification_id_by_activity(mention_activity) + favorite_notification_id = get_notification_id_by_activity(favorite_activity) + reblog_notification_id = get_notification_id_by_activity(reblog_activity) + follow_notification_id = get_notification_id_by_activity(follow_activity) + + conn_res = get(conn, "/api/v1/notifications", %{include_types: ["follow"]}) + + assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications", %{include_types: ["mention"]}) + + assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications", %{include_types: ["favourite"]}) + + assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications", %{include_types: ["reblog"]}) + + assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200) + + result = conn |> get("/api/v1/notifications") |> json_response(200) + + assert length(result) == 4 + + result = + conn + |> get("/api/v1/notifications", %{ + include_types: ["follow", "mention", "favourite", "reblog"] + }) + |> json_response(200) + + assert length(result) == 4 + end + test "destroy multiple" do %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) other_user = insert(:user) From fe15f0ba15d02809fa4c21fb646e65d06060f3bb Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 20 Mar 2020 13:04:37 +0300 Subject: [PATCH 61/79] restrict_unauthenticated setting --- CHANGELOG.md | 1 + config/config.exs | 5 + config/description.exs | 60 +++++ docs/configuration/cheatsheet.md | 18 ++ lib/pleroma/user.ex | 13 +- lib/pleroma/web/activity_pub/visibility.ex | 14 +- .../controllers/account_controller.ex | 7 +- .../controllers/status_controller.ex | 2 +- .../controllers/timeline_controller.ex | 35 ++- .../controllers/account_controller_test.exs | 213 +++++++++++++++++- .../controllers/status_controller_test.exs | 169 ++++++++++++++ .../controllers/timeline_controller_test.exs | 111 ++++++++- 12 files changed, 615 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27200895..15a073c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list. +- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
    API Changes - Mastodon API: Support for `include_types` in `/api/v1/notifications`. diff --git a/config/config.exs b/config/config.exs index 3357e23e7..2ab939107 100644 --- a/config/config.exs +++ b/config/config.exs @@ -624,6 +624,11 @@ parameters: [gin_fuzzy_search_limit: "500"], prepare: :unnamed +config :pleroma, :restrict_unauthenticated, + timelines: %{local: false, federated: false}, + profiles: %{local: false, remote: false}, + activities: %{local: false, remote: false} + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/description.exs b/config/description.exs index 732c76734..3781fb9cb 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2915,5 +2915,65 @@ suggestions: [2] } ] + }, + %{ + group: :pleroma, + key: :restrict_unauthenticated, + type: :group, + description: + "Disallow viewing timelines, user profiles and statuses for unauthenticated users.", + children: [ + %{ + key: :timelines, + type: :map, + description: "Settings for public and federated timelines.", + children: [ + %{ + key: :local, + type: :boolean, + description: "Disallow view public timeline." + }, + %{ + key: :federated, + type: :boolean, + description: "Disallow view federated timeline." + } + ] + }, + %{ + key: :profiles, + type: :map, + description: "Settings for user profiles.", + children: [ + %{ + key: :local, + type: :boolean, + description: "Disallow view local user profiles." + }, + %{ + key: :remote, + type: :boolean, + description: "Disallow view remote user profiles." + } + ] + }, + %{ + key: :activities, + type: :map, + description: "Settings for statuses.", + children: [ + %{ + key: :local, + type: :boolean, + description: "Disallow view local statuses." + }, + %{ + key: :remote, + type: :boolean, + description: "Disallow view remote statuses." + } + ] + } + ] } ] diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 4012fe9b1..d16435e11 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -872,3 +872,21 @@ config :auto_linker, ## :configurable_from_database Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information. + + + +## Restrict entities access for unauthenticated users + +### :restrict_unauthenticated + +Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. + +* `timelines` - public and federated timelines + * `local` - public timeline + * `federated` +* `profiles` - user profiles + * `local` + * `remote` +* `activities` - statuses + * `local` + * `remote` \ No newline at end of file diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 911dde6e2..8693c0b80 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -237,7 +237,18 @@ def visible_for?(user, for_user \\ nil) def visible_for?(%User{invisible: true}, _), do: false - def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true + def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true + + def visible_for?(%User{local: local} = user, nil) do + cfg_key = + if local, + do: :local, + else: :remote + + if Config.get([:restrict_unauthenticated, :profiles, cfg_key]), + do: false, + else: account_status(user) == :active + end def visible_for?(%User{} = user, for_user) do account_status(user) == :active || superuser?(for_user) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 6f226fc92..453a6842e 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -44,6 +44,7 @@ def is_direct?(activity) do def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(_), do: false + @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do @@ -55,14 +56,21 @@ def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{ def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false - def visible_for_user?(activity, nil) do - is_public?(activity) + def visible_for_user?(%{local: local} = activity, nil) do + cfg_key = + if local, + do: :local, + else: :remote + + if Pleroma.Config.get([:restrict_unauthenticated, :activities, cfg_key]), + do: false, + else: is_public?(activity) end def visible_for_user?(activity, user) do x = [user.ap_id | User.following(user)] y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) - visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) + is_public?(activity) || Enum.any?(x, &(&1 in y)) end def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 88c997b9f..6dbf11ac9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -60,7 +60,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do plug( Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - when action != :create + when action not in [:create, :show, :statuses] ) @relations [:follow, :unfollow] @@ -259,7 +259,8 @@ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do @doc "GET /api/v1/accounts/:id/statuses" def statuses(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do + with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user), + true <- User.visible_for?(user, reading_user) do params = params |> Map.put("tag", params["tagged"]) @@ -271,6 +272,8 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do |> add_link_headers(activities) |> put_view(StatusView) |> render("index.json", activities: activities, for: reading_user, as: :activity) + else + _e -> render_error(conn, :not_found, "Can't find user") end end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 5c90065f6..37afe6949 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -76,7 +76,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show]) @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 09e08271b..91f41416d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) @@ -75,17 +75,30 @@ def direct(%{assigns: %{user: user}} = conn, params) do def public(%{assigns: %{user: user}} = conn, params) do local_only = truthy_param?(params["local"]) - activities = - params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> ActivityPub.fetch_public_activities() + cfg_key = + if local_only do + :local + else + :federated + end - conn - |> add_link_headers(activities, %{"local" => local_only}) - |> render("index.json", activities: activities, for: user, as: :activity) + restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) + + if not (restrict? and is_nil(user)) do + activities = + params + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("local_only", local_only) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> ActivityPub.fetch_public_activities() + + conn + |> add_link_headers(activities, %{"local" => local_only}) + |> render("index.json", activities: activities, for: user, as: :activity) + else + render_error(conn, :unauthorized, "authorization required for timeline view") + end end def hashtag_fetching(params, user, local_only) do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 7efccd9c4..2182dd28e 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.Config alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -46,7 +47,7 @@ test "works by nickname" do end test "works by nickname for remote users" do - Pleroma.Config.put([:instance, :limit_to_local_content], false) + Config.put([:instance, :limit_to_local_content], false) user = insert(:user, nickname: "user@example.com", local: false) conn = @@ -58,7 +59,7 @@ test "works by nickname for remote users" do end test "respects limit_to_local_content == :all for remote user nicknames" do - Pleroma.Config.put([:instance, :limit_to_local_content], :all) + Config.put([:instance, :limit_to_local_content], :all) user = insert(:user, nickname: "user@example.com", local: false) @@ -70,7 +71,7 @@ test "respects limit_to_local_content == :all for remote user nicknames" do end test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + Config.put([:instance, :limit_to_local_content], :unauthenticated) user = insert(:user, nickname: "user@example.com", local: false) reading_user = insert(:user) @@ -140,6 +141,106 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do end end + defp local_and_remote_users do + local = insert(:user) + remote = insert(:user, local: false) + {:ok, local: local, remote: remote} + end + + describe "user fetching with restrict unauthenticated profiles for local and remote" do + setup do: local_and_remote_users() + + clear_config([:restrict_unauthenticated, :profiles, :local]) do + Config.put([:restrict_unauthenticated, :profiles, :local], true) + end + + clear_config([:restrict_unauthenticated, :profiles, :remote]) do + Config.put([:restrict_unauthenticated, :profiles, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + end + + describe "user fetching with restrict unauthenticated profiles for local" do + setup do: local_and_remote_users() + + clear_config([:restrict_unauthenticated, :profiles, :local]) do + Config.put([:restrict_unauthenticated, :profiles, :local], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + end + + describe "user fetching with restrict unauthenticated profiles for remote" do + setup do: local_and_remote_users() + + clear_config([:restrict_unauthenticated, :profiles, :remote]) do + Config.put([:restrict_unauthenticated, :profiles, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + end + describe "user timelines" do setup do: oauth_access(["read:statuses"]) @@ -293,6 +394,110 @@ test "the user views their own timelines and excludes direct messages", %{ end end + defp local_and_remote_activities(%{local: local, remote: remote}) do + insert(:note_activity, user: local) + insert(:note_activity, user: remote, local: false) + + :ok + end + + describe "statuses with restrict unauthenticated profiles for local and remote" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + clear_config([:restrict_unauthenticated, :profiles, :local]) do + Config.put([:restrict_unauthenticated, :profiles, :local], true) + end + + clear_config([:restrict_unauthenticated, :profiles, :remote]) do + Config.put([:restrict_unauthenticated, :profiles, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + end + end + + describe "statuses with restrict unauthenticated profiles for local" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + clear_config([:restrict_unauthenticated, :profiles, :local]) do + Config.put([:restrict_unauthenticated, :profiles, :local], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + end + end + + describe "statuses with restrict unauthenticated profiles for remote" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + clear_config([:restrict_unauthenticated, :profiles, :remote]) do + Config.put([:restrict_unauthenticated, :profiles, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Can't find user" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response(res_conn, 200)) == 1 + end + end + describe "followers" do setup do: oauth_access(["read:accounts"]) @@ -757,7 +962,7 @@ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_ describe "create account by app / rate limit" do clear_config([:rate_limit, :app_account_creation]) do - Pleroma.Config.put([:rate_limit, :app_account_creation], {10_000, 2}) + Config.put([:rate_limit, :app_account_creation], {10_000, 2}) end test "respects rate limit setting", %{conn: conn} do diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index fbf63f608..81513a429 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -476,6 +476,103 @@ test "get a status" do assert id == to_string(activity.id) end + defp local_and_remote_activities do + local = insert(:note_activity) + remote = insert(:note_activity, local: false) + {:ok, local: local, remote: remote} + end + + describe "status with restrict unauthenticated activities for local and remote" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :activities, :local]) do + Config.put([:restrict_unauthenticated, :activities, :local], true) + end + + clear_config([:restrict_unauthenticated, :activities, :remote]) do + Config.put([:restrict_unauthenticated, :activities, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Record not found" + } + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Record not found" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + end + + describe "status with restrict unauthenticated activities for local" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :activities, :local]) do + Config.put([:restrict_unauthenticated, :activities, :local], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Record not found" + } + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + end + + describe "status with restrict unauthenticated activities for remote" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :activities, :remote]) do + Config.put([:restrict_unauthenticated, :activities, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + + assert json_response(res_conn, :not_found) == %{ + "error" => "Record not found" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response(res_conn, 200) + end + end + test "getting a status that doesn't exist returns 404" do %{conn: conn} = oauth_access(["read:statuses"]) activity = insert(:note_activity) @@ -514,6 +611,78 @@ test "get statuses by IDs" do assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"]) end + describe "getting statuses by ids with restricted unauthenticated for local and remote" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :activities, :local]) do + Config.put([:restrict_unauthenticated, :activities, :local], true) + end + + clear_config([:restrict_unauthenticated, :activities, :remote]) do + Config.put([:restrict_unauthenticated, :activities, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) + + assert json_response(res_conn, 200) == [] + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) + + assert length(json_response(res_conn, 200)) == 2 + end + end + + describe "getting statuses by ids with restricted unauthenticated for local" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :activities, :local]) do + Config.put([:restrict_unauthenticated, :activities, :local], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) + + remote_id = remote.id + assert [%{"id" => ^remote_id}] = json_response(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) + + assert length(json_response(res_conn, 200)) == 2 + end + end + + describe "getting statuses by ids with restricted unauthenticated for remote" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :activities, :remote]) do + Config.put([:restrict_unauthenticated, :activities, :remote], true) + end + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) + + local_id = local.id + assert [%{"id" => ^local_id}] = json_response(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) + + assert length(json_response(res_conn, 200)) == 2 + end + end + describe "deleting a status" do test "when you created it" do %{user: author, conn: conn} = oauth_access(["write:statuses"]) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 2c03b0a75..a15c759d4 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -12,8 +12,6 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI - clear_config([:instance, :public]) - setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok @@ -80,15 +78,6 @@ test "the public timeline", %{conn: conn} do assert [%{"content" => "test"}] = json_response(conn, :ok) end - test "the public timeline when public is set to false", %{conn: conn} do - Config.put([:instance, :public], false) - - assert %{"error" => "This resource requires authentication."} == - conn - |> get("/api/v1/timelines/public", %{"local" => "False"}) - |> json_response(:forbidden) - end - test "the public timeline includes only public statuses for an authenticated user" do %{user: user, conn: conn} = oauth_access(["read:statuses"]) @@ -102,6 +91,106 @@ test "the public timeline includes only public statuses for an authenticated use end end + defp local_and_remote_activities do + insert(:note_activity) + insert(:note_activity, local: false) + :ok + end + + describe "public with restrict unauthenticated timeline for local and federated timelines" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :timelines, :local]) do + Config.put([:restrict_unauthenticated, :timelines, :local], true) + end + + clear_config([:restrict_unauthenticated, :timelines, :federated]) do + Config.put([:restrict_unauthenticated, :timelines, :federated], true) + end + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) + + assert json_response(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "false"}) + + assert json_response(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + end + + test "if user is authenticated" do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "false"}) + assert length(json_response(res_conn, 200)) == 2 + end + end + + describe "public with restrict unauthenticated timeline for local" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :timelines, :local]) do + Config.put([:restrict_unauthenticated, :timelines, :local], true) + end + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) + + assert json_response(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "false"}) + assert length(json_response(res_conn, 200)) == 2 + end + + test "if user is authenticated", %{conn: _conn} do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "false"}) + assert length(json_response(res_conn, 200)) == 2 + end + end + + describe "public with restrict unauthenticated timeline for remote" do + setup do: local_and_remote_activities() + + clear_config([:restrict_unauthenticated, :timelines, :federated]) do + Config.put([:restrict_unauthenticated, :timelines, :federated], true) + end + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "false"}) + + assert json_response(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + end + + test "if user is authenticated", %{conn: _conn} do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "false"}) + assert length(json_response(res_conn, 200)) == 2 + end + end + describe "direct" do test "direct timeline", %{conn: conn} do user_one = insert(:user) From 1c05f539aaea32fe993e5299e656aa44c322e8de Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 20 Mar 2020 18:33:00 +0300 Subject: [PATCH 62/79] Improved in-test `clear_config/n` applicability (setup / setup_all / in-test usage). --- test/activity_expiration_test.exs | 2 +- test/activity_test.exs | 2 +- test/captcha_test.exs | 3 +- test/config/transfer_task_test.exs | 2 +- test/conversation_test.exs | 2 +- test/emails/mailer_test.exs | 3 +- test/http/request_builder_test.exs | 4 +- test/object/fetcher_test.exs | 5 +- test/object_test.exs | 4 +- .../admin_secret_authentication_plug_test.exs | 2 +- ...sure_public_or_authenticated_plug_test.exs | 2 +- test/plugs/http_security_plug_test.exs | 6 +- test/plugs/instance_static_test.exs | 2 +- test/plugs/oauth_scopes_plug_test.exs | 2 +- test/plugs/rate_limiter_test.exs | 8 +-- test/plugs/remote_ip_test.exs | 3 +- test/plugs/user_enabled_plug_test.exs | 2 +- test/plugs/user_is_admin_plug_test.exs | 4 +- test/repo_test.exs | 2 +- test/scheduled_activity_test.exs | 2 +- test/support/helpers.ex | 58 ++----------------- test/tasks/config_test.exs | 2 +- test/tasks/robots_txt_test.exs | 2 +- .../upload/filter/anonymize_filename_test.exs | 2 +- test/upload/filter/mogrify_test.exs | 2 +- test/upload/filter_test.exs | 2 +- test/upload_test.exs | 2 +- test/uploaders/s3_test.exs | 9 +-- test/user_search_test.exs | 2 +- test/user_test.exs | 29 +++++----- .../activity_pub_controller_test.exs | 8 +-- test/web/activity_pub/activity_pub_test.exs | 6 +- .../mrf/hellthread_policy_test.exs | 2 +- .../activity_pub/mrf/keyword_policy_test.exs | 2 +- .../activity_pub/mrf/mention_policy_test.exs | 2 +- test/web/activity_pub/mrf/mrf_test.exs | 2 +- .../mrf/object_age_policy_test.exs | 9 +-- .../mrf/reject_non_public_test.exs | 2 +- .../activity_pub/mrf/simple_policy_test.exs | 21 +++---- .../activity_pub/mrf/subchain_policy_test.exs | 3 +- .../mrf/user_allowlist_policy_test.exs | 2 +- .../mrf/vocabulary_policy_test.exs | 4 +- test/web/activity_pub/publisher_test.exs | 2 +- test/web/activity_pub/relay_test.exs | 2 +- .../transmogrifier/follow_handling_test.exs | 2 +- test/web/activity_pub/transmogrifier_test.exs | 16 +++-- .../activity_pub/views/object_view_test.exs | 2 +- .../admin_api/admin_api_controller_test.exs | 18 +++--- test/web/chat_channel_test.exs | 2 +- test/web/common_api/common_api_test.exs | 6 +- test/web/federator_test.exs | 9 ++- test/web/feed/tag_controller_test.exs | 2 +- test/web/feed/user_controller_test.exs | 4 +- test/web/instances/instance_test.exs | 2 +- test/web/instances/instances_test.exs | 2 +- test/web/masto_fe_controller_test.exs | 2 +- .../update_credentials_test.exs | 3 +- .../controllers/account_controller_test.exs | 40 ++++--------- .../controllers/media_controller_test.exs | 4 +- .../scheduled_activity_controller_test.exs | 2 +- .../controllers/status_controller_test.exs | 40 ++++--------- .../controllers/timeline_controller_test.exs | 16 ++--- .../media_proxy_controller_test.exs | 4 +- test/web/media_proxy/media_proxy_test.exs | 4 +- test/web/metadata/opengraph_test.exs | 2 +- test/web/metadata/twitter_card_test.exs | 2 +- test/web/node_info_test.exs | 6 +- test/web/oauth/ldap_authorization_test.exs | 4 +- test/web/oauth/oauth_controller_test.exs | 7 +-- test/web/ostatus/ostatus_controller_test.exs | 2 +- .../controllers/account_controller_test.exs | 2 +- .../controllers/emoji_api_controller_test.exs | 3 +- test/web/plugs/federating_plug_test.exs | 2 +- test/web/rich_media/helpers_test.exs | 2 +- .../static_fe/static_fe_controller_test.exs | 5 +- test/web/streamer/streamer_test.exs | 3 +- .../remote_follow_controller_test.exs | 9 ++- test/web/twitter_api/twitter_api_test.exs | 8 +-- test/web/twitter_api/util_controller_test.exs | 8 +-- .../web_finger/web_finger_controller_test.exs | 2 +- .../cron/clear_oauth_token_worker_test.exs | 2 +- .../cron/digest_emails_worker_test.exs | 2 +- .../purge_expired_activities_worker_test.exs | 2 +- .../scheduled_activity_worker_test.exs | 2 +- 84 files changed, 196 insertions(+), 298 deletions(-) diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs index 4cda5e985..e899d4509 100644 --- a/test/activity_expiration_test.exs +++ b/test/activity_expiration_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.ActivityExpirationTest do alias Pleroma.ActivityExpiration import Pleroma.Factory - clear_config([ActivityExpiration, :enabled]) + setup do: clear_config([ActivityExpiration, :enabled]) test "finds activities due to be deleted only" do activity = insert(:note_activity) diff --git a/test/activity_test.exs b/test/activity_test.exs index 46b55beaa..0c19f481b 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -138,7 +138,7 @@ test "when association is not loaded" do } end - clear_config([:instance, :limit_to_local_content]) + setup do: clear_config([:instance, :limit_to_local_content]) test "finds utf8 text in statuses", %{ japanese_activity: japanese_activity, diff --git a/test/captcha_test.exs b/test/captcha_test.exs index 5e29b48b0..ac1d846e8 100644 --- a/test/captcha_test.exs +++ b/test/captcha_test.exs @@ -12,8 +12,7 @@ defmodule Pleroma.CaptchaTest do alias Pleroma.Captcha.Native @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] - - clear_config([Pleroma.Captcha, :enabled]) + setup do: clear_config([Pleroma.Captcha, :enabled]) describe "Kocaptcha" do setup do diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 7bfae67bf..0265a6156 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Config.TransferTaskTest do alias Pleroma.Config.TransferTask alias Pleroma.ConfigDB - clear_config(:configurable_from_database, true) + setup do: clear_config(:configurable_from_database, true) test "transfer config values from db to env" do refute Application.get_env(:pleroma, :test_key) diff --git a/test/conversation_test.exs b/test/conversation_test.exs index 3c54253e3..056a0e920 100644 --- a/test/conversation_test.exs +++ b/test/conversation_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.ConversationTest do import Pleroma.Factory - clear_config_all([:instance, :federating], true) + setup_all do: clear_config([:instance, :federating], true) test "it goes through old direct conversations" do user = insert(:user) diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs index f30aa6a72..e6e34cba8 100644 --- a/test/emails/mailer_test.exs +++ b/test/emails/mailer_test.exs @@ -14,8 +14,7 @@ defmodule Pleroma.Emails.MailerTest do subject: "Pleroma test email", to: [{"Test User", "user1@example.com"}] } - - clear_config([Pleroma.Emails.Mailer, :enabled]) + setup do: clear_config([Pleroma.Emails.Mailer, :enabled]) test "not send email when mailer is disabled" do Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs index 11a9314ae..bf3a15ebe 100644 --- a/test/http/request_builder_test.exs +++ b/test/http/request_builder_test.exs @@ -8,8 +8,8 @@ defmodule Pleroma.HTTP.RequestBuilderTest do alias Pleroma.HTTP.RequestBuilder describe "headers/2" do - clear_config([:http, :send_user_agent]) - clear_config([:http, :user_agent]) + setup do: clear_config([:http, :send_user_agent]) + setup do: clear_config([:http, :user_agent]) test "don't send pleroma user agent" do assert RequestBuilder.headers(%{}, []) == %{headers: []} diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 4775ee152..c06e91f12 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -28,8 +28,7 @@ defmodule Pleroma.Object.FetcherTest do describe "max thread distance restriction" do @ap_id "http://mastodon.example.org/@admin/99541947525187367" - - clear_config([:instance, :federation_incoming_replies_max_depth]) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) test "it returns thread depth exceeded error if thread depth is exceeded" do Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) @@ -160,7 +159,7 @@ test "it can refetch pruned objects" do end describe "signed fetches" do - clear_config([:activitypub, :sign_object_fetches]) + setup do: clear_config([:activitypub, :sign_object_fetches]) test_with_mock "it signs fetches when configured to do so", Pleroma.Signature, diff --git a/test/object_test.exs b/test/object_test.exs index 85b2a3f6d..fe583decd 100644 --- a/test/object_test.exs +++ b/test/object_test.exs @@ -74,8 +74,8 @@ test "ensures cache is cleared for the object" do end describe "delete attachments" do - clear_config([Pleroma.Upload]) - clear_config([:instance, :cleanup_attachments]) + setup do: clear_config([Pleroma.Upload]) + setup do: clear_config([:instance, :cleanup_attachments]) test "Disabled via config" do Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs index 2e300ac0c..100016c62 100644 --- a/test/plugs/admin_secret_authentication_plug_test.exs +++ b/test/plugs/admin_secret_authentication_plug_test.exs @@ -23,7 +23,7 @@ test "does nothing if a user is assigned", %{conn: conn} do end describe "when secret set it assigns an admin user" do - clear_config([:admin_token]) + setup do: clear_config([:admin_token]) test "with `admin_token` query parameter", %{conn: conn} do Pleroma.Config.put(:admin_token, "password123") diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs index 3fcb4d372..411252274 100644 --- a/test/plugs/ensure_public_or_authenticated_plug_test.exs +++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.User - clear_config([:instance, :public]) + setup do: clear_config([:instance, :public]) test "it halts if not public and no user is assigned", %{conn: conn} do Config.put([:instance, :public], false) diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 944a9a139..84e4c274f 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -7,9 +7,9 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do alias Pleroma.Config alias Plug.Conn - clear_config([:http_securiy, :enabled]) - clear_config([:http_security, :sts]) - clear_config([:http_security, :referrer_policy]) + setup do: clear_config([:http_securiy, :enabled]) + setup do: clear_config([:http_security, :sts]) + setup do: clear_config([:http_security, :referrer_policy]) describe "http security enabled" do setup do diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs index 2e9d2dc46..b8f070d6a 100644 --- a/test/plugs/instance_static_test.exs +++ b/test/plugs/instance_static_test.exs @@ -12,7 +12,7 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do on_exit(fn -> File.rm_rf(@dir) end) end - clear_config([:instance, :static_dir], @dir) + setup do: clear_config([:instance, :static_dir], @dir) test "overrides index" do bundled_index = get(build_conn(), "/") diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index 1b3aa85b6..e79ecf263 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -193,7 +193,7 @@ test "filters scopes which directly match or are ancestors of supported scopes" end describe "transform_scopes/2" do - clear_config([:auth, :enforce_oauth_admin_scope_usage]) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage]) setup do {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}} diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs index c6e494c13..0ce9f3a0a 100644 --- a/test/plugs/rate_limiter_test.exs +++ b/test/plugs/rate_limiter_test.exs @@ -12,14 +12,12 @@ defmodule Pleroma.Plugs.RateLimiterTest do import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] # Note: each example must work with separate buckets in order to prevent concurrency issues - - clear_config([Pleroma.Web.Endpoint, :http, :ip]) - clear_config(:rate_limit) + setup do: clear_config([Pleroma.Web.Endpoint, :http, :ip]) + setup do: clear_config(:rate_limit) describe "config" do @limiter_name :test_init - - clear_config([Pleroma.Plugs.RemoteIp, :enabled]) + setup do: clear_config([Pleroma.Plugs.RemoteIp, :enabled]) test "config is required for plug to work" do Config.put([:rate_limit, @limiter_name], {1, 1}) diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index 9c3737b0b..752ab32e7 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -9,8 +9,7 @@ defmodule Pleroma.Plugs.RemoteIpTest do alias Pleroma.Plugs.RemoteIp import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] - - clear_config(RemoteIp) + setup do: clear_config(RemoteIp) test "disabled" do Pleroma.Config.put(RemoteIp, enabled: false) diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs index 931513d83..b219d8abf 100644 --- a/test/plugs/user_enabled_plug_test.exs +++ b/test/plugs/user_enabled_plug_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Plugs.UserEnabledPlugTest do alias Pleroma.Plugs.UserEnabledPlug import Pleroma.Factory - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) test "doesn't do anything if the user isn't set", %{conn: conn} do ret_conn = diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 1062d6e70..fd6a50e53 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do import Pleroma.Factory describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) test "accepts a user that is an admin" do user = insert(:user, is_admin: true) @@ -40,7 +40,7 @@ test "denies when a user isn't set" do end describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage], true) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) setup do admin_user = insert(:user, is_admin: true) diff --git a/test/repo_test.exs b/test/repo_test.exs index 75e85f974..daffc6542 100644 --- a/test/repo_test.exs +++ b/test/repo_test.exs @@ -67,7 +67,7 @@ test "return error if has not assoc " do :ok end - clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) + setup do: clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) test "raises if it detects unapplied migrations" do assert_raise Pleroma.Repo.UnappliedMigrationsError, fn -> diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs index 4369e7e8a..7faa5660d 100644 --- a/test/scheduled_activity_test.exs +++ b/test/scheduled_activity_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.ScheduledActivityTest do alias Pleroma.ScheduledActivity import Pleroma.Factory - clear_config([ScheduledActivity, :enabled]) + setup do: clear_config([ScheduledActivity, :enabled]) setup context do DataCase.ensure_local_uploader(context) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index c6f7fa5e2..e68e9bfd2 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -17,12 +17,10 @@ defmacro clear_config(config_path) do defmacro clear_config(config_path, do: yield) do quote do - setup do - initial_setting = Config.get(unquote(config_path)) - unquote(yield) - on_exit(fn -> Config.put(unquote(config_path), initial_setting) end) - :ok - end + initial_setting = Config.get(unquote(config_path)) + unquote(yield) + on_exit(fn -> Config.put(unquote(config_path), initial_setting) end) + :ok end end @@ -34,58 +32,12 @@ defmacro clear_config(config_path, temp_setting) do end end - @doc """ - From _within a test case_, sets config to provided value and restores initial value on exit. - For multi-case setup use `clear_config/2` instead. - """ - def set_config(config_path, temp_setting) do - initial_setting = Config.get(config_path) - Config.put(config_path, temp_setting) - - ExUnit.Callbacks.on_exit(fn -> Config.put(config_path, initial_setting) end) - end - - @doc "Stores initial config value and restores it after *all* test examples are executed." - defmacro clear_config_all(config_path) do - quote do - clear_config_all(unquote(config_path)) do - end - end - end - - @doc """ - Stores initial config value and restores it after *all* test examples are executed. - Only use if *all* test examples should work with the same stubbed value - (*no* examples set a different value). - """ - defmacro clear_config_all(config_path, do: yield) do - quote do - setup_all do - initial_setting = Config.get(unquote(config_path)) - unquote(yield) - on_exit(fn -> Config.put(unquote(config_path), initial_setting) end) - :ok - end - end - end - - defmacro clear_config_all(config_path, temp_setting) do - quote do - clear_config_all(unquote(config_path)) do - Config.put(unquote(config_path), unquote(temp_setting)) - end - end - end - defmacro __using__(_opts) do quote do import Pleroma.Tests.Helpers, only: [ clear_config: 1, - clear_config: 2, - clear_config_all: 1, - clear_config_all: 2, - set_config: 2 + clear_config: 2 ] def to_datetime(naive_datetime) do diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index b0c2efc98..3dee4f082 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -20,7 +20,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do :ok end - clear_config_all(:configurable_from_database, true) + setup_all do: clear_config(:configurable_from_database, true) test "error if file with custom settings doesn't exist" do Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs") diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs index e03c9c192..7040a0e4e 100644 --- a/test/tasks/robots_txt_test.exs +++ b/test/tasks/robots_txt_test.exs @@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do use Pleroma.Tests.Helpers alias Mix.Tasks.Pleroma.RobotsTxt - clear_config([:instance, :static_dir]) + setup do: clear_config([:instance, :static_dir]) test "creates new dir" do path = "test/fixtures/new_dir/" diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs index 330158580..2d5c580f1 100644 --- a/test/upload/filter/anonymize_filename_test.exs +++ b/test/upload/filter/anonymize_filename_test.exs @@ -18,7 +18,7 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do %{upload_file: upload_file} end - clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) test "it replaces filename on pre-defined text", %{upload_file: upload_file} do Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs index 52483d80c..b6a463e8c 100644 --- a/test/upload/filter/mogrify_test.exs +++ b/test/upload/filter/mogrify_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do alias Pleroma.Upload alias Pleroma.Upload.Filter - clear_config([Filter.Mogrify, :args]) + setup do: clear_config([Filter.Mogrify, :args]) test "apply mogrify filter" do Config.put([Filter.Mogrify, :args], [{"tint", "40"}]) diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs index 2ffc5247b..352b66402 100644 --- a/test/upload/filter_test.exs +++ b/test/upload/filter_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Upload.FilterTest do alias Pleroma.Config alias Pleroma.Upload.Filter - clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) test "applies filters" do Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") diff --git a/test/upload_test.exs b/test/upload_test.exs index 6bf7f2417..060a940bb 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -250,7 +250,7 @@ test "escapes reserved uri characters" do end describe "Setting a custom base_url for uploaded media" do - clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") + setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") test "returns a media url with configured base_url" do base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs index 96c21c0e5..6950ccb25 100644 --- a/test/uploaders/s3_test.exs +++ b/test/uploaders/s3_test.exs @@ -11,10 +11,11 @@ defmodule Pleroma.Uploaders.S3Test do import Mock import ExUnit.CaptureLog - clear_config(Pleroma.Uploaders.S3, - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com" - ) + setup do: + clear_config(Pleroma.Uploaders.S3, + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com" + ) describe "get_file/1" do test "it returns path to local folder for files" do diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 406cc8fb2..cb847b516 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.UserSearchTest do end describe "User.search" do - clear_config([:instance, :limit_to_local_content]) + setup do: clear_config([:instance, :limit_to_local_content]) test "excluded invisible users from results" do user = insert(:user, %{nickname: "john t1000"}) diff --git a/test/user_test.exs b/test/user_test.exs index e0e7a26b8..119a36ec1 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -24,7 +24,7 @@ defmodule Pleroma.UserTest do :ok end - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) describe "service actors" do test "returns updated invisible actor" do @@ -297,7 +297,7 @@ test "local users do not automatically follow local locked accounts" do end describe "unfollow/2" do - clear_config([:instance, :external_user_synchronization]) + setup do: clear_config([:instance, :external_user_synchronization]) test "unfollow with syncronizes external user" do Pleroma.Config.put([:instance, :external_user_synchronization], true) @@ -375,10 +375,9 @@ test "fetches correct profile for nickname beginning with number" do password_confirmation: "test", email: "email@example.com" } - - clear_config([:instance, :autofollowed_nicknames]) - clear_config([:instance, :welcome_message]) - clear_config([:instance, :welcome_user_nickname]) + setup do: clear_config([:instance, :autofollowed_nicknames]) + setup do: clear_config([:instance, :welcome_message]) + setup do: clear_config([:instance, :welcome_user_nickname]) test "it autofollows accounts that are set for it" do user = insert(:user) @@ -412,7 +411,7 @@ test "it sends a welcome message if it is set" do assert activity.actor == welcome_user.ap_id end - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do Pleroma.Config.put([:instance, :account_activation_required], true) @@ -475,8 +474,7 @@ test "it sets the password_hash and ap_id" do password_confirmation: "test", email: "email@example.com" } - - clear_config([:instance, :account_activation_required], true) + setup do: clear_config([:instance, :account_activation_required], true) test "it creates unconfirmed user" do changeset = User.register_changeset(%User{}, @full_user_data) @@ -619,9 +617,8 @@ test "returns an ap_followers link for a user" do ap_id: "http...", avatar: %{some: "avatar"} } - - clear_config([:instance, :user_bio_length]) - clear_config([:instance, :user_name_length]) + setup do: clear_config([:instance, :user_bio_length]) + setup do: clear_config([:instance, :user_name_length]) test "it confirms validity" do cs = User.remote_user_creation(@valid_remote) @@ -1114,7 +1111,7 @@ test "hide a user's statuses from timelines and notifications" do [user: user] end - clear_config([:instance, :federating]) + setup do: clear_config([:instance, :federating]) test ".delete_user_activities deletes all create activities", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) @@ -1295,7 +1292,7 @@ test "User.delete() plugs any possible zombie objects" do end describe "account_status/1" do - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) test "return confirmation_pending for unconfirm user" do Pleroma.Config.put([:instance, :account_activation_required], true) @@ -1663,7 +1660,7 @@ test "performs update cache if user updated" do end describe "following/followers synchronization" do - clear_config([:instance, :external_user_synchronization]) + setup do: clear_config([:instance, :external_user_synchronization]) test "updates the counters normally on following/getting a follow when disabled" do Pleroma.Config.put([:instance, :external_user_synchronization], false) @@ -1768,7 +1765,7 @@ test "changes email", %{user: user} do [local_user: local_user, remote_user: remote_user] end - clear_config([:instance, :limit_to_local_content]) + setup do: clear_config([:instance, :limit_to_local_content]) test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{ remote_user: remote_user diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index df0c53458..573853afa 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -26,10 +26,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do :ok end - clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :federating], true) describe "/relay" do - clear_config([:instance, :allow_relay]) + setup do: clear_config([:instance, :allow_relay]) test "with the relay active, it returns the relay user", %{conn: conn} do res = @@ -1225,8 +1225,8 @@ test "GET /api/ap/whoami", %{conn: conn} do |> json_response(403) end - clear_config([:media_proxy]) - clear_config([Pleroma.Upload]) + setup do: clear_config([:media_proxy]) + setup do: clear_config([Pleroma.Upload]) test "POST /api/ap/upload_media", %{conn: conn} do user = insert(:user) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index d86c8260e..a43dd34f0 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do :ok end - clear_config([:instance, :federating]) + setup do: clear_config([:instance, :federating]) describe "streaming out participations" do test "it streams them out" do @@ -1396,7 +1396,7 @@ test "creates an undo activity for the last block" do end describe "deletion" do - clear_config([:instance, :rewrite_policy]) + setup do: clear_config([:instance, :rewrite_policy]) test "it reverts deletion on error" do note = insert(:note_activity) @@ -1580,7 +1580,7 @@ test "it filters broken threads" do end describe "update" do - clear_config([:instance, :max_pinned_statuses]) + setup do: clear_config([:instance, :max_pinned_statuses]) test "it creates an update activity with the new user data" do user = insert(:user) diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs index 916b95692..95ef0b168 100644 --- a/test/web/activity_pub/mrf/hellthread_policy_test.exs +++ b/test/web/activity_pub/mrf/hellthread_policy_test.exs @@ -26,7 +26,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do [user: user, message: message] end - clear_config(:mrf_hellthread) + setup do: clear_config(:mrf_hellthread) describe "reject" do test "rejects the message if the recipient count is above reject_threshold", %{ diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs index 18242a889..fd1f7aec8 100644 --- a/test/web/activity_pub/mrf/keyword_policy_test.exs +++ b/test/web/activity_pub/mrf/keyword_policy_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy - clear_config(:mrf_keyword) + setup do: clear_config(:mrf_keyword) setup do Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []}) diff --git a/test/web/activity_pub/mrf/mention_policy_test.exs b/test/web/activity_pub/mrf/mention_policy_test.exs index 08f7be542..aa003bef5 100644 --- a/test/web/activity_pub/mrf/mention_policy_test.exs +++ b/test/web/activity_pub/mrf/mention_policy_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do alias Pleroma.Web.ActivityPub.MRF.MentionPolicy - clear_config(:mrf_mention) + setup do: clear_config(:mrf_mention) test "pass filter if allow list is empty" do Pleroma.Config.delete([:mrf_mention]) diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs index 04709df17..c941066f2 100644 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ b/test/web/activity_pub/mrf/mrf_test.exs @@ -60,7 +60,7 @@ test "matches are case-insensitive" do end describe "describe/0" do - clear_config([:instance, :rewrite_policy]) + setup do: clear_config([:instance, :rewrite_policy]) test "it works as expected with noop policy" do expected = %{ diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs index bdbbb1fc4..0fbc5f57a 100644 --- a/test/web/activity_pub/mrf/object_age_policy_test.exs +++ b/test/web/activity_pub/mrf/object_age_policy_test.exs @@ -9,10 +9,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy alias Pleroma.Web.ActivityPub.Visibility - clear_config(:mrf_object_age, - threshold: 172_800, - actions: [:delist, :strip_followers] - ) + setup do: + clear_config(:mrf_object_age, + threshold: 172_800, + actions: [:delist, :strip_followers] + ) setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs index fc1d190bb..abfd32df8 100644 --- a/test/web/activity_pub/mrf/reject_non_public_test.exs +++ b/test/web/activity_pub/mrf/reject_non_public_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic - clear_config([:mrf_rejectnonpublic]) + setup do: clear_config([:mrf_rejectnonpublic]) describe "public message" do test "it's allowed when address is public" do diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index 97aec6622..5aebbc675 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -8,16 +8,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do alias Pleroma.Config alias Pleroma.Web.ActivityPub.MRF.SimplePolicy - clear_config(:mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - report_removal: [], - reject: [], - accept: [], - avatar_removal: [], - banner_removal: [] - ) + setup do: + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + accept: [], + avatar_removal: [], + banner_removal: [] + ) describe "when :media_removal" do test "is empty" do diff --git a/test/web/activity_pub/mrf/subchain_policy_test.exs b/test/web/activity_pub/mrf/subchain_policy_test.exs index 221b8958e..fff66cb7e 100644 --- a/test/web/activity_pub/mrf/subchain_policy_test.exs +++ b/test/web/activity_pub/mrf/subchain_policy_test.exs @@ -13,8 +13,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do "type" => "Create", "object" => %{"content" => "hi"} } - - clear_config([:mrf_subchain, :match_actor]) + setup do: clear_config([:mrf_subchain, :match_actor]) test "it matches and processes subchains when the actor matches a configured target" do Pleroma.Config.put([:mrf_subchain, :match_actor], %{ diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs index 87c9e1b29..724bae058 100644 --- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs +++ b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy - clear_config([:mrf_user_allowlist, :localhost]) + setup do: clear_config([:mrf_user_allowlist, :localhost]) test "pass filter if allow list is empty" do actor = insert(:user) diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs index d9207b095..69f22bb77 100644 --- a/test/web/activity_pub/mrf/vocabulary_policy_test.exs +++ b/test/web/activity_pub/mrf/vocabulary_policy_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy describe "accept" do - clear_config([:mrf_vocabulary, :accept]) + setup do: clear_config([:mrf_vocabulary, :accept]) test "it accepts based on parent activity type" do Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) @@ -65,7 +65,7 @@ test "it does not accept disallowed parent types" do end describe "reject" do - clear_config([:mrf_vocabulary, :reject]) + setup do: clear_config([:mrf_vocabulary, :reject]) test "it rejects based on parent activity type" do Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs index ed9c951dd..801da03c1 100644 --- a/test/web/activity_pub/publisher_test.exs +++ b/test/web/activity_pub/publisher_test.exs @@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do :ok end - clear_config_all([:instance, :federating], true) + setup_all do: clear_config([:instance, :federating], true) describe "gather_webfinger_links/1" do test "it returns links" do diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index e3115dcd8..040625e4d 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -68,7 +68,7 @@ test "returns activity" do end describe "publish/1" do - clear_config([:instance, :federating]) + setup do: clear_config([:instance, :federating]) test "returns error when activity not `Create` type" do activity = insert(:like_activity) diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index c3d3f9830..967389fae 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do end describe "handle_incoming" do - clear_config([:user, :deny_follow_blocked]) + setup do: clear_config([:user, :deny_follow_blocked]) test "it works for osada follow request" do user = insert(:user) diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index c025b6b78..b2cabbd30 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -25,7 +25,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :ok end - clear_config([:instance, :max_remote_account_fields]) + setup do: clear_config([:instance, :max_remote_account_fields]) describe "handle_incoming" do test "it ignores an incoming notice if we already have it" do @@ -1351,9 +1351,8 @@ test "it accepts Move activities" do end describe "`handle_incoming/2`, Mastodon format `replies` handling" do - clear_config([:activitypub, :note_replies_output_limit], 5) - - clear_config([:instance, :federation_incoming_replies_max_depth]) + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) setup do data = @@ -1392,9 +1391,8 @@ test "does NOT schedule background fetching of `replies` beyond max thread depth end describe "`handle_incoming/2`, Pleroma format `replies` handling" do - clear_config([:activitypub, :note_replies_output_limit], 5) - - clear_config([:instance, :federation_incoming_replies_max_depth]) + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) setup do user = insert(:user) @@ -1878,7 +1876,7 @@ test "returns fixed object" do end describe "fix_in_reply_to/2" do - clear_config([:instance, :federation_incoming_replies_max_depth]) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) setup do data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) @@ -2141,7 +2139,7 @@ test "returns object with emoji when object contains map tag" do end describe "set_replies/1" do - clear_config([:activitypub, :note_replies_output_limit], 2) + setup do: clear_config([:activitypub, :note_replies_output_limit], 2) test "returns unmodified object if activity doesn't have self-replies" do data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs index 7dda20ec3..de5ffc5b3 100644 --- a/test/web/activity_pub/views/object_view_test.exs +++ b/test/web/activity_pub/views/object_view_test.exs @@ -37,7 +37,7 @@ test "renders a note activity" do end describe "note activity's `replies` collection rendering" do - clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) test "renders `replies` collection for a note activity" do user = insert(:user) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 5f3064941..0a902585d 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -43,7 +43,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage], true) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", %{admin: admin} do @@ -91,7 +91,7 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro end describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) test "GET /api/pleroma/admin/users/:nickname requires " <> "read:accounts or admin:read:accounts or broader scope", @@ -577,8 +577,8 @@ test "/:right DELETE, can remove from a permission group (multiple)", %{ end describe "POST /api/pleroma/admin/email_invite, with valid config" do - clear_config([:instance, :registrations_open], false) - clear_config([:instance, :invites_enabled], true) + setup do: clear_config([:instance, :registrations_open], false) + setup do: clear_config([:instance, :invites_enabled], true) test "sends invitation and returns 204", %{admin: admin, conn: conn} do recipient_email = "foo@bar.com" @@ -629,8 +629,8 @@ test "it returns 403 if requested by a non-admin" do end describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do - clear_config([:instance, :registrations_open]) - clear_config([:instance, :invites_enabled]) + setup do: clear_config([:instance, :registrations_open]) + setup do: clear_config([:instance, :invites_enabled]) test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do Config.put([:instance, :registrations_open], false) @@ -1879,7 +1879,7 @@ test "returns 404 when the status does not exist", %{conn: conn} do end describe "GET /api/pleroma/admin/config" do - clear_config(:configurable_from_database, true) + setup do: clear_config(:configurable_from_database, true) test "when configuration from database is off", %{conn: conn} do Config.put(:configurable_from_database, false) @@ -2030,7 +2030,7 @@ test "POST /api/pleroma/admin/config error", %{conn: conn} do end) end - clear_config(:configurable_from_database, true) + setup do: clear_config(:configurable_from_database, true) @tag capture_log: true test "create new config setting in db", %{conn: conn} do @@ -3039,7 +3039,7 @@ test "proxy tuple ip", %{conn: conn} do end describe "GET /api/pleroma/admin/restart" do - clear_config(:configurable_from_database, true) + setup do: clear_config(:configurable_from_database, true) test "pleroma restarts", %{conn: conn} do capture_log(fn -> diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs index 68c24a9f9..f18f3a212 100644 --- a/test/web/chat_channel_test.exs +++ b/test/web/chat_channel_test.exs @@ -21,7 +21,7 @@ test "it broadcasts a message", %{socket: socket} do end describe "message lengths" do - clear_config([:instance, :chat_limit]) + setup do: clear_config([:instance, :chat_limit]) test "it ignores messages of length zero", %{socket: socket} do push(socket, "new_msg", %{"text" => ""}) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index b80523160..0da0bd2e2 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -17,9 +17,9 @@ defmodule Pleroma.Web.CommonAPITest do require Pleroma.Constants - clear_config([:instance, :safe_dm_mentions]) - clear_config([:instance, :limit]) - clear_config([:instance, :max_pinned_statuses]) + setup do: clear_config([:instance, :safe_dm_mentions]) + setup do: clear_config([:instance, :limit]) + setup do: clear_config([:instance, :max_pinned_statuses]) test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do user = insert(:user) diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 2b321d263..da844c24c 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -21,11 +21,10 @@ defmodule Pleroma.Web.FederatorTest do :ok end - clear_config_all([:instance, :federating], true) - - clear_config([:instance, :allow_relay]) - clear_config([:instance, :rewrite_policy]) - clear_config([:mrf_keyword]) + setup_all do: clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :allow_relay]) + setup do: clear_config([:instance, :rewrite_policy]) + setup do: clear_config([:mrf_keyword]) describe "Publish an activity" do setup do diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs index 5950605e8..1ec39ec5d 100644 --- a/test/web/feed/tag_controller_test.exs +++ b/test/web/feed/tag_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do alias Pleroma.Web.Feed.FeedView - clear_config([:feed]) + setup do: clear_config([:feed]) test "gets a feed (ATOM)", %{conn: conn} do Pleroma.Config.put( diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs index 49cfecde3..3e52eb42b 100644 --- a/test/web/feed/user_controller_test.exs +++ b/test/web/feed/user_controller_test.exs @@ -12,10 +12,10 @@ defmodule Pleroma.Web.Feed.UserControllerTest do alias Pleroma.Object alias Pleroma.User - clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :federating], true) describe "feed" do - clear_config([:feed]) + setup do: clear_config([:feed]) test "gets a feed", %{conn: conn} do Config.put( diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index ab8e5643b..e463200ca 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Instances.InstanceTest do import Pleroma.Factory - clear_config_all([:instance, :federation_reachability_timeout_days], 1) + setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) describe "set_reachable/1" do test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs index 1d83c1a1c..d2618025c 100644 --- a/test/web/instances/instances_test.exs +++ b/test/web/instances/instances_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.InstancesTest do use Pleroma.DataCase - clear_config_all([:instance, :federation_reachability_timeout_days], 1) + setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) describe "reachable?/1" do test "returns `true` for host / url with unknown reachability status" do diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs index 9a2d76e0b..1d107d56c 100644 --- a/test/web/masto_fe_controller_test.exs +++ b/test/web/masto_fe_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.MastoFEController do import Pleroma.Factory - clear_config([:instance, :public]) + setup do: clear_config([:instance, :public]) test "put settings", %{conn: conn} do user = insert(:user) diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index cba68859e..43538cb17 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -9,7 +9,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do use Pleroma.Web.ConnCase import Pleroma.Factory - clear_config([:instance, :max_account_fields]) + + setup do: clear_config([:instance, :max_account_fields]) describe "updating credentials" do setup do: oauth_access(["write:accounts"]) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 59ad0a596..a9fa0ce48 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -16,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do import Pleroma.Factory describe "account fetching" do - clear_config([:instance, :limit_to_local_content]) + setup do: clear_config([:instance, :limit_to_local_content]) test "works by id" do user = insert(:user) @@ -150,13 +150,9 @@ defp local_and_remote_users do describe "user fetching with restrict unauthenticated profiles for local and remote" do setup do: local_and_remote_users() - clear_config([:restrict_unauthenticated, :profiles, :local]) do - Config.put([:restrict_unauthenticated, :profiles, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - clear_config([:restrict_unauthenticated, :profiles, :remote]) do - Config.put([:restrict_unauthenticated, :profiles, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}") @@ -186,9 +182,7 @@ test "if user is authenticated", %{local: local, remote: remote} do describe "user fetching with restrict unauthenticated profiles for local" do setup do: local_and_remote_users() - clear_config([:restrict_unauthenticated, :profiles, :local]) do - Config.put([:restrict_unauthenticated, :profiles, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}") @@ -215,9 +209,7 @@ test "if user is authenticated", %{local: local, remote: remote} do describe "user fetching with restrict unauthenticated profiles for remote" do setup do: local_and_remote_users() - clear_config([:restrict_unauthenticated, :profiles, :remote]) do - Config.put([:restrict_unauthenticated, :profiles, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}") @@ -405,13 +397,9 @@ defp local_and_remote_activities(%{local: local, remote: remote}) do setup do: local_and_remote_users() setup :local_and_remote_activities - clear_config([:restrict_unauthenticated, :profiles, :local]) do - Config.put([:restrict_unauthenticated, :profiles, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - clear_config([:restrict_unauthenticated, :profiles, :remote]) do - Config.put([:restrict_unauthenticated, :profiles, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") @@ -442,9 +430,7 @@ test "if user is authenticated", %{local: local, remote: remote} do setup do: local_and_remote_users() setup :local_and_remote_activities - clear_config([:restrict_unauthenticated, :profiles, :local]) do - Config.put([:restrict_unauthenticated, :profiles, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") @@ -472,9 +458,7 @@ test "if user is authenticated", %{local: local, remote: remote} do setup do: local_and_remote_users() setup :local_and_remote_activities - clear_config([:restrict_unauthenticated, :profiles, :remote]) do - Config.put([:restrict_unauthenticated, :profiles, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") @@ -806,7 +790,7 @@ test "blocking / unblocking a user" do [valid_params: valid_params] end - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) test "Account registration via Application", %{conn: conn} do conn = @@ -904,7 +888,7 @@ test "returns bad_request if missing required params", %{ end) end - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) test "returns bad_request if missing email params when :account_activation_required is enabled", %{conn: conn, valid_params: valid_params} do @@ -961,7 +945,7 @@ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_ end describe "create account by app / rate limit" do - clear_config([:rate_limit, :app_account_creation], {10_000, 2}) + setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) test "respects rate limit setting", %{conn: conn} do app_token = insert(:oauth_token, user: nil) diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs index 203fa73b0..6ac4cf63b 100644 --- a/test/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/web/mastodon_api/controllers/media_controller_test.exs @@ -22,8 +22,8 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do [image: image] end - clear_config([:media_proxy]) - clear_config([Pleroma.Upload]) + setup do: clear_config([:media_proxy]) + setup do: clear_config([Pleroma.Upload]) test "returns uploaded image", %{conn: conn, image: image} do desc = "Description of the image" diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs index 3cd08c189..f86274d57 100644 --- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs +++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do import Pleroma.Factory import Ecto.Query - clear_config([ScheduledActivity, :enabled]) + setup do: clear_config([ScheduledActivity, :enabled]) test "shows scheduled activities" do %{user: user, conn: conn} = oauth_access(["read:statuses"]) diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index beb547780..d59974d50 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -19,9 +19,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do import Pleroma.Factory - clear_config([:instance, :federating]) - clear_config([:instance, :allow_relay]) - clear_config([:rich_media, :enabled]) + setup do: clear_config([:instance, :federating]) + setup do: clear_config([:instance, :allow_relay]) + setup do: clear_config([:rich_media, :enabled]) describe "posting statuses" do setup do: oauth_access(["write:statuses"]) @@ -485,13 +485,9 @@ defp local_and_remote_activities do describe "status with restrict unauthenticated activities for local and remote" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :activities, :local]) do - Config.put([:restrict_unauthenticated, :activities, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - clear_config([:restrict_unauthenticated, :activities, :remote]) do - Config.put([:restrict_unauthenticated, :activities, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/statuses/#{local.id}") @@ -520,9 +516,7 @@ test "if user is authenticated", %{local: local, remote: remote} do describe "status with restrict unauthenticated activities for local" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :activities, :local]) do - Config.put([:restrict_unauthenticated, :activities, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/statuses/#{local.id}") @@ -548,9 +542,7 @@ test "if user is authenticated", %{local: local, remote: remote} do describe "status with restrict unauthenticated activities for remote" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :activities, :remote]) do - Config.put([:restrict_unauthenticated, :activities, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/statuses/#{local.id}") @@ -614,13 +606,9 @@ test "get statuses by IDs" do describe "getting statuses by ids with restricted unauthenticated for local and remote" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :activities, :local]) do - Config.put([:restrict_unauthenticated, :activities, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - clear_config([:restrict_unauthenticated, :activities, :remote]) do - Config.put([:restrict_unauthenticated, :activities, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) @@ -640,9 +628,7 @@ test "if user is authenticated", %{local: local, remote: remote} do describe "getting statuses by ids with restricted unauthenticated for local" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :activities, :local]) do - Config.put([:restrict_unauthenticated, :activities, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) @@ -663,9 +649,7 @@ test "if user is authenticated", %{local: local, remote: remote} do describe "getting statuses by ids with restricted unauthenticated for remote" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :activities, :remote]) do - Config.put([:restrict_unauthenticated, :activities, :remote], true) - end + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]}) @@ -908,7 +892,7 @@ test "returns 404 error for a wrong id", %{conn: conn} do %{activity: activity} end - clear_config([:instance, :max_pinned_statuses], 1) + setup do: clear_config([:instance, :max_pinned_statuses], 1) test "pin status", %{conn: conn, user: user, activity: activity} do id_str = to_string(activity.id) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index a15c759d4..6fedb4223 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -100,13 +100,9 @@ defp local_and_remote_activities do describe "public with restrict unauthenticated timeline for local and federated timelines" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :timelines, :local]) do - Config.put([:restrict_unauthenticated, :timelines, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - clear_config([:restrict_unauthenticated, :timelines, :federated]) do - Config.put([:restrict_unauthenticated, :timelines, :federated], true) - end + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) test "if user is unauthenticated", %{conn: conn} do res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) @@ -136,9 +132,7 @@ test "if user is authenticated" do describe "public with restrict unauthenticated timeline for local" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :timelines, :local]) do - Config.put([:restrict_unauthenticated, :timelines, :local], true) - end + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) test "if user is unauthenticated", %{conn: conn} do res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) @@ -165,9 +159,7 @@ test "if user is authenticated", %{conn: _conn} do describe "public with restrict unauthenticated timeline for remote" do setup do: local_and_remote_activities() - clear_config([:restrict_unauthenticated, :timelines, :federated]) do - Config.put([:restrict_unauthenticated, :timelines, :federated], true) - end + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) test "if user is unauthenticated", %{conn: conn} do res_conn = get(conn, "/api/v1/timelines/public", %{"local" => "true"}) diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index 7ac7e4af1..da79d38a5 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -7,8 +7,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do import Mock alias Pleroma.Config - clear_config(:media_proxy) - clear_config([Pleroma.Web.Endpoint, :secret_key_base]) + setup do: clear_config(:media_proxy) + setup do: clear_config([Pleroma.Web.Endpoint, :secret_key_base]) test "it returns 404 when MediaProxy disabled", %{conn: conn} do Config.put([:media_proxy, :enabled], false) diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index 8f5fcf2eb..69c2d5dae 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -8,8 +8,8 @@ defmodule Pleroma.Web.MediaProxyTest do import Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy.MediaProxyController - clear_config([:media_proxy, :enabled]) - clear_config(Pleroma.Upload) + setup do: clear_config([:media_proxy, :enabled]) + setup do: clear_config(Pleroma.Upload) describe "when enabled" do setup do diff --git a/test/web/metadata/opengraph_test.exs b/test/web/metadata/opengraph_test.exs index 9d7c009eb..218540e6c 100644 --- a/test/web/metadata/opengraph_test.exs +++ b/test/web/metadata/opengraph_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do import Pleroma.Factory alias Pleroma.Web.Metadata.Providers.OpenGraph - clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) test "it renders all supported types of attachments and skips unknown types" do user = insert(:user) diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs index 3d75d1ed5..9e9c6853a 100644 --- a/test/web/metadata/twitter_card_test.exs +++ b/test/web/metadata/twitter_card_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do alias Pleroma.Web.Metadata.Utils alias Pleroma.Web.Router - clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) test "it renders twitter card for user info" do user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index ee10ad5db..43f322606 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -7,8 +7,8 @@ defmodule Pleroma.Web.NodeInfoTest do import Pleroma.Factory - clear_config([:mrf_simple]) - clear_config(:instance) + setup do: clear_config([:mrf_simple]) + setup do: clear_config(:instance) test "GET /.well-known/nodeinfo", %{conn: conn} do links = @@ -105,7 +105,7 @@ test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do end describe "`metadata/federation/enabled`" do - clear_config([:instance, :federating]) + setup do: clear_config([:instance, :federating]) test "it shows if federation is enabled/disabled", %{conn: conn} do Pleroma.Config.put([:instance, :federating], true) diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs index b348281c5..a8fe8a841 100644 --- a/test/web/oauth/ldap_authorization_test.exs +++ b/test/web/oauth/ldap_authorization_test.exs @@ -12,9 +12,9 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do @skip if !Code.ensure_loaded?(:eldap), do: :skip - clear_config_all([:ldap, :enabled], true) + setup_all do: clear_config([:ldap, :enabled], true) - clear_config_all(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) + setup_all do: clear_config(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) @tag @skip test "authorizes the existing user using LDAP credentials" do diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 592612ddf..0b0972b17 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -17,8 +17,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do key: "_test", signing_salt: "cooldude" ] - - clear_config([:instance, :account_activation_required]) + setup do: clear_config([:instance, :account_activation_required]) describe "in OAuth consumer mode, " do setup do @@ -31,7 +30,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do ] end - clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) + setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ app: app, @@ -939,7 +938,7 @@ test "rejects an invalid authorization code" do end describe "POST /oauth/token - refresh token" do - clear_config([:oauth2, :issue_new_refresh_token]) + setup do: clear_config([:oauth2, :issue_new_refresh_token]) test "issues a new access token with keep fresh token" do Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true) diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 6a3dcf2cd..6787b414b 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -17,7 +17,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do :ok end - clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :federating], true) # Note: see ActivityPubControllerTest for JSON format tests describe "GET /objects/:uuid (text/html)" do diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs index bc359707d..2aa87ac30 100644 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -27,7 +27,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do [user: user] end - clear_config([:instance, :account_activation_required], true) + setup do: clear_config([:instance, :account_activation_required], true) test "resend account confirmation email", %{conn: conn, user: user} do conn diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs index 146f3f4fe..435fb6592 100644 --- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -12,8 +12,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do Pleroma.Config.get!([:instance, :static_dir]), "emoji" ) - - clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) test "shared & non-shared pack information in list_packs is ok" do conn = build_conn() diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs index 13edc4359..2f8aadadc 100644 --- a/test/web/plugs/federating_plug_test.exs +++ b/test/web/plugs/federating_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.FederatingPlugTest do use Pleroma.Web.ConnCase - clear_config([:instance, :federating]) + setup do: clear_config([:instance, :federating]) test "returns and halt the conn when federating is disabled" do Pleroma.Config.put([:instance, :federating], false) diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs index 8237802a7..aa0c5c830 100644 --- a/test/web/rich_media/helpers_test.exs +++ b/test/web/rich_media/helpers_test.exs @@ -19,7 +19,7 @@ defmodule Pleroma.Web.RichMedia.HelpersTest do :ok end - clear_config([:rich_media, :enabled]) + setup do: clear_config([:rich_media, :enabled]) test "refuses to crawl incomplete URLs" do user = insert(:user) diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index aabbedb17..430683ea0 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -8,9 +8,8 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do import Pleroma.Factory - clear_config_all([:static_fe, :enabled], true) - - clear_config([:instance, :federating], true) + setup_all do: clear_config([:static_fe, :enabled], true) + setup do: clear_config([:instance, :federating], true) setup %{conn: conn} do conn = put_req_header(conn, "accept", "text/html") diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 339f99bbf..a5d6e8ecf 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -19,8 +19,7 @@ defmodule Pleroma.Web.StreamerTest do @streamer_timeout 150 @streamer_start_wait 10 - - clear_config([:instance, :skip_thread_containment]) + setup do: clear_config([:instance, :skip_thread_containment]) describe "user streams" do setup do diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs index 5c6087527..5ff8694a8 100644 --- a/test/web/twitter_api/remote_follow_controller_test.exs +++ b/test/web/twitter_api/remote_follow_controller_test.exs @@ -17,11 +17,10 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do :ok end - clear_config_all([:instance, :federating], true) - - clear_config([:instance]) - clear_config([:frontend_configurations, :pleroma_fe]) - clear_config([:user, :deny_follow_blocked]) + setup_all do: clear_config([:instance, :federating], true) + setup do: clear_config([:instance]) + setup do: clear_config([:frontend_configurations, :pleroma_fe]) + setup do: clear_config([:user, :deny_follow_blocked]) describe "GET /ostatus_subscribe - remote_follow/2" do test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index 0e787715a..92f9aa0f5 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -117,7 +117,7 @@ test "it registers a new user and parses mentions in the bio" do end describe "register with one time token" do - clear_config([:instance, :registrations_open], false) + setup do: clear_config([:instance, :registrations_open], false) test "returns user on success" do {:ok, invite} = UserInviteToken.create_invite() @@ -182,7 +182,7 @@ test "returns error on expired token" do end describe "registers with date limited token" do - clear_config([:instance, :registrations_open], false) + setup do: clear_config([:instance, :registrations_open], false) setup do data = %{ @@ -242,7 +242,7 @@ test "returns an error on overdue date", %{data: data} do end describe "registers with reusable token" do - clear_config([:instance, :registrations_open], false) + setup do: clear_config([:instance, :registrations_open], false) test "returns user on success, after him registration fails" do {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) @@ -286,7 +286,7 @@ test "returns user on success, after him registration fails" do end describe "registers with reusable date limited token" do - clear_config([:instance, :registrations_open], false) + setup do: clear_config([:instance, :registrations_open], false) test "returns user on success" do {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 71ecd1aa7..30e54bebd 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -18,8 +18,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do :ok end - clear_config([:instance]) - clear_config([:frontend_configurations, :pleroma_fe]) + setup do: clear_config([:instance]) + setup do: clear_config([:frontend_configurations, :pleroma_fe]) describe "POST /api/pleroma/follow_import" do setup do: oauth_access(["follow"]) @@ -318,7 +318,7 @@ test "returns json with custom emoji with tags", %{conn: conn} do end describe "GET /api/pleroma/healthcheck" do - clear_config([:instance, :healthcheck]) + setup do: clear_config([:instance, :healthcheck]) test "returns 503 when healthcheck disabled", %{conn: conn} do Config.put([:instance, :healthcheck], false) @@ -427,7 +427,7 @@ test "it returns version in json format", %{conn: conn} do end describe "POST /main/ostatus - remote_subscribe/2" do - clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :federating], true) test "renders subscribe form", %{conn: conn} do user = insert(:user) diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs index fcf14dc1e..0023f1e81 100644 --- a/test/web/web_finger/web_finger_controller_test.exs +++ b/test/web/web_finger/web_finger_controller_test.exs @@ -14,7 +14,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do :ok end - clear_config_all([:instance, :federating], true) + setup_all do: clear_config([:instance, :federating], true) test "GET host-meta" do response = diff --git a/test/workers/cron/clear_oauth_token_worker_test.exs b/test/workers/cron/clear_oauth_token_worker_test.exs index f056b1a3e..df82dc75d 100644 --- a/test/workers/cron/clear_oauth_token_worker_test.exs +++ b/test/workers/cron/clear_oauth_token_worker_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Workers.Cron.ClearOauthTokenWorkerTest do import Pleroma.Factory alias Pleroma.Workers.Cron.ClearOauthTokenWorker - clear_config([:oauth2, :clean_expired_tokens]) + setup do: clear_config([:oauth2, :clean_expired_tokens]) test "deletes expired tokens" do insert(:oauth_token, diff --git a/test/workers/cron/digest_emails_worker_test.exs b/test/workers/cron/digest_emails_worker_test.exs index 5d65b9fef..0a63bf4e0 100644 --- a/test/workers/cron/digest_emails_worker_test.exs +++ b/test/workers/cron/digest_emails_worker_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI - clear_config([:email_notifications, :digest]) + setup do: clear_config([:email_notifications, :digest]) setup do Pleroma.Config.put([:email_notifications, :digest], %{ diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs index 56c5aa409..5864f9e5f 100644 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ b/test/workers/cron/purge_expired_activities_worker_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do import Pleroma.Factory import ExUnit.CaptureLog - clear_config([ActivityExpiration, :enabled]) + setup do: clear_config([ActivityExpiration, :enabled]) test "deletes an expiration activity" do Pleroma.Config.put([ActivityExpiration, :enabled], true) diff --git a/test/workers/scheduled_activity_worker_test.exs b/test/workers/scheduled_activity_worker_test.exs index ab9f9c125..b312d975b 100644 --- a/test/workers/scheduled_activity_worker_test.exs +++ b/test/workers/scheduled_activity_worker_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Workers.ScheduledActivityWorkerTest do import Pleroma.Factory import ExUnit.CaptureLog - clear_config([ScheduledActivity, :enabled]) + setup do: clear_config([ScheduledActivity, :enabled]) test "creates a status from the scheduled activity" do Pleroma.Config.put([ScheduledActivity, :enabled], true) From 981e015f1b68c7cf807b0ddbf3948809f11b7fff Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 22 Mar 2020 17:10:37 +0300 Subject: [PATCH 63/79] Mastodon API Account view: Remove an outdated hack The hack with caching the follow relationship was introduced when we still were storing it inside the follow activity, resulting in slow queries. Now we store follow state in `FollowRelationship` table, so this is no longer necessary. --- lib/pleroma/user.ex | 18 ------------------ lib/pleroma/web/activity_pub/activity_pub.ex | 3 +-- lib/pleroma/web/activity_pub/utils.ex | 5 +---- .../web/mastodon_api/views/account_view.ex | 13 +++---------- 4 files changed, 5 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 8693c0b80..12c2ad815 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -292,24 +292,6 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa def ap_following(%User{} = user), do: "#{ap_id(user)}/following" - def follow_state(%User{} = user, %User{} = target) do - case Utils.fetch_latest_follow(user, target) do - %{data: %{"state" => state}} -> state - # Ideally this would be nil, but then Cachex does not commit the value - _ -> false - end - end - - def get_cached_follow_state(user, target) do - key = "follow_state:#{user.ap_id}|#{target.ap_id}" - Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end) - end - - @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()} - def set_follow_state_cache(user_ap_id, target_ap_id, state) do - Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state) - end - @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() def restrict_deactivated(query) do from(u in query, where: u.deactivated != ^true) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index d9f74b6a4..30e282840 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -503,8 +503,7 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do defp do_follow(follower, followed, activity_id, local) do with data <- make_follow_data(follower, followed, activity_id), {:ok, activity} <- insert(data, local), - :ok <- maybe_federate(activity), - _ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do + :ok <- maybe_federate(activity) do {:ok, activity} else {:error, error} -> Repo.rollback(error) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 15dd2ed45..c65bbed67 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -440,22 +440,19 @@ def update_follow_state_for_all( |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)]) |> Repo.update_all([]) - User.set_follow_state_cache(actor, object, state) - activity = Activity.get_by_id(activity.id) {:ok, activity} end def update_follow_state( - %Activity{data: %{"actor" => actor, "object" => object}} = activity, + %Activity{} = activity, state ) do new_data = Map.put(activity.data, "state", state) changeset = Changeset.change(activity, data: new_data) with {:ok, activity} <- Repo.update(changeset) do - User.set_follow_state_cache(actor, object, state) {:ok, activity} end end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 341dc2c91..4ebce73b4 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -36,25 +36,18 @@ def render("relationship.json", %{user: nil, target: _target}) do end def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do - follow_state = User.get_cached_follow_state(user, target) - - requested = - if follow_state && !User.following?(user, target) do - follow_state == "pending" - else - false - end + follow_state = User.get_follow_state(user, target) %{ id: to_string(target.id), - following: User.following?(user, target), + following: follow_state == "accept", followed_by: User.following?(target, user), blocking: User.blocks_user?(user, target), blocked_by: User.blocks_user?(target, user), muting: User.mutes?(user, target), muting_notifications: User.muted_notifications?(user, target), subscribing: User.subscribed_to?(user, target), - requested: requested, + requested: follow_state == "pending", domain_blocking: User.blocks_domain?(user, target), showing_reblogs: User.showing_reblogs?(user, target), endorsed: false From 15be6ba9c200b2a4ae153d26876be1b5cbb6357e Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 22 Mar 2020 16:38:12 +0100 Subject: [PATCH 64/79] AccountView: fix for other forms of
    in bio Closes: https://git.pleroma.social/pleroma/pleroma/issues/1643 --- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- .../account_controller/update_credentials_test.exs | 4 ++-- test/web/mastodon_api/views/account_view_test.exs | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 4ebce73b4..2bf711386 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -115,7 +115,7 @@ defp do_render("show.json", %{user: user} = opts) do fields: user.fields, bot: bot, source: %{ - note: Pleroma.HTML.strip_tags((user.bio || "") |> String.replace("
    ", "\n")), + note: (user.bio || "") |> String.replace(~r(
    ), "\n") |> Pleroma.HTML.strip_tags(), sensitive: false, fields: user.raw_fields, pleroma: %{ diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 43538cb17..51cebe567 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -76,7 +76,7 @@ test "updates the user's bio", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{ - "note" => "I drink #cofe with @#{user2.nickname}" + "note" => "I drink #cofe with @#{user2.nickname}\n\nsuya.." }) assert user_data = json_response(conn, 200) @@ -84,7 +84,7 @@ test "updates the user's bio", %{conn: conn} do assert user_data["note"] == ~s(I drink #cofe with @#{user2.nickname}) + }" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@#{user2.nickname}

    suya..) end test "updates the user's locking status", %{conn: conn} do diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index d60ed7b64..983886c6b 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -32,7 +32,8 @@ test "Represent a user account" do background: background_image, nickname: "shp@shitposter.club", name: ":karjalanpiirakka: shp", - bio: "valid html", + bio: + "valid html. a
    b
    c
    d
    f", inserted_at: ~N[2017-08-15 15:47:06.597036] }) @@ -46,7 +47,7 @@ test "Represent a user account" do followers_count: 3, following_count: 0, statuses_count: 5, - note: "valid html", + note: "valid html. a
    b
    c
    d
    f", url: user.ap_id, avatar: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png", @@ -63,7 +64,7 @@ test "Represent a user account" do fields: [], bot: false, source: %{ - note: "valid html", + note: "valid html. a\nb\nc\nd\nf", sensitive: false, pleroma: %{ actor_type: "Person", From c2e415143b1dfe5d89eff06fbce6840c445aa5fa Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 22 Mar 2020 21:51:44 +0300 Subject: [PATCH 65/79] WIP: preloading of user relations for timeline/statuses rendering (performance improvement). --- lib/pleroma/user.ex | 6 +- lib/pleroma/user_relationship.ex | 44 ++++++++++++ .../web/mastodon_api/views/account_view.ex | 69 ++++++++++++++++--- .../web/mastodon_api/views/status_view.ex | 60 ++++++++++++++-- 4 files changed, 160 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 12c2ad815..daaa6d86b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1642,8 +1642,12 @@ def all_superusers do |> Repo.all() end + def muting_reblogs?(%User{} = user, %User{} = target) do + UserRelationship.reblog_mute_exists?(user, target) + end + def showing_reblogs?(%User{} = user, %User{} = target) do - not UserRelationship.reblog_mute_exists?(user, target) + not muting_reblogs?(user, target) end @doc """ diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 393947942..167a3919c 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do import Ecto.Changeset import Ecto.Query + alias FlakeId.Ecto.CompatType alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserRelationship @@ -34,6 +35,10 @@ def unquote(:"#{relationship_type}_exists?")(source, target), do: exists?(unquote(relationship_type), source, target) end + def user_relationship_types, do: Keyword.keys(user_relationship_mappings()) + + def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__() + def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do user_relationship |> cast(params, [:relationship_type, :source_id, :target_id]) @@ -72,6 +77,45 @@ def delete(relationship_type, %User{} = source, %User{} = target) do end end + def dictionary( + source_users, + target_users, + source_to_target_rel_types \\ nil, + target_to_source_rel_types \\ nil + ) + when is_list(source_users) and is_list(target_users) do + get_bin_ids = fn user -> + with {:ok, bin_id} <- CompatType.dump(user.id), do: bin_id + end + + source_user_ids = Enum.map(source_users, &get_bin_ids.(&1)) + target_user_ids = Enum.map(target_users, &get_bin_ids.(&1)) + + get_rel_type_codes = fn rel_type -> user_relationship_mappings()[rel_type] end + + source_to_target_rel_types = + Enum.map(source_to_target_rel_types || user_relationship_types(), &get_rel_type_codes.(&1)) + + target_to_source_rel_types = + Enum.map(target_to_source_rel_types || user_relationship_types(), &get_rel_type_codes.(&1)) + + __MODULE__ + |> where( + fragment( + "(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?)) OR \ + (source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?))", + ^source_user_ids, + ^target_user_ids, + ^source_to_target_rel_types, + ^target_user_ids, + ^source_user_ids, + ^target_to_source_rel_types + ) + ) + |> select([ur], [ur.relationship_type, ur.source_id, ur.target_id]) + |> Repo.all() + end + defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do changeset |> validate_change(:target_id, fn _, target_id -> diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 4ebce73b4..15a579278 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -10,6 +10,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MediaProxy + def test_rel(user_relationships, rel_type, source, target, func) do + cond do + is_nil(source) or is_nil(target) -> + false + + user_relationships -> + [rel_type, source.id, target.id] in user_relationships + + true -> + func.(source, target) + end + end + def render("index.json", %{users: users} = opts) do users |> render_many(AccountView, "show.json", opts) @@ -35,21 +48,50 @@ def render("relationship.json", %{user: nil, target: _target}) do %{} end - def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do - follow_state = User.get_follow_state(user, target) + def render( + "relationship.json", + %{user: %User{} = reading_user, target: %User{} = target} = opts + ) do + user_relationships = Map.get(opts, :user_relationships) + follow_state = User.get_follow_state(reading_user, target) + + # TODO: add a note on adjusting StatusView.user_relationships_opt/1 re: preloading of user relations %{ id: to_string(target.id), following: follow_state == "accept", - followed_by: User.following?(target, user), - blocking: User.blocks_user?(user, target), - blocked_by: User.blocks_user?(target, user), - muting: User.mutes?(user, target), - muting_notifications: User.muted_notifications?(user, target), - subscribing: User.subscribed_to?(user, target), + followed_by: User.following?(target, reading_user), + blocking: + test_rel(user_relationships, :block, reading_user, target, &User.blocks_user?(&1, &2)), + blocked_by: + test_rel(user_relationships, :block, target, reading_user, &User.blocks_user?(&1, &2)), + muting: test_rel(user_relationships, :mute, reading_user, target, &User.mutes?(&1, &2)), + muting_notifications: + test_rel( + user_relationships, + :notification_mute, + reading_user, + target, + &User.muted_notifications?(&1, &2) + ), + subscribing: + test_rel( + user_relationships, + :inverse_subscription, + target, + reading_user, + &User.subscribed_to?(&2, &1) + ), requested: follow_state == "pending", - domain_blocking: User.blocks_domain?(user, target), - showing_reblogs: User.showing_reblogs?(user, target), + domain_blocking: User.blocks_domain?(reading_user, target), + showing_reblogs: + not test_rel( + user_relationships, + :reblog_mute, + reading_user, + target, + &User.muting_reblogs?(&1, &2) + ), endorsed: false } end @@ -93,7 +135,12 @@ defp do_render("show.json", %{user: user} = opts) do } end) - relationship = render("relationship.json", %{user: opts[:for], target: user}) + relationship = + render("relationship.json", %{ + user: opts[:for], + target: user, + user_relationships: opts[:user_relationships] + }) %{ id: to_string(user.id), diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index f7469cdff..e0c368ec9 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView @@ -70,11 +71,34 @@ defp reblogged?(activity, user) do present?(user && user.ap_id in (object.data["announcements"] || [])) end - def render("index.json", opts) do - replied_to_activities = get_replied_to_activities(opts.activities) - opts = Map.put(opts, :replied_to_activities, replied_to_activities) + defp user_relationships_opt(opts) do + reading_user = opts[:for] - safe_render_many(opts.activities, StatusView, "show.json", opts) + if reading_user do + activities = opts[:activities] + actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end) + + UserRelationship.dictionary( + [reading_user], + actors, + [:block, :mute, :notification_mute, :reblog_mute], + [:block, :inverse_subscription] + ) + else + [] + end + end + + def render("index.json", opts) do + activities = opts.activities + replied_to_activities = get_replied_to_activities(activities) + + opts = + opts + |> Map.put(:replied_to_activities, replied_to_activities) + |> Map.put(:user_relationships, user_relationships_opt(opts)) + + safe_render_many(activities, StatusView, "show.json", opts) end def render( @@ -107,7 +131,12 @@ def render( id: to_string(activity.id), uri: activity_object.data["id"], url: activity_object.data["id"], - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + account: + AccountView.render("show.json", %{ + user: user, + for: opts[:for], + user_relationships: opts[:user_relationships] + }), in_reply_to_id: nil, in_reply_to_account_id: nil, reblog: reblogged, @@ -253,11 +282,28 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} _ -> [] end + user_relationships_opt = opts[:user_relationships] + + muted = + thread_muted? || + Pleroma.Web.MastodonAPI.AccountView.test_rel( + user_relationships_opt, + :mute, + opts[:for], + user, + fn for_user, user -> User.mutes?(for_user, user) end + ) + %{ id: to_string(activity.id), uri: object.data["id"], url: url, - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + account: + AccountView.render("show.json", %{ + user: user, + for: opts[:for], + user_relationships: user_relationships_opt + }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), reblog: nil, @@ -270,7 +316,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} reblogged: reblogged?(activity, opts[:for]), favourited: present?(favorited), bookmarked: present?(bookmarked), - muted: thread_muted? || User.mutes?(opts[:for], user), + muted: muted, pinned: pinned?(activity, user), sensitive: sensitive, spoiler_text: summary, From 3c78e5f3275494b3dc4546e65f19eb3a3c97033a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 23 Mar 2020 12:01:11 +0300 Subject: [PATCH 66/79] Preloading of follow relations for timeline/statuses rendering (performance improvement). Refactoring. --- lib/pleroma/following_relationship.ex | 26 +++++++ lib/pleroma/user.ex | 7 ++ lib/pleroma/user_relationship.ex | 13 ++++ .../web/mastodon_api/views/account_view.ex | 75 ++++++++++++++----- .../web/mastodon_api/views/status_view.ex | 46 +++++++----- 5 files changed, 130 insertions(+), 37 deletions(-) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index a6d281151..dd1696136 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -129,4 +129,30 @@ def move_following(origin, target) do move_following(origin, target) end end + + def all_between_user_sets( + source_users, + target_users + ) + when is_list(source_users) and is_list(target_users) do + get_bin_ids = fn user -> + with {:ok, bin_id} <- CompatType.dump(user.id), do: bin_id + end + + source_user_ids = Enum.map(source_users, &get_bin_ids.(&1)) + target_user_ids = Enum.map(target_users, &get_bin_ids.(&1)) + + __MODULE__ + |> where( + fragment( + "(follower_id = ANY(?) AND following_id = ANY(?)) OR \ + (follower_id = ANY(?) AND following_id = ANY(?))", + ^source_user_ids, + ^target_user_ids, + ^target_user_ids, + ^source_user_ids + ) + ) + |> Repo.all() + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index daaa6d86b..eb72755a0 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -674,7 +674,14 @@ def unfollow(%User{} = follower, %User{} = followed) do def get_follow_state(%User{} = follower, %User{} = following) do following_relationship = FollowingRelationship.get(follower, following) + get_follow_state(follower, following, following_relationship) + end + def get_follow_state( + %User{} = follower, + %User{} = following, + following_relationship + ) do case {following_relationship, following.local} do {nil, false} -> case Utils.fetch_latest_follow(follower, following) do diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 167a3919c..9423e3a42 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -116,6 +116,19 @@ def dictionary( |> Repo.all() end + def exists?(dictionary, rel_type, source, target, func) do + cond do + is_nil(source) or is_nil(target) -> + false + + dictionary -> + [rel_type, source.id, target.id] in dictionary + + true -> + func.(source, target) + end + end + defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do changeset |> validate_change(:target_id, fn _, target_id -> diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 15a579278..2fe46158b 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -6,21 +6,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do use Pleroma.Web, :view alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MediaProxy - def test_rel(user_relationships, rel_type, source, target, func) do - cond do - is_nil(source) or is_nil(target) -> - false - - user_relationships -> - [rel_type, source.id, target.id] in user_relationships - - true -> - func.(source, target) - end + defp find_following_rel(following_relationships, follower, following) do + Enum.find(following_relationships, fn + fr -> fr.follower_id == follower.id and fr.following_id == following.id + end) end def render("index.json", %{users: users} = opts) do @@ -53,21 +47,61 @@ def render( %{user: %User{} = reading_user, target: %User{} = target} = opts ) do user_relationships = Map.get(opts, :user_relationships) + following_relationships = opts[:following_relationships] - follow_state = User.get_follow_state(reading_user, target) + follow_state = + if following_relationships do + user_to_target_following_relation = + find_following_rel(following_relationships, reading_user, target) + + User.get_follow_state(reading_user, target, user_to_target_following_relation) + else + User.get_follow_state(reading_user, target) + end + + followed_by = + if following_relationships do + with %{state: "accept"} <- + find_following_rel(following_relationships, target, reading_user) do + true + else + _ -> false + end + else + User.following?(target, reading_user) + end # TODO: add a note on adjusting StatusView.user_relationships_opt/1 re: preloading of user relations %{ id: to_string(target.id), following: follow_state == "accept", - followed_by: User.following?(target, reading_user), + followed_by: followed_by, blocking: - test_rel(user_relationships, :block, reading_user, target, &User.blocks_user?(&1, &2)), + UserRelationship.exists?( + user_relationships, + :block, + reading_user, + target, + &User.blocks_user?(&1, &2) + ), blocked_by: - test_rel(user_relationships, :block, target, reading_user, &User.blocks_user?(&1, &2)), - muting: test_rel(user_relationships, :mute, reading_user, target, &User.mutes?(&1, &2)), + UserRelationship.exists?( + user_relationships, + :block, + target, + reading_user, + &User.blocks_user?(&1, &2) + ), + muting: + UserRelationship.exists?( + user_relationships, + :mute, + reading_user, + target, + &User.mutes?(&1, &2) + ), muting_notifications: - test_rel( + UserRelationship.exists?( user_relationships, :notification_mute, reading_user, @@ -75,7 +109,7 @@ def render( &User.muted_notifications?(&1, &2) ), subscribing: - test_rel( + UserRelationship.exists?( user_relationships, :inverse_subscription, target, @@ -85,7 +119,7 @@ def render( requested: follow_state == "pending", domain_blocking: User.blocks_domain?(reading_user, target), showing_reblogs: - not test_rel( + not UserRelationship.exists?( user_relationships, :reblog_mute, reading_user, @@ -139,7 +173,8 @@ defp do_render("show.json", %{user: user} = opts) do render("relationship.json", %{ user: opts[:for], target: user, - user_relationships: opts[:user_relationships] + user_relationships: opts[:user_relationships], + following_relationships: opts[:following_relationships] }) %{ diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e0c368ec9..55a5513f9 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Activity alias Pleroma.ActivityExpiration + alias Pleroma.FollowingRelationship alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -71,22 +72,31 @@ defp reblogged?(activity, user) do present?(user && user.ap_id in (object.data["announcements"] || [])) end - defp user_relationships_opt(opts) do + defp relationships_opts(opts) do reading_user = opts[:for] - if reading_user do - activities = opts[:activities] - actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end) + {user_relationships, following_relationships} = + if reading_user do + activities = opts[:activities] + actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end) - UserRelationship.dictionary( - [reading_user], - actors, - [:block, :mute, :notification_mute, :reblog_mute], - [:block, :inverse_subscription] - ) - else - [] - end + user_relationships = + UserRelationship.dictionary( + [reading_user], + actors, + [:block, :mute, :notification_mute, :reblog_mute], + [:block, :inverse_subscription] + ) + + following_relationships = + FollowingRelationship.all_between_user_sets([reading_user], actors) + + {user_relationships, following_relationships} + else + {[], []} + end + + %{user_relationships: user_relationships, following_relationships: following_relationships} end def render("index.json", opts) do @@ -96,7 +106,7 @@ def render("index.json", opts) do opts = opts |> Map.put(:replied_to_activities, replied_to_activities) - |> Map.put(:user_relationships, user_relationships_opt(opts)) + |> Map.merge(relationships_opts(opts)) safe_render_many(activities, StatusView, "show.json", opts) end @@ -135,7 +145,8 @@ def render( AccountView.render("show.json", %{ user: user, for: opts[:for], - user_relationships: opts[:user_relationships] + user_relationships: opts[:user_relationships], + following_relationships: opts[:following_relationships] }), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -286,7 +297,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} muted = thread_muted? || - Pleroma.Web.MastodonAPI.AccountView.test_rel( + UserRelationship.exists?( user_relationships_opt, :mute, opts[:for], @@ -302,7 +313,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} AccountView.render("show.json", %{ user: user, for: opts[:for], - user_relationships: user_relationships_opt + user_relationships: user_relationships_opt, + following_relationships: opts[:following_relationships] }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), From 5a34dca8eda46479a3459b60c623d6fa94fc662b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 23 Mar 2020 14:03:31 +0400 Subject: [PATCH 67/79] Add emoji support in statuses in staticfe --- .../web/static_fe/static_fe_controller.ex | 4 +++- priv/static/static/static-fe.css | Bin 2629 -> 2715 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 7f9464268..7a35238d7 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -60,7 +60,9 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do content = if data["content"] do - Pleroma.HTML.filter_tags(data["content"]) + data["content"] + |> Pleroma.HTML.filter_tags() + |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) else nil end diff --git a/priv/static/static/static-fe.css b/priv/static/static/static-fe.css index 19c56387b1ea9aa19f3d0f8d596c949f0b0c6485..db61ff2665ba2aef7f33dbfa8dbf29b7f6cdcba7 100644 GIT binary patch delta 94 zcmX>qGFxQmwgaxd3*-9ZLWJ delta 7 OcmbO&dQ@bCD;EF@j{=DR From eec1fcaf55bdcbc2d3aed4eaf044bb8ef6c4effa Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 23 Mar 2020 15:58:55 +0100 Subject: [PATCH 68/79] Home timeline tests: Add failing test for relationships --- .../controllers/timeline_controller_test.exs | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 6fedb4223..47849fc48 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -21,9 +21,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do setup do: oauth_access(["read:statuses"]) test "the home timeline", %{user: user, conn: conn} do - following = insert(:user) + following = insert(:user, nickname: "followed") + third_user = insert(:user, nickname: "repeated") - {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) + {:ok, _activity} = CommonAPI.post(following, %{"status" => "post"}) + {:ok, activity} = CommonAPI.post(third_user, %{"status" => "repeated post"}) + {:ok, _, _} = CommonAPI.repeat(activity.id, following) ret_conn = get(conn, "/api/v1/timelines/home") @@ -31,9 +34,55 @@ test "the home timeline", %{user: user, conn: conn} do {:ok, _user} = User.follow(user, following) - conn = get(conn, "/api/v1/timelines/home") + ret_conn = get(conn, "/api/v1/timelines/home") - assert [%{"content" => "test"}] = json_response(conn, :ok) + assert [ + %{ + "reblog" => %{ + "content" => "repeated post", + "account" => %{ + "pleroma" => %{ + "relationship" => %{"following" => false, "followed_by" => false} + } + } + }, + "account" => %{"pleroma" => %{"relationship" => %{"following" => true}}} + }, + %{ + "content" => "post", + "account" => %{ + "acct" => "followed", + "pleroma" => %{"relationship" => %{"following" => true}} + } + } + ] = json_response(ret_conn, :ok) + + {:ok, _user} = User.follow(third_user, user) + + ret_conn = get(conn, "/api/v1/timelines/home") + + assert [ + %{ + "reblog" => %{ + "content" => "repeated post", + "account" => %{ + "acct" => "repeated", + "pleroma" => %{ + # This part does not match correctly + "relationship" => %{"following" => false, "followed_by" => true} + } + } + }, + "account" => %{"pleroma" => %{"relationship" => %{"following" => true}}} + }, + %{ + "content" => "post", + "account" => %{ + "acct" => "followed", + "pleroma" => %{"relationship" => %{"following" => true}} + } + } + ] = json_response(ret_conn, :ok) end test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do From 3bd2829e5c125f961b7508bf40ef534a21070562 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 23 Mar 2020 18:56:01 +0100 Subject: [PATCH 69/79] Benchmarks: Add timeline benchmark --- benchmarks/load_testing/generator.ex | 3 +- .../mix/tasks/pleroma/benchmarks/timelines.ex | 76 +++++++++++++++++++ lib/pleroma/web/controller_helper.ex | 7 +- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex index 3f88fefd7..17e89c13c 100644 --- a/benchmarks/load_testing/generator.ex +++ b/benchmarks/load_testing/generator.ex @@ -22,9 +22,10 @@ def generate_like_activities(user, posts) do def generate_users(opts) do IO.puts("Starting generating #{opts[:users_max]} users...") - {time, _} = :timer.tc(fn -> do_generate_users(opts) end) + {time, users} = :timer.tc(fn -> do_generate_users(opts) end) IO.puts("Inserting users take #{to_sec(time)} sec.\n") + users end defp do_generate_users(opts) do diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex new file mode 100644 index 000000000..dc6f3d3fc --- /dev/null +++ b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex @@ -0,0 +1,76 @@ +defmodule Mix.Tasks.Pleroma.Benchmarks.Timelines do + use Mix.Task + alias Pleroma.Repo + alias Pleroma.LoadTesting.Generator + + alias Pleroma.Web.CommonAPI + + def run(_args) do + Mix.Pleroma.start_pleroma() + + # Cleaning tables + clean_tables() + + [{:ok, user} | users] = Generator.generate_users(users_max: 1000) + + # Let the user make 100 posts + + 1..100 + |> Enum.each(fn i -> CommonAPI.post(user, %{"status" => to_string(i)}) end) + + # Let 10 random users post + posts = + users + |> Enum.take_random(10) + |> Enum.map(fn {:ok, random_user} -> + {:ok, activity} = CommonAPI.post(random_user, %{"status" => "."}) + activity + end) + + # let our user repeat them + posts + |> Enum.each(fn activity -> + CommonAPI.repeat(activity.id, user) + end) + + Benchee.run( + %{ + "user timeline, no followers" => fn reading_user -> + conn = + Phoenix.ConnTest.build_conn() + |> Plug.Conn.assign(:user, reading_user) + |> Plug.Conn.assign(:skip_link_headers, true) + + Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id}) + end + }, + inputs: %{"user" => user, "no user" => nil}, + time: 60 + ) + + users + |> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end) + + Benchee.run( + %{ + "user timeline, all following" => fn reading_user -> + conn = + Phoenix.ConnTest.build_conn() + |> Plug.Conn.assign(:user, reading_user) + |> Plug.Conn.assign(:skip_link_headers, true) + + Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id}) + end + }, + inputs: %{"user" => user, "no user" => nil}, + time: 60 + ) + end + + defp clean_tables do + IO.puts("Deleting old data...\n") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") + end +end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index ad293cda9..b49523ec3 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -34,7 +34,12 @@ defp param_to_integer(val, default) when is_binary(val) do defp param_to_integer(_, default), do: default - def add_link_headers(conn, activities, extra_params \\ %{}) do + def add_link_headers(conn, activities, extra_params \\ %{}) + + def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params), + do: conn + + def add_link_headers(conn, activities, extra_params) do case List.last(activities) do %{id: max_id} -> params = From d1a9716a988fe9f670033ad46cc9637038fbd1e8 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 24 Mar 2020 17:38:18 +0400 Subject: [PATCH 70/79] Fix activity deletion --- lib/pleroma/web/activity_pub/activity_pub.ex | 10 ++++++++++ test/web/activity_pub/activity_pub_test.exs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 30e282840..974231925 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -583,6 +583,16 @@ defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) end end + defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do + activity = + ap_id + |> Activity.Queries.by_object_id() + |> Activity.Queries.by_type("Delete") + |> Repo.one() + + {:ok, activity} + end + @spec block(User.t(), User.t(), String.t() | nil, boolean()) :: {:ok, Activity.t()} | {:error, any()} def block(blocker, blocked, activity_id \\ nil, local \\ true) do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index a43dd34f0..049b14498 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1425,6 +1425,12 @@ test "it creates a delete activity and deletes the original object" do assert Repo.get(Object, object.id).data["type"] == "Tombstone" end + test "it doesn't fail when an activity was already deleted" do + {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete() + + assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete() + end + test "decrements user note count only for public activities" do user = insert(:user, note_count: 10) From 13cbb9f6ada8dcb15bb7ed12be4d88a18c5db7f7 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 24 Mar 2020 22:14:26 +0300 Subject: [PATCH 71/79] Implemented preloading of relationships with parent activities' actors for statuses/timeline rendering. Applied preloading for notifications rendering. Fixed announces rendering issue (preloading-related). --- lib/pleroma/activity/queries.ex | 7 ++ .../web/mastodon_api/views/account_view.ex | 15 ++- .../mastodon_api/views/notification_view.ex | 98 +++++++++++++++---- .../web/mastodon_api/views/status_view.ex | 85 +++++++++------- .../controllers/timeline_controller_test.exs | 1 - 5 files changed, 138 insertions(+), 68 deletions(-) diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index 04593b9fb..a34c20343 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -35,6 +35,13 @@ def by_author(query \\ Activity, %User{ap_id: ap_id}) do from(a in query, where: a.actor == ^ap_id) end + def find_by_object_ap_id(activities, object_ap_id) do + Enum.find( + activities, + &(object_ap_id in [is_map(&1.data["object"]) && &1.data["object"]["id"], &1.data["object"]]) + ) + end + @spec by_object_id(query, String.t() | [String.t()]) :: query def by_object_id(query \\ Activity, object_id) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 2fe46158b..89bea9957 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -46,8 +46,8 @@ def render( "relationship.json", %{user: %User{} = reading_user, target: %User{} = target} = opts ) do - user_relationships = Map.get(opts, :user_relationships) - following_relationships = opts[:following_relationships] + user_relationships = get_in(opts, [:relationships, :user_relationships]) + following_relationships = get_in(opts, [:relationships, :following_relationships]) follow_state = if following_relationships do @@ -61,17 +61,15 @@ def render( followed_by = if following_relationships do - with %{state: "accept"} <- - find_following_rel(following_relationships, target, reading_user) do - true - else + case find_following_rel(following_relationships, target, reading_user) do + %{state: "accept"} -> true _ -> false end else User.following?(target, reading_user) end - # TODO: add a note on adjusting StatusView.user_relationships_opt/1 re: preloading of user relations + # NOTE: adjust StatusView.relationships_opts/2 if adding new relation-related flags %{ id: to_string(target.id), following: follow_state == "accept", @@ -173,8 +171,7 @@ defp do_render("show.json", %{user: user} = opts) do render("relationship.json", %{ user: opts[:for], target: user, - user_relationships: opts[:user_relationships], - following_relationships: opts[:following_relationships] + relationships: opts[:relationships] }) %{ diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 33145c484..e9c618496 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -13,19 +13,68 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView - def render("index.json", %{notifications: notifications, for: user}) do - safe_render_many(notifications, NotificationView, "show.json", %{for: user}) + def render("index.json", %{notifications: notifications, for: reading_user}) do + activities = Enum.map(notifications, & &1.activity) + + parent_activities = + activities + |> Enum.filter( + &(Activity.mastodon_notification_type(&1) in [ + "favourite", + "reblog", + "pleroma:emoji_reaction" + ]) + ) + |> Enum.map(& &1.data["object"]) + |> Activity.create_by_object_ap_id() + |> Activity.with_preloaded_object(:left) + |> Pleroma.Repo.all() + + move_activities_targets = + activities + |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) + |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) + + actors = + activities + |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end) + |> Enum.filter(& &1) + |> Kernel.++(move_activities_targets) + + opts = %{ + for: reading_user, + parent_activities: parent_activities, + relationships: StatusView.relationships_opts(reading_user, actors) + } + + safe_render_many(notifications, NotificationView, "show.json", opts) end - def render("show.json", %{ - notification: %Notification{activity: activity} = notification, - for: user - }) do + def render( + "show.json", + %{ + notification: %Notification{activity: activity} = notification, + for: reading_user + } = opts + ) do actor = User.get_cached_by_ap_id(activity.data["actor"]) - parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) + + parent_activity_fn = fn -> + if opts[:parent_activities] do + Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"]) + else + Activity.get_create_by_object_ap_id(activity.data["object"]) + end + end + mastodon_type = Activity.mastodon_notification_type(activity) - with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do + with %{id: _} = account <- + AccountView.render("show.json", %{ + user: actor, + for: reading_user, + relationships: opts[:relationships] + }) do response = %{ id: to_string(notification.id), type: mastodon_type, @@ -36,24 +85,28 @@ def render("show.json", %{ } } + relationships_opts = %{relationships: opts[:relationships]} + case mastodon_type do "mention" -> - put_status(response, activity, user) + put_status(response, activity, reading_user, relationships_opts) "favourite" -> - put_status(response, parent_activity, user) + put_status(response, parent_activity_fn.(), reading_user, relationships_opts) "reblog" -> - put_status(response, parent_activity, user) + put_status(response, parent_activity_fn.(), reading_user, relationships_opts) "move" -> - put_target(response, activity, user) + put_target(response, activity, reading_user, relationships_opts) "follow" -> response "pleroma:emoji_reaction" -> - put_status(response, parent_activity, user) |> put_emoji(activity) + response + |> put_status(parent_activity_fn.(), reading_user, relationships_opts) + |> put_emoji(activity) _ -> nil @@ -64,16 +117,21 @@ def render("show.json", %{ end defp put_emoji(response, activity) do - response - |> Map.put(:emoji, activity.data["content"]) + Map.put(response, :emoji, activity.data["content"]) end - defp put_status(response, activity, user) do - Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user})) + defp put_status(response, activity, reading_user, opts) do + status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user}) + status_render = StatusView.render("show.json", status_render_opts) + + Map.put(response, :status, status_render) end - defp put_target(response, activity, user) do - target = User.get_cached_by_ap_id(activity.data["target"]) - Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user})) + defp put_target(response, activity, reading_user, opts) do + target_user = User.get_cached_by_ap_id(activity.data["target"]) + target_render_opts = Map.merge(opts, %{user: target_user, for: reading_user}) + target_render = AccountView.render("show.json", target_render_opts) + + Map.put(response, :target, target_render) end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 55a5513f9..0ef65b352 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -72,41 +72,46 @@ defp reblogged?(activity, user) do present?(user && user.ap_id in (object.data["announcements"] || [])) end - defp relationships_opts(opts) do - reading_user = opts[:for] + def relationships_opts(_reading_user = nil, _actors) do + %{user_relationships: [], following_relationships: []} + end - {user_relationships, following_relationships} = - if reading_user do - activities = opts[:activities] - actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end) + def relationships_opts(reading_user, actors) do + user_relationships = + UserRelationship.dictionary( + [reading_user], + actors, + [:block, :mute, :notification_mute, :reblog_mute], + [:block, :inverse_subscription] + ) - user_relationships = - UserRelationship.dictionary( - [reading_user], - actors, - [:block, :mute, :notification_mute, :reblog_mute], - [:block, :inverse_subscription] - ) - - following_relationships = - FollowingRelationship.all_between_user_sets([reading_user], actors) - - {user_relationships, following_relationships} - else - {[], []} - end + following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) %{user_relationships: user_relationships, following_relationships: following_relationships} end def render("index.json", opts) do - activities = opts.activities + # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list + activities = Enum.filter(opts.activities, & &1) replied_to_activities = get_replied_to_activities(activities) + parent_activities = + activities + |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"])) + |> Enum.map(&Object.normalize(&1).data["id"]) + |> Activity.create_by_object_ap_id() + |> Activity.with_preloaded_object(:left) + |> Activity.with_preloaded_bookmark(opts[:for]) + |> Activity.with_set_thread_muted_field(opts[:for]) + |> Repo.all() + + actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) + opts = opts |> Map.put(:replied_to_activities, replied_to_activities) - |> Map.merge(relationships_opts(opts)) + |> Map.put(:parent_activities, parent_activities) + |> Map.put(:relationships, relationships_opts(opts[:for], actors)) safe_render_many(activities, StatusView, "show.json", opts) end @@ -119,17 +124,25 @@ def render( created_at = Utils.to_masto_date(activity.data["published"]) activity_object = Object.normalize(activity) - reblogged_activity = - Activity.create_by_object_ap_id(activity_object.data["id"]) - |> Activity.with_preloaded_bookmark(opts[:for]) - |> Activity.with_set_thread_muted_field(opts[:for]) - |> Repo.one() + reblogged_parent_activity = + if opts[:parent_activities] do + Activity.Queries.find_by_object_ap_id( + opts[:parent_activities], + activity_object.data["id"] + ) + else + Activity.create_by_object_ap_id(activity_object.data["id"]) + |> Activity.with_preloaded_bookmark(opts[:for]) + |> Activity.with_set_thread_muted_field(opts[:for]) + |> Repo.one() + end - reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity)) + reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity) + reblogged = render("show.json", reblog_rendering_opts) favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) - bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil + bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil mentions = activity.recipients @@ -145,8 +158,7 @@ def render( AccountView.render("show.json", %{ user: user, for: opts[:for], - user_relationships: opts[:user_relationships], - following_relationships: opts[:following_relationships] + relationships: opts[:relationships] }), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -156,7 +168,7 @@ def render( reblogs_count: 0, replies_count: 0, favourites_count: 0, - reblogged: reblogged?(reblogged_activity, opts[:for]), + reblogged: reblogged?(reblogged_parent_activity, opts[:for]), favourited: present?(favorited), bookmarked: present?(bookmarked), muted: false, @@ -293,12 +305,10 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} _ -> [] end - user_relationships_opt = opts[:user_relationships] - muted = thread_muted? || UserRelationship.exists?( - user_relationships_opt, + get_in(opts, [:relationships, :user_relationships]), :mute, opts[:for], user, @@ -313,8 +323,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} AccountView.render("show.json", %{ user: user, for: opts[:for], - user_relationships: user_relationships_opt, - following_relationships: opts[:following_relationships] + relationships: opts[:relationships] }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 47849fc48..97b1c3e66 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -68,7 +68,6 @@ test "the home timeline", %{user: user, conn: conn} do "account" => %{ "acct" => "repeated", "pleroma" => %{ - # This part does not match correctly "relationship" => %{"following" => false, "followed_by" => true} } } From e743c2232970e321c833604b232520587ad8e402 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 25 Mar 2020 09:04:00 +0300 Subject: [PATCH 72/79] Fixed incorrect usage of "relations" as a short form of "relationships". --- config/description.exs | 2 +- lib/pleroma/notification.ex | 6 +++--- lib/pleroma/user.ex | 20 +++++++++---------- lib/pleroma/web/activity_pub/activity_pub.ex | 8 ++++---- .../controllers/account_controller.ex | 10 +++++++--- lib/pleroma/web/streamer/worker.ex | 2 +- test/user_test.exs | 6 +++--- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/config/description.exs b/config/description.exs index 732c76734..68fa8b03b 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2442,7 +2442,7 @@ %{ key: :relations_actions, type: [:tuple, {:list, :tuple}], - description: "For actions on relations with all users (follow, unfollow)", + description: "For actions on relationships with all users (follow, unfollow)", suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]] }, %{ diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 104368fd1..bc691dce3 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -39,11 +39,11 @@ def changeset(%Notification{} = notification, attrs) do end defp for_user_query_ap_id_opts(user, opts) do - ap_id_relations = + ap_id_relationships = [:block] ++ if opts[@include_muted_option], do: [], else: [:notification_mute] - preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations) + preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships) exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) @@ -370,7 +370,7 @@ def exclude_relation_restricting_ap_ids(ap_ids, %Activity{} = activity) do relation_restricted_ap_ids = activity |> Activity.user_actor() - |> User.incoming_relations_ungrouped_ap_ids([ + |> User.incoming_relationships_ungrouped_ap_ids([ :block, :notification_mute ]) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 05efc74d4..4919c8e58 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1222,15 +1222,15 @@ def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do end @doc """ - Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type. - E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` + Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type. + E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` """ - @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} - def outgoing_relations_ap_ids(_user, []), do: %{} + @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} + def outgoing_relationships_ap_ids(_user, []), do: %{} - def outgoing_relations_ap_ids(nil, _relationship_types), do: %{} + def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{} - def outgoing_relations_ap_ids(%User{} = user, relationship_types) + def outgoing_relationships_ap_ids(%User{} = user, relationship_types) when is_list(relationship_types) do db_result = user @@ -1249,13 +1249,13 @@ def outgoing_relations_ap_ids(%User{} = user, relationship_types) ) end - def incoming_relations_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil) + def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil) - def incoming_relations_ungrouped_ap_ids(_user, [], _ap_ids), do: [] + def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: [] - def incoming_relations_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: [] + def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: [] - def incoming_relations_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids) + def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids) when is_list(relationship_types) do user |> assoc(:incoming_relationships) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index d9f74b6a4..60e74758f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1230,17 +1230,17 @@ defp maybe_order(query, _), do: query defp fetch_activities_query_ap_ids_ops(opts) do source_user = opts["muting_user"] - ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: [] + ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: [] - ap_id_relations = - ap_id_relations ++ + ap_id_relationships = + ap_id_relationships ++ if opts["blocking_user"] && opts["blocking_user"] == source_user do [:block] else [] end - preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations) + preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships) restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 88c997b9f..9d83a9fc1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -63,11 +63,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do when action != :create ) - @relations [:follow, :unfollow] + @relationship_actions [:follow, :unfollow] @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a - plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations) - plug(RateLimiter, [name: :relations_actions] when action in @relations) + plug( + RateLimiter, + [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions + ) + + plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions) plug(RateLimiter, [name: :app_account_creation] when action == :create) plug(:assign_account_by_id when action in @needs_account) diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 29f992a67..abfed21c8 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -130,7 +130,7 @@ defp do_stream(%{topic: topic, item: item}) do defp should_send?(%User{} = user, %Activity{} = item) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = - User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute]) + User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) recipients = MapSet.new(item.recipients) diff --git a/test/user_test.exs b/test/user_test.exs index b07fed42b..f3d044a80 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -86,7 +86,7 @@ test "returns invisible actor" do {:ok, user: insert(:user)} end - test "outgoing_relations_ap_ids/1", %{user: user} do + test "outgoing_relationships_ap_ids/1", %{user: user} do rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] ap_ids_by_rel = @@ -124,10 +124,10 @@ test "outgoing_relations_ap_ids/1", %{user: user} do assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) - outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types) + outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types) assert ap_ids_by_rel == - Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) + Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) end end From 3fa3d45dbecafb06fb7eb4f0260f610d4225e0a7 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 25 Mar 2020 13:05:00 +0300 Subject: [PATCH 73/79] [#1364] Minor improvements / comments. Further fixes of incorrect usage of "relations" as a short form of "relationships". --- lib/pleroma/activity.ex | 1 + lib/pleroma/notification.ex | 12 +++++++----- lib/pleroma/thread_mute.ex | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index bbaa561a7..5a8329e69 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -95,6 +95,7 @@ def with_preloaded_object(query, join_type \\ :inner) do |> preload([activity, object: object], object: object) end + # Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.) def user_actor(%Activity{actor: nil}), do: nil def user_actor(%Activity{} = activity) do diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 63e3e9be9..04ee510b9 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -322,6 +322,8 @@ def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) @doc """ Returns a tuple with 2 elements: {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)} + + NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1 """ def get_notified_from_activity(activity, local_only \\ true) @@ -338,7 +340,7 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs notification_enabled_ap_ids = potential_receiver_ap_ids - |> exclude_relation_restricting_ap_ids(activity) + |> exclude_relationship_restricted_ap_ids(activity) |> exclude_thread_muter_ap_ids(activity) potential_receivers = @@ -355,10 +357,10 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo def get_notified_from_activity(_, _local_only), do: {[], []} @doc "Filters out AP IDs of users basing on their relationships with activity actor user" - def exclude_relation_restricting_ap_ids([], _activity), do: [] + def exclude_relationship_restricted_ap_ids([], _activity), do: [] - def exclude_relation_restricting_ap_ids(ap_ids, %Activity{} = activity) do - relation_restricted_ap_ids = + def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do + relationship_restricted_ap_ids = activity |> Activity.user_actor() |> User.incoming_relationships_ungrouped_ap_ids([ @@ -366,7 +368,7 @@ def exclude_relation_restricting_ap_ids(ap_ids, %Activity{} = activity) do :notification_mute ]) - Enum.uniq(ap_ids) -- relation_restricted_ap_ids + Enum.uniq(ap_ids) -- relationship_restricted_ap_ids end @doc "Filters out AP IDs of users who mute activity thread" diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex index 2b4cf02cf..a7ea13891 100644 --- a/lib/pleroma/thread_mute.ex +++ b/lib/pleroma/thread_mute.ex @@ -41,15 +41,16 @@ def muters_query(context) do def muter_ap_ids(context, ap_ids \\ nil) - def muter_ap_ids(context, ap_ids) when context not in [nil, ""] do + # Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.) + def muter_ap_ids(context, _ap_ids) when is_nil(context), do: [] + + def muter_ap_ids(context, ap_ids) do context |> muters_query() |> maybe_filter_on_ap_id(ap_ids) |> Repo.all() end - def muter_ap_ids(_context, _ap_ids), do: [] - defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do where(query, [tm, u], u.ap_id in ^ap_ids) end From be5e2c4dbba63831ea6a0617556e686969b5080f Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 25 Mar 2020 17:01:45 +0300 Subject: [PATCH 74/79] Applied relationships preloading to GET /api/v1/accounts/relationships. Refactoring (User.binary_id/1). --- lib/pleroma/conversation/participation.ex | 11 ++++------- lib/pleroma/following_relationship.ex | 8 ++------ lib/pleroma/thread_mute.ex | 4 ++-- lib/pleroma/user.ex | 15 +++++++++++++++ lib/pleroma/user_relationship.ex | 9 ++------- .../web/mastodon_api/views/account_view.ex | 6 +++++- 6 files changed, 30 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 693825cf5..215265fc9 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -129,21 +129,18 @@ def for_user(user, params \\ %{}) do end def restrict_recipients(query, user, %{"recipients" => user_ids}) do - user_ids = + user_binary_ids = [user.id | user_ids] |> Enum.uniq() - |> Enum.reduce([], fn user_id, acc -> - {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id) - [user_id | acc] - end) + |> User.binary_id() conversation_subquery = __MODULE__ |> group_by([p], p.conversation_id) |> having( [p], - count(p.user_id) == ^length(user_ids) and - fragment("array_agg(?) @> ?", p.user_id, ^user_ids) + count(p.user_id) == ^length(user_binary_ids) and + fragment("array_agg(?) @> ?", p.user_id, ^user_binary_ids) ) |> select([p], %{id: p.conversation_id}) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index dd1696136..624bddfe4 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -135,12 +135,8 @@ def all_between_user_sets( target_users ) when is_list(source_users) and is_list(target_users) do - get_bin_ids = fn user -> - with {:ok, bin_id} <- CompatType.dump(user.id), do: bin_id - end - - source_user_ids = Enum.map(source_users, &get_bin_ids.(&1)) - target_user_ids = Enum.map(target_users, &get_bin_ids.(&1)) + source_user_ids = User.binary_id(source_users) + target_user_ids = User.binary_id(target_users) __MODULE__ |> where( diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex index cc815430a..f657758aa 100644 --- a/lib/pleroma/thread_mute.ex +++ b/lib/pleroma/thread_mute.ex @@ -24,10 +24,10 @@ def changeset(mute, params \\ %{}) do end def query(user_id, context) do - {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id) + user_binary_id = User.binary_id(user_id) ThreadMute - |> Ecto.Query.where(user_id: ^user_id) + |> Ecto.Query.where(user_id: ^user_binary_id) |> Ecto.Query.where(context: ^context) end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f74e43cce..699256a3b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -218,6 +218,21 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \ end end + @doc "Dumps id to SQL-compatible format" + def binary_id(source_id) when is_binary(source_id) do + with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do + dumped_id + else + _ -> source_id + end + end + + def binary_id(source_ids) when is_list(source_ids) do + Enum.map(source_ids, &binary_id/1) + end + + def binary_id(%User{} = user), do: binary_id(user.id) + @doc "Returns status account" @spec account_status(User.t()) :: account_status() def account_status(%User{deactivated: true}), do: :deactivated diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 9423e3a42..519d2998d 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -8,7 +8,6 @@ defmodule Pleroma.UserRelationship do import Ecto.Changeset import Ecto.Query - alias FlakeId.Ecto.CompatType alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserRelationship @@ -84,12 +83,8 @@ def dictionary( target_to_source_rel_types \\ nil ) when is_list(source_users) and is_list(target_users) do - get_bin_ids = fn user -> - with {:ok, bin_id} <- CompatType.dump(user.id), do: bin_id - end - - source_user_ids = Enum.map(source_users, &get_bin_ids.(&1)) - target_user_ids = Enum.map(target_users, &get_bin_ids.(&1)) + source_user_ids = User.binary_id(source_users) + target_user_ids = User.binary_id(target_users) get_rel_type_codes = fn rel_type -> user_relationship_mappings()[rel_type] end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 702d9e658..6b2eca1f3 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.UserRelationship alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy defp find_following_rel(following_relationships, follower, following) do @@ -129,7 +130,10 @@ def render( end def render("relationships.json", %{user: user, targets: targets}) do - render_many(targets, AccountView, "relationship.json", user: user, as: :target) + relationships_opts = StatusView.relationships_opts(user, targets) + opts = %{as: :target, user: user, relationships: relationships_opts} + + render_many(targets, AccountView, "relationship.json", opts) end defp do_render("show.json", %{user: user} = opts) do From 9081a071eecd0eeb4b67008754555e9c9d73eae7 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 25 Mar 2020 18:46:17 +0400 Subject: [PATCH 75/79] Add a test for accounts/update_credentials --- .../account_controller/update_credentials_test.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 51cebe567..b693c1a47 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -118,6 +118,18 @@ test "updates the user's hide_followers status", %{conn: conn} do assert user_data["pleroma"]["hide_followers"] == true end + test "updates the user's discoverable status", %{conn: conn} do + assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} = + conn + |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"}) + |> json_response(:ok) + + assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} = + conn + |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"}) + |> json_response(:ok) + end + test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{ From c8475cd5c63af18471864fe57504999ddd09e496 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 25 Mar 2020 15:48:15 +0000 Subject: [PATCH 76/79] Apply suggestion to benchmarks/load_testing/generator.ex --- benchmarks/load_testing/generator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex index 17e89c13c..e4673757c 100644 --- a/benchmarks/load_testing/generator.ex +++ b/benchmarks/load_testing/generator.ex @@ -24,7 +24,7 @@ def generate_users(opts) do IO.puts("Starting generating #{opts[:users_max]} users...") {time, users} = :timer.tc(fn -> do_generate_users(opts) end) - IO.puts("Inserting users take #{to_sec(time)} sec.\n") + IO.puts("Inserting users took #{to_sec(time)} sec.\n") users end From 460e41585c2cd3f137c0f80173da60167fb318bf Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 25 Mar 2020 20:33:34 +0300 Subject: [PATCH 77/79] Further preloading (more endpoints), refactoring, tests. --- lib/pleroma/following_relationship.ex | 6 + lib/pleroma/user.ex | 5 +- lib/pleroma/user_relationship.ex | 20 ++++ .../web/mastodon_api/views/account_view.ex | 36 +++--- .../mastodon_api/views/notification_view.ex | 42 ++++--- .../web/mastodon_api/views/status_view.ex | 29 ++--- .../mastodon_api/views/account_view_test.exs | 109 ++++++++++-------- .../views/notification_view_test.exs | 42 +++---- .../mastodon_api/views/status_view_test.exs | 15 ++- 9 files changed, 179 insertions(+), 125 deletions(-) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 624bddfe4..a9538ea4e 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -151,4 +151,10 @@ def all_between_user_sets( ) |> Repo.all() end + + def find(following_relationships, follower, following) do + Enum.find(following_relationships, fn + fr -> fr.follower_id == follower.id and fr.following_id == following.id + end) + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 699256a3b..8ccb9242d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -218,7 +218,10 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \ end end - @doc "Dumps id to SQL-compatible format" + @doc """ + Dumps Flake Id to SQL-compatible format (16-byte UUID). + E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>> + """ def binary_id(source_id) when is_binary(source_id) do with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do dumped_id diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 519d2998d..011cf6822 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do import Ecto.Changeset import Ecto.Query + alias Pleroma.FollowingRelationship alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserRelationship @@ -124,6 +125,25 @@ def exists?(dictionary, rel_type, source, target, func) do end end + @doc ":relationships option for StatusView / AccountView / NotificationView" + def view_relationships_option(nil = _reading_user, _actors) do + %{user_relationships: [], following_relationships: []} + end + + def view_relationships_option(%User{} = reading_user, actors) do + user_relationships = + UserRelationship.dictionary( + [reading_user], + actors, + [:block, :mute, :notification_mute, :reblog_mute], + [:block, :inverse_subscription] + ) + + following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) + + %{user_relationships: user_relationships, following_relationships: following_relationships} + end + defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do changeset |> validate_change(:target_id, fn _, target_id -> diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 6b2eca1f3..2cdfac7af 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -5,20 +5,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do use Pleroma.Web, :view + alias Pleroma.FollowingRelationship alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy - defp find_following_rel(following_relationships, follower, following) do - Enum.find(following_relationships, fn - fr -> fr.follower_id == follower.id and fr.following_id == following.id - end) - end - def render("index.json", %{users: users} = opts) do + relationships_opt = + if Map.has_key?(opts, :relationships) do + opts[:relationships] + else + UserRelationship.view_relationships_option(opts[:for], users) + end + + opts = Map.put(opts, :relationships, relationships_opt) + users |> render_many(AccountView, "show.json", opts) |> Enum.filter(&Enum.any?/1) @@ -53,7 +56,7 @@ def render( follow_state = if following_relationships do user_to_target_following_relation = - find_following_rel(following_relationships, reading_user, target) + FollowingRelationship.find(following_relationships, reading_user, target) User.get_follow_state(reading_user, target, user_to_target_following_relation) else @@ -62,7 +65,7 @@ def render( followed_by = if following_relationships do - case find_following_rel(following_relationships, target, reading_user) do + case FollowingRelationship.find(following_relationships, target, reading_user) do %{state: "accept"} -> true _ -> false end @@ -70,7 +73,7 @@ def render( User.following?(target, reading_user) end - # NOTE: adjust StatusView.relationships_opts/2 if adding new relation-related flags + # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags %{ id: to_string(target.id), following: follow_state == "accept", @@ -129,11 +132,16 @@ def render( } end - def render("relationships.json", %{user: user, targets: targets}) do - relationships_opts = StatusView.relationships_opts(user, targets) - opts = %{as: :target, user: user, relationships: relationships_opts} + def render("relationships.json", %{user: user, targets: targets} = opts) do + relationships_opt = + if Map.has_key?(opts, :relationships) do + opts[:relationships] + else + UserRelationship.view_relationships_option(user, targets) + end - render_many(targets, AccountView, "relationship.json", opts) + render_opts = %{as: :target, user: user, relationships: relationships_opt} + render_many(targets, AccountView, "relationship.json", render_opts) end defp do_render("show.json", %{user: user} = opts) do diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index e9c618496..db434271c 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -8,12 +8,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView - def render("index.json", %{notifications: notifications, for: reading_user}) do + def render("index.json", %{notifications: notifications, for: reading_user} = opts) do activities = Enum.map(notifications, & &1.activity) parent_activities = @@ -30,21 +31,28 @@ def render("index.json", %{notifications: notifications, for: reading_user}) do |> Activity.with_preloaded_object(:left) |> Pleroma.Repo.all() - move_activities_targets = - activities - |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) - |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) + relationships_opt = + if Map.has_key?(opts, :relationships) do + opts[:relationships] + else + move_activities_targets = + activities + |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) + |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) - actors = - activities - |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end) - |> Enum.filter(& &1) - |> Kernel.++(move_activities_targets) + actors = + activities + |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end) + |> Enum.filter(& &1) + |> Kernel.++(move_activities_targets) + + UserRelationship.view_relationships_option(reading_user, actors) + end opts = %{ for: reading_user, parent_activities: parent_activities, - relationships: StatusView.relationships_opts(reading_user, actors) + relationships: relationships_opt } safe_render_many(notifications, NotificationView, "show.json", opts) @@ -85,27 +93,27 @@ def render( } } - relationships_opts = %{relationships: opts[:relationships]} + relationships_opt = %{relationships: opts[:relationships]} case mastodon_type do "mention" -> - put_status(response, activity, reading_user, relationships_opts) + put_status(response, activity, reading_user, relationships_opt) "favourite" -> - put_status(response, parent_activity_fn.(), reading_user, relationships_opts) + put_status(response, parent_activity_fn.(), reading_user, relationships_opt) "reblog" -> - put_status(response, parent_activity_fn.(), reading_user, relationships_opts) + put_status(response, parent_activity_fn.(), reading_user, relationships_opt) "move" -> - put_target(response, activity, reading_user, relationships_opts) + put_target(response, activity, reading_user, relationships_opt) "follow" -> response "pleroma:emoji_reaction" -> response - |> put_status(parent_activity_fn.(), reading_user, relationships_opts) + |> put_status(parent_activity_fn.(), reading_user, relationships_opt) |> put_emoji(activity) _ -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 0ef65b352..7b1cb7bf8 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Activity alias Pleroma.ActivityExpiration - alias Pleroma.FollowingRelationship alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -72,24 +71,6 @@ defp reblogged?(activity, user) do present?(user && user.ap_id in (object.data["announcements"] || [])) end - def relationships_opts(_reading_user = nil, _actors) do - %{user_relationships: [], following_relationships: []} - end - - def relationships_opts(reading_user, actors) do - user_relationships = - UserRelationship.dictionary( - [reading_user], - actors, - [:block, :mute, :notification_mute, :reblog_mute], - [:block, :inverse_subscription] - ) - - following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) - - %{user_relationships: user_relationships, following_relationships: following_relationships} - end - def render("index.json", opts) do # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list activities = Enum.filter(opts.activities, & &1) @@ -105,13 +86,19 @@ def render("index.json", opts) do |> Activity.with_set_thread_muted_field(opts[:for]) |> Repo.all() - actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) + relationships_opt = + if Map.has_key?(opts, :relationships) do + opts[:relationships] + else + actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) + UserRelationship.view_relationships_option(opts[:for], actors) + end opts = opts |> Map.put(:replied_to_activities, replied_to_activities) |> Map.put(:parent_activities, parent_activities) - |> Map.put(:relationships, relationships_opts(opts[:for], actors)) + |> Map.put(:relationships, relationships_opt) safe_render_many(activities, StatusView, "show.json", opts) end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 983886c6b..ede62903f 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -4,8 +4,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView @@ -182,6 +185,29 @@ test "Represent a smaller mention" do end describe "relationship" do + defp test_relationship_rendering(user, other_user, expected_result) do + opts = %{user: user, target: other_user} + assert expected_result == AccountView.render("relationship.json", opts) + + relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) + opts = Map.put(opts, :relationships, relationships_opt) + assert expected_result == AccountView.render("relationship.json", opts) + end + + @blank_response %{ + following: false, + followed_by: false, + blocking: false, + blocked_by: false, + muting: false, + muting_notifications: false, + subscribing: false, + requested: false, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + } + test "represent a relationship for the following and followed user" do user = insert(:user) other_user = insert(:user) @@ -192,23 +218,21 @@ test "represent a relationship for the following and followed user" do {:ok, _user_relationships} = User.mute(user, other_user, true) {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) - expected = %{ - id: to_string(other_user.id), - following: true, - followed_by: true, - blocking: false, - blocked_by: false, - muting: true, - muting_notifications: true, - subscribing: true, - requested: false, - domain_blocking: false, - showing_reblogs: false, - endorsed: false - } + expected = + Map.merge( + @blank_response, + %{ + following: true, + followed_by: true, + muting: true, + muting_notifications: true, + subscribing: true, + showing_reblogs: false, + id: to_string(other_user.id) + } + ) - assert expected == - AccountView.render("relationship.json", %{user: user, target: other_user}) + test_relationship_rendering(user, other_user, expected) end test "represent a relationship for the blocking and blocked user" do @@ -220,23 +244,13 @@ test "represent a relationship for the blocking and blocked user" do {:ok, _user_relationship} = User.block(user, other_user) {:ok, _user_relationship} = User.block(other_user, user) - expected = %{ - id: to_string(other_user.id), - following: false, - followed_by: false, - blocking: true, - blocked_by: true, - muting: false, - muting_notifications: false, - subscribing: false, - requested: false, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - } + expected = + Map.merge( + @blank_response, + %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)} + ) - assert expected == - AccountView.render("relationship.json", %{user: user, target: other_user}) + test_relationship_rendering(user, other_user, expected) end test "represent a relationship for the user blocking a domain" do @@ -245,8 +259,13 @@ test "represent a relationship for the user blocking a domain" do {:ok, user} = User.block_domain(user, "bad.site") - assert %{domain_blocking: true, blocking: false} = - AccountView.render("relationship.json", %{user: user, target: other_user}) + expected = + Map.merge( + @blank_response, + %{domain_blocking: true, blocking: false, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) end test "represent a relationship for the user with a pending follow request" do @@ -257,23 +276,13 @@ test "represent a relationship for the user with a pending follow request" do user = User.get_cached_by_id(user.id) other_user = User.get_cached_by_id(other_user.id) - expected = %{ - id: to_string(other_user.id), - following: false, - followed_by: false, - blocking: false, - blocked_by: false, - muting: false, - muting_notifications: false, - subscribing: false, - requested: true, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - } + expected = + Map.merge( + @blank_response, + %{requested: true, following: false, id: to_string(other_user.id)} + ) - assert expected == - AccountView.render("relationship.json", %{user: user, target: other_user}) + test_relationship_rendering(user, other_user, expected) end end diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index d04c3022f..7965af00a 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -16,6 +16,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Web.MastodonAPI.StatusView import Pleroma.Factory + defp test_notifications_rendering(notifications, user, expected_result) do + result = NotificationView.render("index.json", %{notifications: notifications, for: user}) + + assert expected_result == result + + result = + NotificationView.render("index.json", %{ + notifications: notifications, + for: user, + relationships: nil + }) + + assert expected_result == result + end + test "Mention notification" do user = insert(:user) mentioned_user = insert(:user) @@ -32,10 +47,7 @@ test "Mention notification" do created_at: Utils.to_masto_date(notification.inserted_at) } - result = - NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user}) - - assert [expected] == result + test_notifications_rendering([notification], mentioned_user, [expected]) end test "Favourite notification" do @@ -55,9 +67,7 @@ test "Favourite notification" do created_at: Utils.to_masto_date(notification.inserted_at) } - result = NotificationView.render("index.json", %{notifications: [notification], for: user}) - - assert [expected] == result + test_notifications_rendering([notification], user, [expected]) end test "Reblog notification" do @@ -77,9 +87,7 @@ test "Reblog notification" do created_at: Utils.to_masto_date(notification.inserted_at) } - result = NotificationView.render("index.json", %{notifications: [notification], for: user}) - - assert [expected] == result + test_notifications_rendering([notification], user, [expected]) end test "Follow notification" do @@ -96,16 +104,12 @@ test "Follow notification" do created_at: Utils.to_masto_date(notification.inserted_at) } - result = - NotificationView.render("index.json", %{notifications: [notification], for: followed}) - - assert [expected] == result + test_notifications_rendering([notification], followed, [expected]) User.perform(:delete, follower) notification = Notification |> Repo.one() |> Repo.preload(:activity) - assert [] == - NotificationView.render("index.json", %{notifications: [notification], for: followed}) + test_notifications_rendering([notification], followed, []) end test "Move notification" do @@ -131,8 +135,7 @@ test "Move notification" do created_at: Utils.to_masto_date(notification.inserted_at) } - assert [expected] == - NotificationView.render("index.json", %{notifications: [notification], for: follower}) + test_notifications_rendering([notification], follower, [expected]) end test "EmojiReact notification" do @@ -158,7 +161,6 @@ test "EmojiReact notification" do created_at: Utils.to_masto_date(notification.inserted_at) } - assert expected == - NotificationView.render("show.json", %{notification: notification, for: user}) + test_notifications_rendering([notification], user, [expected]) end end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 191895c6f..9191730cd 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -12,10 +12,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView + import Pleroma.Factory import Tesla.Mock @@ -212,12 +214,21 @@ test "tells if the message is muted for some reason" do {:ok, _user_relationships} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) - status = StatusView.render("show.json", %{activity: activity}) + relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) + + opts = %{activity: activity} + status = StatusView.render("show.json", opts) assert status.muted == false - status = StatusView.render("show.json", %{activity: activity, for: user}) + status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt)) + assert status.muted == false + for_opts = %{activity: activity, for: user} + status = StatusView.render("show.json", for_opts) + assert status.muted == true + + status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt)) assert status.muted == true end From 6b793d3f8336fcba5cac596f9e76d0274633f98d Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 26 Mar 2020 21:54:01 +0300 Subject: [PATCH 78/79] Ensured no auxiliary computations (actors list preparation etc.) related to relationships preloading if no user is present (for statuses / accounts / relationships rendering). --- .../web/mastodon_api/views/account_view.ex | 26 +++++++++++----- .../mastodon_api/views/notification_view.ex | 31 +++++++++++-------- .../web/mastodon_api/views/status_view.ex | 16 +++++++--- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 2cdfac7af..0efcabc01 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -14,10 +14,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do def render("index.json", %{users: users} = opts) do relationships_opt = - if Map.has_key?(opts, :relationships) do - opts[:relationships] - else - UserRelationship.view_relationships_option(opts[:for], users) + cond do + Map.has_key?(opts, :relationships) -> + opts[:relationships] + + is_nil(opts[:for]) -> + UserRelationship.view_relationships_option(nil, []) + + true -> + UserRelationship.view_relationships_option(opts[:for], users) end opts = Map.put(opts, :relationships, relationships_opt) @@ -134,10 +139,15 @@ def render( def render("relationships.json", %{user: user, targets: targets} = opts) do relationships_opt = - if Map.has_key?(opts, :relationships) do - opts[:relationships] - else - UserRelationship.view_relationships_option(user, targets) + cond do + Map.has_key?(opts, :relationships) -> + opts[:relationships] + + is_nil(opts[:for]) -> + UserRelationship.view_relationships_option(nil, []) + + true -> + UserRelationship.view_relationships_option(user, targets) end render_opts = %{as: :target, user: user, relationships: relationships_opt} diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index db434271c..a809080fd 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -32,21 +32,26 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op |> Pleroma.Repo.all() relationships_opt = - if Map.has_key?(opts, :relationships) do - opts[:relationships] - else - move_activities_targets = - activities - |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) - |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) + cond do + Map.has_key?(opts, :relationships) -> + opts[:relationships] - actors = - activities - |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end) - |> Enum.filter(& &1) - |> Kernel.++(move_activities_targets) + is_nil(opts[:for]) -> + UserRelationship.view_relationships_option(nil, []) - UserRelationship.view_relationships_option(reading_user, actors) + true -> + move_activities_targets = + activities + |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) + |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) + + actors = + activities + |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end) + |> Enum.filter(& &1) + |> Kernel.++(move_activities_targets) + + UserRelationship.view_relationships_option(reading_user, actors) end opts = %{ diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7b1cb7bf8..d36b9ee5c 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -87,11 +87,17 @@ def render("index.json", opts) do |> Repo.all() relationships_opt = - if Map.has_key?(opts, :relationships) do - opts[:relationships] - else - actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) - UserRelationship.view_relationships_option(opts[:for], actors) + cond do + Map.has_key?(opts, :relationships) -> + opts[:relationships] + + is_nil(opts[:for]) -> + UserRelationship.view_relationships_option(nil, []) + + true -> + actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) + + UserRelationship.view_relationships_option(opts[:for], actors) end opts = From dfbc05d4965a04a82d4c4c5b8842f4117757f30e Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 27 Mar 2020 08:01:03 +0300 Subject: [PATCH 79/79] Misc refactoring / tweaks (`ThreadMute.exists?/2`). --- lib/pleroma/thread_mute.ex | 4 ++-- lib/pleroma/web/common_api/common_api.ex | 2 +- .../web/mastodon_api/views/notification_view.ex | 12 ++++++------ lib/pleroma/web/mastodon_api/views/status_view.ex | 7 ++++--- test/web/mastodon_api/views/account_view_test.exs | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex index 5768e7711..be01d541d 100644 --- a/lib/pleroma/thread_mute.ex +++ b/lib/pleroma/thread_mute.ex @@ -68,8 +68,8 @@ def remove_mute(user_id, context) do |> Repo.delete_all() end - def check_muted(user_id, context) do + def exists?(user_id, context) do query(user_id, context) - |> Repo.all() + |> Repo.exists?() end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 091011c6b..2646b9f7b 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -358,7 +358,7 @@ def remove_mute(user, activity) do def thread_muted?(%{id: nil} = _user, _activity), do: false def thread_muted?(user, activity) do - ThreadMute.check_muted(user.id, activity.data["context"]) != [] + ThreadMute.exists?(user.id, activity.data["context"]) end def report(user, %{"account_id" => account_id} = data) do diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index a809080fd..89f5734ff 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -98,27 +98,27 @@ def render( } } - relationships_opt = %{relationships: opts[:relationships]} + render_opts = %{relationships: opts[:relationships]} case mastodon_type do "mention" -> - put_status(response, activity, reading_user, relationships_opt) + put_status(response, activity, reading_user, render_opts) "favourite" -> - put_status(response, parent_activity_fn.(), reading_user, relationships_opt) + put_status(response, parent_activity_fn.(), reading_user, render_opts) "reblog" -> - put_status(response, parent_activity_fn.(), reading_user, relationships_opt) + put_status(response, parent_activity_fn.(), reading_user, render_opts) "move" -> - put_target(response, activity, reading_user, relationships_opt) + put_target(response, activity, reading_user, render_opts) "follow" -> response "pleroma:emoji_reaction" -> response - |> put_status(parent_activity_fn.(), reading_user, relationships_opt) + |> put_status(parent_activity_fn.(), reading_user, render_opts) |> put_emoji(activity) _ -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index d36b9ee5c..440eef4ba 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -228,9 +228,10 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} end thread_muted? = - case activity.thread_muted? do - thread_muted? when is_boolean(thread_muted?) -> thread_muted? - nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false + cond do + is_nil(opts[:for]) -> false + is_boolean(activity.thread_muted?) -> activity.thread_muted? + true -> CommonAPI.thread_muted?(opts[:for], activity) end attachment_data = object.data["attachment"] || [] diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index ede62903f..0d1c3ecb3 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -186,7 +186,7 @@ test "Represent a smaller mention" do describe "relationship" do defp test_relationship_rendering(user, other_user, expected_result) do - opts = %{user: user, target: other_user} + opts = %{user: user, target: other_user, relationships: nil} assert expected_result == AccountView.render("relationship.json", opts) relationships_opt = UserRelationship.view_relationships_option(user, [other_user])