From 241a3d744ae4e9d040247ad0aeb6287156acf920 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 11 Feb 2020 13:53:24 +0400 Subject: [PATCH 001/375] Add ActivityExpirationPolicy --- config/config.exs | 2 + lib/pleroma/web/activity_pub/mrf.ex | 7 +--- .../mrf/activity_expiration_policy.ex | 35 +++++++++++++++++ .../mrf/activity_expiration_policy_test.exs | 38 +++++++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex create mode 100644 test/web/activity_pub/mrf/activity_expiration_policy_test.exs diff --git a/config/config.exs b/config/config.exs index 41c1ff637..d5b298c16 100644 --- a/config/config.exs +++ b/config/config.exs @@ -361,6 +361,8 @@ config :pleroma, :mrf_subchain, match_actor: %{} +config :pleroma, :mrf_activity_expiration, days: 365 + config :pleroma, :mrf_vocabulary, accept: [], reject: [] diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 263ed11af..b6e737de5 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -8,11 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do def filter(policies, %{} = object) do policies |> Enum.reduce({:ok, object}, fn - policy, {:ok, object} -> - policy.filter(object) - - _, error -> - error + policy, {:ok, object} -> policy.filter(object) + _, error -> error end) end diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex new file mode 100644 index 000000000..1b8860161 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do + @moduledoc "Adds expiration to all local activities" + @behaviour Pleroma.Web.ActivityPub.MRF + + @impl true + def filter(%{"id" => id} = activity) do + activity = + if String.starts_with?(id, Pleroma.Web.Endpoint.url()) do + maybe_add_expiration(activity) + else + activity + end + + {:ok, activity} + end + + @impl true + def describe, do: {:ok, %{}} + + defp maybe_add_expiration(activity) do + days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) + expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days) + + with %{"expires_at" => existing_expires_at} <- activity, + :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do + activity + else + _ -> Map.put(activity, "expires_at", expires_at) + end + end +end diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs new file mode 100644 index 000000000..2e65048c0 --- /dev/null +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do + use ExUnit.Case, async: true + alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + + @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" + + test "adds `expires_at` property" do + assert {:ok, %{"expires_at" => expires_at}} = ActivityExpirationPolicy.filter(%{"id" => @id}) + + assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 + end + + test "keeps existing `expires_at` if it less than the config setting" do + expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1) + + assert {:ok, %{"expires_at" => ^expires_at}} = + ActivityExpirationPolicy.filter(%{"id" => @id, "expires_at" => expires_at}) + end + + test "owerwrites existing `expires_at` if it greater than the config setting" do + too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2) + + assert {:ok, %{"expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{"id" => @id, "expires_at" => too_distant_future}) + + assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 + end + + test "ignores remote activities" do + assert {:ok, activity} = ActivityExpirationPolicy.filter(%{"id" => "https://example.com/123"}) + + refute Map.has_key?(activity, "expires_at") + end +end From 4d459b0e9906b2ebc0280b36c92007b2e680671f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 12 Feb 2020 22:51:26 +0400 Subject: [PATCH 002/375] Move ActivityExpiration creation from CommonApi.post/2 to ActivityPub.insert/4 --- lib/pleroma/web/activity_pub/activity_pub.ex | 17 ++++++++++++++--- lib/pleroma/web/common_api/activity_draft.ex | 9 ++++++++- lib/pleroma/web/common_api/common_api.ex | 12 +----------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 5c436941a..408f6c966 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1,10 +1,11 @@ # 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.ActivityPub.ActivityPub do alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics + alias Pleroma.ActivityExpiration alias Pleroma.Config alias Pleroma.Conversation alias Pleroma.Conversation.Participation @@ -135,12 +136,14 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when {:containment, :ok} <- {:containment, Containment.contain_child(map)}, {:ok, map, object} <- insert_full_object(map) do {:ok, activity} = - Repo.insert(%Activity{ + %Activity{ data: map, local: local, actor: map["actor"], recipients: recipients - }) + } + |> Repo.insert() + |> maybe_create_activity_expiration() # Splice in the child object if we have one. activity = @@ -180,6 +183,14 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when end end + defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do + with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do + {:ok, activity} + end + end + + defp maybe_create_activity_expiration(result), do: result + defp create_or_bump_conversation(activity, actor) do with {:ok, conversation} <- Conversation.create_or_bump_for(activity), %User{} = user <- User.get_cached_by_ap_id(actor), diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index f7da81b34..7a83cad9c 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -193,6 +193,13 @@ defp preview?(draft) do defp changes(draft) do direct? = draft.visibility == "direct" + additional = %{"cc" => draft.cc, "directMessage" => direct?} + + additional = + case draft.expires_at do + %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at) + _ -> additional + end changes = %{ @@ -200,7 +207,7 @@ defp changes(draft) do actor: draft.user, context: draft.context, object: draft.object, - additional: %{"cc" => draft.cc, "directMessage" => direct?} + additional: additional } |> Utils.maybe_add_list_data(draft.user, draft.visibility) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2a348dcf6..03921de27 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -277,20 +277,10 @@ def listen(user, %{"title" => _} = data) do def post(user, %{"status" => _} = data) do with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do - draft.changes - |> ActivityPub.create(draft.preview?) - |> maybe_create_activity_expiration(draft.expires_at) + ActivityPub.create(draft.changes, draft.preview?) end end - defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do - with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do - {:ok, activity} - end - end - - defp maybe_create_activity_expiration(result, _), do: result - # Updates the emojis for a user based on their profile def update(user) do emoji = emoji_from_profile(user) From e2d358f1fb0babbdd2a318bad863e27afecbb3d1 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 14 Feb 2020 15:19:23 +0400 Subject: [PATCH 003/375] Fix typo --- test/web/activity_pub/mrf/activity_expiration_policy_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs index 2e65048c0..2f2f90b44 100644 --- a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -21,7 +21,7 @@ test "keeps existing `expires_at` if it less than the config setting" do ActivityExpirationPolicy.filter(%{"id" => @id, "expires_at" => expires_at}) end - test "owerwrites existing `expires_at` if it greater than the config setting" do + test "overwrites existing `expires_at` if it greater than the config setting" do too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2) assert {:ok, %{"expires_at" => expires_at}} = From 57878f870879995f53227bb7a24b810531dd4217 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 14 Feb 2020 15:50:31 +0400 Subject: [PATCH 004/375] Improve readability --- .../web/activity_pub/mrf/activity_expiration_policy.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 1b8860161..5d823f2c7 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do @behaviour Pleroma.Web.ActivityPub.MRF @impl true - def filter(%{"id" => id} = activity) do + def filter(activity) do activity = - if String.starts_with?(id, Pleroma.Web.Endpoint.url()) do + if local?(activity) do maybe_add_expiration(activity) else activity @@ -21,6 +21,10 @@ def filter(%{"id" => id} = activity) do @impl true def describe, do: {:ok, %{}} + defp local?(%{"id" => id}) do + String.starts_with?(id, Pleroma.Web.Endpoint.url()) + end + defp maybe_add_expiration(activity) do days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days) From 3732b0ba729bb7443e338b5f6bcc7e018983aa4c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 14 Feb 2020 16:39:02 +0400 Subject: [PATCH 005/375] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150fd27cd..e4a641a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ 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. +- MFR policy to set global expiration for every local activity +
API Changes From 0ddcd67d32eb40cb6cb2a3dfee4c55e930e7f37c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 14 Feb 2020 16:53:53 +0400 Subject: [PATCH 006/375] Update `cheatsheet.md` and `config/description.exs` --- config/description.exs | 15 +++++++++++++++ docs/configuration/cheatsheet.md | 9 +++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/config/description.exs b/config/description.exs index e5bac9b3f..d86a4ccca 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1346,6 +1346,21 @@ } ] }, + %{ + group: :pleroma, + key: :mrf_activity_expiration, + label: "MRF Activity Expiration Policy", + type: :group, + description: "Adds expiration to all local activities", + children: [ + %{ + key: :days, + type: :integer, + description: "Default global expiration time for all local activities (in days)", + suggestions: [90, 365] + } + ] + }, %{ group: :pleroma, key: :mrf_subchain, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 2bd935983..bd03aec66 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -33,7 +33,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. - * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). + * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)). * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). @@ -43,7 +43,8 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). -* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)). +* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). @@ -142,6 +143,10 @@ config :pleroma, :mrf_user_allowlist, * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines * `:reject` rejects the message entirely +#### :mrf_activity_expiration + +* `days`: Default global expiration time for all local activities (in days) + ### :activitypub * ``unfollow_blocked``: Whether blocks result in people getting unfollowed * ``outgoing_blocks``: Whether to federate blocks to other instances From 819cd467170cb6dd1334cde0a0c79dbb785a22b6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 20 Feb 2020 22:04:02 +0400 Subject: [PATCH 007/375] Auto-expire Create activities only --- .../mrf/activity_expiration_policy.ex | 2 +- test/web/activity_pub/activity_pub_test.exs | 16 +++++++++ .../mrf/activity_expiration_policy_test.exs | 35 +++++++++++++++---- .../purge_expired_activities_worker_test.exs | 30 ++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 5d823f2c7..274bb9a5c 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do @impl true def filter(activity) do activity = - if local?(activity) do + if activity["type"] == "Create" && local?(activity) do maybe_add_expiration(activity) else activity diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index ce68e7d0e..2cd908a87 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1784,4 +1784,20 @@ test "old user must be in the new user's `also_known_as` list" do ActivityPub.move(old_user, new_user) end end + + describe "global activity expiration" do + clear_config([:instance, :rewrite_policy]) + + test "creates an activity expiration for local Create activities" do + Pleroma.Config.put( + [:instance, :rewrite_policy], + Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + ) + + {:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) + + assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all() + end + end end diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs index 2f2f90b44..0d3bcc457 100644 --- a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -9,7 +9,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" test "adds `expires_at` property" do - assert {:ok, %{"expires_at" => expires_at}} = ActivityExpirationPolicy.filter(%{"id" => @id}) + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{"id" => @id, "type" => "Create"}) assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 end @@ -17,21 +18,43 @@ test "adds `expires_at` property" do test "keeps existing `expires_at` if it less than the config setting" do expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1) - assert {:ok, %{"expires_at" => ^expires_at}} = - ActivityExpirationPolicy.filter(%{"id" => @id, "expires_at" => expires_at}) + assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "type" => "Create", + "expires_at" => expires_at + }) end test "overwrites existing `expires_at` if it greater than the config setting" do too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2) - assert {:ok, %{"expires_at" => expires_at}} = - ActivityExpirationPolicy.filter(%{"id" => @id, "expires_at" => too_distant_future}) + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "type" => "Create", + "expires_at" => too_distant_future + }) assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 end test "ignores remote activities" do - assert {:ok, activity} = ActivityExpirationPolicy.filter(%{"id" => "https://example.com/123"}) + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "type" => "Create" + }) + + refute Map.has_key?(activity, "expires_at") + end + + test "ignores non-Create activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "type" => "Follow" + }) refute Map.has_key?(activity, "expires_at") end diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs index c2561683e..c6c7ff388 100644 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ b/test/workers/cron/purge_expired_activities_worker_test.exs @@ -12,6 +12,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do import ExUnit.CaptureLog clear_config([ActivityExpiration, :enabled]) + clear_config([:instance, :rewrite_policy]) test "deletes an expiration activity" do Pleroma.Config.put([ActivityExpiration, :enabled], true) @@ -36,6 +37,35 @@ test "deletes an expiration activity" do refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) end + test "works with ActivityExpirationPolicy" do + Pleroma.Config.put([ActivityExpiration, :enabled], true) + + Pleroma.Config.put( + [:instance, :rewrite_policy], + Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + ) + + user = insert(:user) + + days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) + + {:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"}) + + past_date = + NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second) + + activity + |> Repo.preload(:expiration) + |> Map.get(:expiration) + |> Ecto.Changeset.change(%{scheduled_at: past_date}) + |> Repo.update!() + + Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid) + + assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] = + Pleroma.Repo.all(Pleroma.Activity) + end + describe "delete_activity/1" do test "adds log message if activity isn't find" do assert capture_log([level: :error], fn -> From 011ede45361096f55dda938078e24574cdf33b2b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 21 Feb 2020 14:42:43 +0400 Subject: [PATCH 008/375] Update documentation --- CHANGELOG.md | 2 +- config/description.exs | 4 ++-- docs/configuration/cheatsheet.md | 4 ++-- .../web/activity_pub/mrf/activity_expiration_policy.ex | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a641a7e..c5558e0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,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. -- MFR policy to set global expiration for every local activity +- MFR policy to set global expiration for all local Create activities
API Changes diff --git a/config/description.exs b/config/description.exs index d86a4ccca..f0c6e3377 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1351,12 +1351,12 @@ key: :mrf_activity_expiration, label: "MRF Activity Expiration Policy", type: :group, - description: "Adds expiration to all local activities", + description: "Adds expiration to all local Create activities", children: [ %{ key: :days, type: :integer, - description: "Default global expiration time for all local activities (in days)", + description: "Default global expiration time for all local Create activities (in days)", suggestions: [90, 365] } ] diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index bd03aec66..f50c8bab7 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -43,7 +43,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). - * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)). + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)). * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. @@ -145,7 +145,7 @@ config :pleroma, :mrf_user_allowlist, #### :mrf_activity_expiration -* `days`: Default global expiration time for all local activities (in days) +* `days`: Default global expiration time for all local Create activities (in days) ### :activitypub * ``unfollow_blocked``: Whether blocks result in people getting unfollowed diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 274bb9a5c..a9bdf3b69 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do - @moduledoc "Adds expiration to all local activities" + @moduledoc "Adds expiration to all local Create activities" @behaviour Pleroma.Web.ActivityPub.MRF @impl true From cb8236cda62cddb72f4320af6347defae44b81ca Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 20 Mar 2020 21:19:34 +0400 Subject: [PATCH 009/375] Add embeddable posts --- lib/pleroma/web/embed_controller.ex | 42 +++++++ lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/router.ex | 2 + .../web/templates/embed/_attachment.html.eex | 8 ++ lib/pleroma/web/templates/embed/show.html.eex | 76 ++++++++++++ .../web/templates/layout/embed.html.eex | 14 +++ lib/pleroma/web/views/embed_view.ex | 83 +++++++++++++ priv/static/embed.css | 115 ++++++++++++++++++ priv/static/embed.js | 43 +++++++ 9 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/embed_controller.ex create mode 100644 lib/pleroma/web/templates/embed/_attachment.html.eex create mode 100644 lib/pleroma/web/templates/embed/show.html.eex create mode 100644 lib/pleroma/web/templates/layout/embed.html.eex create mode 100644 lib/pleroma/web/views/embed_view.ex create mode 100644 priv/static/embed.css create mode 100644 priv/static/embed.js diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex new file mode 100644 index 000000000..f6b8a5ee1 --- /dev/null +++ b/lib/pleroma/web/embed_controller.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.EmbedController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User + + alias Pleroma.Web.ActivityPub.Visibility + + plug(:put_layout, :embed) + + def show(conn, %{"id" => id}) do + with %Activity{local: true} = activity <- + Activity.get_by_id_with_object(id), + true <- Visibility.is_public?(activity.object) do + {:ok, author} = User.get_or_fetch(activity.object.data["actor"]) + + conn + |> delete_resp_header("x-frame-options") + |> delete_resp_header("content-security-policy") + |> render("show.html", + activity: activity, + author: User.sanitize_html(author), + counts: get_counts(activity) + ) + end + end + + defp get_counts(%Activity{} = activity) do + %Object{data: data} = Object.normalize(activity) + + %{ + likes: Map.get(data, "like_count", 0), + replies: Map.get(data, "repliesCount", 0), + announces: Map.get(data, "announcement_count", 0) + } + end +end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 72cb3ee27..4f665db12 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Web.Endpoint do at: "/", from: :pleroma, only: - ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc), + ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css), # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength gzip: true, cache_control_for_etags: @static_cache_control, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3f36f6c1a..eef0a8023 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -637,6 +637,8 @@ defmodule Pleroma.Web.Router do post("/auth/password", MastodonAPI.AuthController, :password_reset) get("/web/*path", MastoFEController, :index) + + get("/embed/:id", EmbedController, :show) end pipeline :remote_media do diff --git a/lib/pleroma/web/templates/embed/_attachment.html.eex b/lib/pleroma/web/templates/embed/_attachment.html.eex new file mode 100644 index 000000000..7e04e9550 --- /dev/null +++ b/lib/pleroma/web/templates/embed/_attachment.html.eex @@ -0,0 +1,8 @@ +<%= case @mediaType do %> +<% "audio" -> %> + +<% "video" -> %> + +<% _ -> %> +<%= @name %> +<% end %> diff --git a/lib/pleroma/web/templates/embed/show.html.eex b/lib/pleroma/web/templates/embed/show.html.eex new file mode 100644 index 000000000..6bf8fac29 --- /dev/null +++ b/lib/pleroma/web/templates/embed/show.html.eex @@ -0,0 +1,76 @@ +
+ + +
+ <%= if status_title(@activity) != "" do %> +
open<% end %>> + <%= raw status_title(@activity) %> +
<%= activity_content(@activity) %>
+
+ <% else %> +
<%= activity_content(@activity) %>
+ <% end %> + <%= for %{"name" => name, "url" => [url | _]} <- attachments(@activity) do %> +
+ <%= if sensitive?(@activity) do %> +
+ <%= Gettext.gettext("sensitive media") %> +
+ <%= render("_attachment.html", %{name: name, url: url["href"], + mediaType: fetch_media_type(url)}) %> +
+
+ <% else %> + <%= render("_attachment.html", %{name: name, url: url["href"], + mediaType: fetch_media_type(url)}) %> + <% end %> +
+ <% end %> +
+ +
+
<%= Gettext.gettext("replies") %>
<%= @counts.replies %>
+
<%= Gettext.gettext("announces") %>
<%= @counts.announces %>
+
<%= Gettext.gettext("likes") %>
<%= @counts.likes %>
+
+ +

+ <%= link published(@activity), to: activity_url(@author, @activity) %> +

+
+ + diff --git a/lib/pleroma/web/templates/layout/embed.html.eex b/lib/pleroma/web/templates/layout/embed.html.eex new file mode 100644 index 000000000..57ae4f802 --- /dev/null +++ b/lib/pleroma/web/templates/layout/embed.html.eex @@ -0,0 +1,14 @@ + + + + + + <%= Pleroma.Config.get([:instance, :name]) %> + + <%= Phoenix.HTML.raw(assigns[:meta] || "") %> + + + + <%= render @view_module, @view_template, assigns %> + + diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex new file mode 100644 index 000000000..77536835b --- /dev/null +++ b/lib/pleroma/web/views/embed_view.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.EmbedView do + use Pleroma.Web, :view + + alias Calendar.Strftime + alias Pleroma.Activity + alias Pleroma.Emoji.Formatter + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.Gettext + alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Metadata.Utils + alias Pleroma.Web.Router.Helpers + + use Phoenix.HTML + + @media_types ["image", "audio", "video"] + + defp emoji_for_user(%User{} = user) do + user.source_data + |> Map.get("tag", []) + |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) + |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} + end) + end + + defp fetch_media_type(%{"mediaType" => mediaType}) do + Utils.fetch_media_type(@media_types, mediaType) + end + + defp open_content? do + Pleroma.Config.get( + [:frontend_configurations, :collapse_message_with_subjects], + true + ) + end + + defp full_nickname(user) do + %{host: host} = URI.parse(user.ap_id) + "@" <> user.nickname <> "@" <> host + end + + defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name), + do: name + + defp status_title(%Activity{object: %Object{data: %{"summary" => summary}}}) + when is_binary(summary), + do: summary + + defp status_title(_), do: nil + + defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) do + content |> Pleroma.HTML.filter_tags() |> raw() + end + + defp activity_content(_), do: nil + + defp activity_url(%User{local: true}, activity) do + Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) + end + + defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do + data["url"] || data["external_url"] || data["id"] + end + + defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do + attachments + end + + defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do + sensitive + end + + defp published(%Activity{object: %Object{data: %{"published" => published}}}) do + published + |> NaiveDateTime.from_iso8601!() + |> Strftime.strftime!("%B %d, %Y, %l:%M %p") + end +end diff --git a/priv/static/embed.css b/priv/static/embed.css new file mode 100644 index 000000000..cc79ee7ab --- /dev/null +++ b/priv/static/embed.css @@ -0,0 +1,115 @@ +body { + background-color: #282c37; + font-family: sans-serif; + color: white; + margin: 0; + padding: 1em; + padding-bottom: 0; +} + +.avatar { + cursor: pointer; +} + +.avatar img { + float: left; + border-radius: 4px; + margin-right: 4px; +} + +.activity-content { + padding-top: 1em; +} + +.attachment { + margin-top: 1em; +} + +.attachment img { + max-width: 100%; +} + +.date a { + text-decoration: none; +} + +.date a:hover { + text-decoration: underline; +} + +.date a, +.counts { + color: #666; + font-size: 0.9em; +} + +.counts dt, +.counts dd { + float: left; + margin-left: 1em; +} + +a { + color: white; +} + +.h-card { + min-height: 48px; + margin-bottom: 8px; +} + +.h-card a { + text-decoration: none; +} + +.h-card a:hover { + text-decoration: underline; +} + +.display-name { + padding-top: 4px; + display: block; + text-overflow: ellipsis; + overflow: hidden; + color: white; +} + +/* keep emoji from being hilariously huge */ +.display-name img { + max-height: 1em; +} + +.display-name .nickname { + padding-top: 4px; + display: block; +} + +.nickname:hover { + text-decoration: none; +} + +.pull-right { + float: right; +} + +.collapse { + margin: 0; + width: auto; +} + +a.button { + box-sizing: border-box; + display: inline-block; + color: white; + background-color: #419bdd; + border-radius: 4px; + border: none; + padding: 10px; + font-weight: 500; + font-size: 0.9em; +} + +a.button:hover { + text-decoration: none; + background-color: #61a6d9; +} diff --git a/priv/static/embed.js b/priv/static/embed.js new file mode 100644 index 000000000..f675f6417 --- /dev/null +++ b/priv/static/embed.js @@ -0,0 +1,43 @@ +(function () { + 'use strict' + + var ready = function (loaded) { + if (['interactive', 'complete'].indexOf(document.readyState) !== -1) { + loaded() + } else { + document.addEventListener('DOMContentLoaded', loaded) + } + } + + ready(function () { + var iframes = [] + + window.addEventListener('message', function (e) { + var data = e.data || {} + + if (data.type !== 'setHeightPleromaEmbed' || !iframes[data.id]) { + return + } + + iframes[data.id].height = data.height + }); + + [].forEach.call(document.querySelectorAll('iframe.pleroma-embed'), function (iframe) { + iframe.scrolling = 'no' + iframe.style.overflow = 'hidden' + + iframes.push(iframe) + + var id = iframes.length - 1 + + iframe.onload = function () { + iframe.contentWindow.postMessage({ + type: 'setHeightPleromaEmbed', + id: id + }, '*') + } + + iframe.onload() + }) + }) +})() From fc2eb1fbd6a5b38a3cf72e557cce1029d6b7f16f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 20 Mar 2020 22:16:57 +0400 Subject: [PATCH 010/375] Fix formatter warnings --- test/workers/cron/purge_expired_activities_worker_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs index 85ae1e5ef..beac55fb2 100644 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ b/test/workers/cron/purge_expired_activities_worker_test.exs @@ -11,8 +11,8 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do import Pleroma.Factory import ExUnit.CaptureLog - setup do - clear_config([ActivityExpiration, :enabled]) + setup do + clear_config([ActivityExpiration, :enabled]) clear_config([:instance, :rewrite_policy]) end From fd97b0e634d30dec3217efcf3d67610d1b54bf8b Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 9 Mar 2020 17:00:16 +0100 Subject: [PATCH 011/375] Chats: Basic implementation. --- lib/pleroma/chat.ex | 41 +++++++++++++++++ .../20200309123730_create_chats.exs | 16 +++++++ test/chat_test.exs | 44 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 lib/pleroma/chat.ex create mode 100644 priv/repo/migrations/20200309123730_create_chats.exs create mode 100644 test/chat_test.exs diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex new file mode 100644 index 000000000..e2a8b8eba --- /dev/null +++ b/lib/pleroma/chat.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Chat do + use Ecto.Schema + import Ecto.Changeset + + alias Pleroma.User + alias Pleroma.Repo + + @moduledoc """ + Chat keeps a reference to DirectMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet). + + It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. + """ + + schema "chats" do + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + field(:recipient, :string) + field(:unread, :integer, default: 0) + + timestamps() + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :recipient]) + |> validate_required([:user_id, :recipient]) + |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) + end + + def get_or_create(user_id, recipient) do + %__MODULE__{} + |> creation_cng(%{user_id: user_id, recipient: recipient}) + |> Repo.insert( + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], + conflict_target: [:user_id, :recipient] + ) + end +end diff --git a/priv/repo/migrations/20200309123730_create_chats.exs b/priv/repo/migrations/20200309123730_create_chats.exs new file mode 100644 index 000000000..715d798ea --- /dev/null +++ b/priv/repo/migrations/20200309123730_create_chats.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.CreateChats do + use Ecto.Migration + + def change do + create table(:chats) do + add(:user_id, references(:users, type: :uuid)) + # Recipient is an ActivityPub id, to future-proof for group support. + add(:recipient, :string) + add(:unread, :integer, default: 0) + timestamps() + end + + # There's only one chat between a user and a recipient. + create(index(:chats, [:user_id, :recipient], unique: true)) + end +end diff --git a/test/chat_test.exs b/test/chat_test.exs new file mode 100644 index 000000000..ca9206802 --- /dev/null +++ b/test/chat_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ChatTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + + import Pleroma.Factory + + describe "creation and getting" do + test "it creates a chat for a user and recipient" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + assert chat.id + end + + test "it returns a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + end + + test "a returning chat will have an updated `update_at` field" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + :timer.sleep(1500) + {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + assert chat.updated_at != chat_two.updated_at + end + end +end From 3775683a04e9b819f88bfba533b755bbd5b3c2df Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 8 Apr 2020 15:55:43 +0200 Subject: [PATCH 012/375] ChatMessage: Basic incoming handling. --- lib/pleroma/chat.ex | 2 +- lib/pleroma/web/activity_pub/activity_pub.ex | 1 + .../web/activity_pub/object_validator.ex | 30 +++++++++- .../chat_message_validator.ex | 58 +++++++++++++++++++ .../create_chat_message_validator.ex | 35 +++++++++++ ..._validator.ex => create_note_validator.ex} | 0 .../object_validators/types/recipients.ex | 23 ++++++++ .../web/activity_pub/transmogrifier.ex | 7 +++ .../transmogrifier/chat_message_handling.ex | 30 ++++++++++ test/fixtures/create-chat-message.json | 19 ++++++ .../types/recipients_test.exs | 15 +++++ .../transmogrifier/chat_message_test.exs | 32 ++++++++++ 12 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex create mode 100644 lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex rename lib/pleroma/web/activity_pub/object_validators/{create_validator.ex => create_note_validator.ex} (100%) create mode 100644 lib/pleroma/web/activity_pub/object_validators/types/recipients.ex create mode 100644 lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex create mode 100644 test/fixtures/create-chat-message.json create mode 100644 test/web/activity_pub/object_validators/types/recipients_test.exs create mode 100644 test/web/activity_pub/transmogrifier/chat_message_test.exs diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index e2a8b8eba..07ad62b97 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Chat do alias Pleroma.Repo @moduledoc """ - Chat keeps a reference to DirectMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet). + Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet). It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. """ diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 19286fd01..0b4892501 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -397,6 +397,7 @@ defp do_unreact_with_emoji(user, reaction_id, options) do end end + # TODO: Is this even used now? # TODO: This is weird, maybe we shouldn't check here if we can make the activity. @spec like(User.t(), Object.t(), String.t() | nil, boolean()) :: {:ok, Activity.t(), Object.t()} | {:error, any()} diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index dc4bce059..49cc72561 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -12,18 +12,46 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) def validate(%{"type" => "Like"} = object, meta) do with {:ok, object} <- - object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do + object + |> LikeValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object |> Map.from_struct()) {:ok, object, meta} end end + def validate(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object} <- + object + |> ChatMessageValidator.cast_and_apply() do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "Create"} = object, meta) do + with {:ok, object} <- + object + |> CreateChatMessageValidator.cast_and_apply() do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def stringify_keys(%{__struct__: _} = object) do + object + |> Map.from_struct() + |> stringify_keys + end + def stringify_keys(object) do object |> Map.new(fn {key, val} -> {to_string(key), val} end) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex new file mode 100644 index 000000000..ab5be3596 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:to, Types.Recipients, default: []) + field(:type, :string) + field(:content, :string) + field(:actor, Types.ObjectID) + field(:published, Types.DateTime) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def fix(data) do + data + |> Map.put_new("actor", data["attributedTo"]) + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields)) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["ChatMessage"]) + |> validate_required([:id, :actor, :to, :type, :content]) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex new file mode 100644 index 000000000..659311480 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTES +# - Can probably be a generic create validator +# - doesn't embed, will only get the object id +# - object has to be validated first, maybe with some meta info from the surrounding create +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + + @primary_key false + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:actor, Types.ObjectID) + field(:type, :string) + field(:to, Types.Recipients, default: []) + field(:object, Types.ObjectID) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_data(data) do + cast(%__MODULE__{}, data, __schema__(:fields)) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex similarity index 100% rename from lib/pleroma/web/activity_pub/object_validators/create_validator.ex rename to lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex new file mode 100644 index 000000000..5a3040842 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex @@ -0,0 +1,23 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do + use Ecto.Type + + def type, do: {:array, :string} + + def cast(object) when is_binary(object) do + cast([object]) + end + + def cast([_ | _] = data), do: {:ok, data} + + def cast(_) do + :error + end + + def dump(data) do + {:ok, data} + end + + def load(data) do + {:ok, data} + end +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0a8ad62ad..becc35ea3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator @@ -612,6 +613,12 @@ def handle_incoming( |> handle_incoming(options) end + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, + options + ), + do: ChatMessageHandling.handle_incoming(data, options) + def handle_incoming(%{"type" => "Like"} = data, _options) do with {_, {:ok, cast_data_sym}} <- {:casting_data, diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex new file mode 100644 index 000000000..b5843736f --- /dev/null +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling do + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator + alias Pleroma.Web.ActivityPub.Pipeline + + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object_data} = data, + _options + ) do + with {_, {:ok, cast_data_sym}} <- + {:casting_data, data |> CreateChatMessageValidator.cast_and_apply()}, + cast_data = ObjectValidator.stringify_keys(cast_data_sym), + {_, {:ok, object_cast_data_sym}} <- + {:casting_object_data, object_data |> ChatMessageValidator.cast_and_apply()}, + object_cast_data = ObjectValidator.stringify_keys(object_cast_data_sym), + {_, {:ok, validated_object, _meta}} <- + {:validate_object, ObjectValidator.validate(object_cast_data, %{})}, + {_, {:ok, _created_object}} <- {:persist_object, Object.create(validated_object)}, + {_, {:ok, activity, _meta}} <- + {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do + {:ok, activity} + end + end +end diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json new file mode 100644 index 000000000..4aa17f4a5 --- /dev/null +++ b/test/fixtures/create-chat-message.json @@ -0,0 +1,19 @@ +{ + "actor": "http://2hu.gensokyo/users/raymoo", + "id": "http://2hu.gensokyo/objects/1", + "object": { + "attributedTo": "http://2hu.gensokyo/users/raymoo", + "content": "You expected a cute girl? Too bad.", + "id": "http://2hu.gensokyo/objects/2", + "published": "2020-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "ChatMessage" + }, + "published": "2018-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "Create" +} diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs new file mode 100644 index 000000000..2f9218774 --- /dev/null +++ b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients + use Pleroma.DataCase + + test "it works with a list" do + list = ["https://lain.com/users/lain"] + assert {:ok, list} == Recipients.cast(list) + end + + test "it turns a single string into a list" do + recipient = "https://lain.com/users/lain" + + assert {:ok, [recipient]} == Recipients.cast(recipient) + end +end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs new file mode 100644 index 000000000..aed62c520 --- /dev/null +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + describe "handle_incoming" do + test "it insert it" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = insert(:user, ap_id: data["actor"], local: false) + recipient = insert(:user, ap_id: List.first(data["to"]), local: false) + + {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) + + assert activity.actor == author.ap_id + assert activity.recipients == [recipient.ap_id, author.ap_id] + + %Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object + end + end +end From 2e78686686f04726ad73749ee744b8a9df91ffb8 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 9 Apr 2020 12:44:20 +0200 Subject: [PATCH 013/375] SideEffects: Handle ChatMessage creation. --- lib/pleroma/chat.ex | 15 ++++++---- lib/pleroma/web/activity_pub/builder.ex | 22 +++++++++++++++ lib/pleroma/web/activity_pub/side_effects.ex | 29 ++++++++++++++++++++ test/chat_test.exs | 14 ++++++---- test/web/activity_pub/side_effects_test.exs | 26 ++++++++++++++++++ 5 files changed, 95 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 07ad62b97..b61bc4c0e 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -18,23 +18,28 @@ defmodule Pleroma.Chat do schema "chats" do belongs_to(:user, User, type: FlakeId.Ecto.CompatType) field(:recipient, :string) - field(:unread, :integer, default: 0) + field(:unread, :integer, default: 0, read_after_writes: true) timestamps() end def creation_cng(struct, params) do struct - |> cast(params, [:user_id, :recipient]) + |> cast(params, [:user_id, :recipient, :unread]) |> validate_required([:user_id, :recipient]) |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) end - def get_or_create(user_id, recipient) do + def get(user_id, recipient) do + __MODULE__ + |> Repo.get_by(user_id: user_id, recipient: recipient) + end + + def bump_or_create(user_id, recipient) do %__MODULE__{} - |> creation_cng(%{user_id: user_id, recipient: recipient}) + |> creation_cng(%{user_id: user_id, recipient: recipient, unread: 1}) |> Repo.insert( - on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()], inc: [unread: 1]], conflict_target: [:user_id, :recipient] ) end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 429a510b8..f0a6c1e1b 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -10,6 +10,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + def create(actor, object_id, recipients) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "actor" => actor.ap_id, + "to" => recipients, + "object" => object_id, + "type" => "Create" + }, []} + end + + def chat_message(actor, recipient, content) do + {:ok, + %{ + "id" => Utils.generate_object_id(), + "actor" => actor.ap_id, + "type" => "ChatMessage", + "to" => [recipient], + "content" => content + }, []} + end + @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} def like(actor, object) do object_actor = User.get_cached_by_ap_id(object.data["actor"]) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 666a4e310..594f32700 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -5,8 +5,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do liked object, a `Follow` activity will add the user to the follower collection, and so on. """ + alias Pleroma.Chat alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@ -21,8 +23,35 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do {:ok, object, meta} end + def handle(%{data: %{"type" => "Create", "object" => object_id}} = activity, meta) do + object = Object.get_by_ap_id(object_id) + + {:ok, _object} = handle_object_creation(object) + + {:ok, activity, meta} + end + # Nothing to do def handle(object, meta) do {:ok, object, meta} end + + def handle_object_creation(%{data: %{"type" => "ChatMessage"}} = object) do + actor = User.get_cached_by_ap_id(object.data["actor"]) + recipient = User.get_cached_by_ap_id(hd(object.data["to"])) + + [[actor, recipient], [recipient, actor]] + |> Enum.each(fn [user, other_user] -> + if user.local do + Chat.bump_or_create(user.id, other_user.ap_id) + end + end) + + {:ok, object} + end + + # Nothing to do + def handle_object_creation(object) do + {:ok, object} + end end diff --git a/test/chat_test.exs b/test/chat_test.exs index ca9206802..bb2b46d51 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -14,7 +14,7 @@ test "it creates a chat for a user and recipient" do user = insert(:user) other_user = insert(:user) - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) assert chat.id end @@ -23,19 +23,21 @@ test "it returns a chat for a user and recipient if it already exists" do user = insert(:user) other_user = insert(:user) - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) assert chat.id == chat_two.id end - test "a returning chat will have an updated `update_at` field" do + test "a returning chat will have an updated `update_at` field and an incremented unread count" do user = insert(:user) other_user = insert(:user) - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + assert chat.unread == 1 :timer.sleep(1500) - {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + assert chat_two.unread == 2 assert chat.id == chat_two.id assert chat.updated_at != chat_two.updated_at diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index b67bd14b3..5fd8372b5 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do use Pleroma.DataCase + alias Pleroma.Chat alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -31,4 +32,29 @@ test "add the like to the original object", %{like: like, user: user} do assert user.ap_id in object.data["likes"] end end + + describe "creation of ChatMessages" do + test "it creates a Chat for the local users and bumps the unread count" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + {:ok, chat_message_object} = Object.create(chat_message_data) + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + + # The remote user won't get a chat + chat = Chat.get(author.id, recipient.ap_id) + refute chat + + # The local user will get a chat + chat = Chat.get(recipient.id, author.ap_id) + assert chat + end + end end From 4b047850718086a6d2edb5b2d94c6f888eba3016 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 9 Apr 2020 12:46:33 +0200 Subject: [PATCH 014/375] SideEffects: Extend ChatMessage test. --- test/web/activity_pub/side_effects_test.exs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 5fd8372b5..b629d0d5d 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -55,6 +55,26 @@ test "it creates a Chat for the local users and bumps the unread count" do # The local user will get a chat chat = Chat.get(recipient.id, author.ap_id) assert chat + + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + {:ok, chat_message_object} = Object.create(chat_message_data) + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + + # Both users are local and get the chat + chat = Chat.get(author.id, recipient.ap_id) + assert chat + + chat = Chat.get(recipient.id, author.ap_id) + assert chat end end end From 8e637ae1a7b75fa08679ae9cf424650fc105de85 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 9 Apr 2020 13:20:16 +0200 Subject: [PATCH 015/375] CommonAPI: Basic ChatMessage support. --- lib/pleroma/web/common_api/common_api.ex | 23 +++++++++++++++++++++++ test/web/common_api/common_api_test.exs | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 636cf3301..39e15adbf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Conversation.Participation alias Pleroma.FollowingRelationship alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.ThreadMute alias Pleroma.User alias Pleroma.UserRelationship @@ -23,6 +24,28 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger + def post_chat_message(user, recipient, content) do + transaction = + Repo.transaction(fn -> + with {_, {:ok, chat_message_data, _meta}} <- + {:build_object, Builder.chat_message(user, recipient.ap_id, content)}, + {_, {:ok, chat_message_object}} <- + {:create_object, Object.create(chat_message_data)}, + {_, {:ok, create_activity_data, _meta}} <- + {:build_create_activity, + Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do + {:ok, activity} + end + end) + + case transaction do + {:ok, value} -> value + error -> error + end + end + def follow(follower, followed) do timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index f46ad0272..1aea06d24 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Conversation.Participation alias Pleroma.Object alias Pleroma.User @@ -21,6 +22,26 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "posting chat messages" do + test "it posts a chat message" do + author = insert(:user) + recipient = insert(:user) + + {:ok, activity} = CommonAPI.post_chat_message(author, recipient, "a test message") + + assert activity.data["type"] == "Create" + assert activity.local + object = Object.normalize(activity) + + assert object.data["type"] == "ChatMessage" + assert object.data["to"] == [recipient.ap_id] + assert object.data["content"] == "a test message" + + assert Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + end + end + 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) {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) From 68abea313d0be49aa6b8d4b980aa361383f991a7 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 9 Apr 2020 15:13:55 +0200 Subject: [PATCH 016/375] ChatController: Add creation and return of chats. --- lib/pleroma/chat.ex | 10 ++++ .../controllers/chat_controller.ex | 47 ++++++++++++++++ lib/pleroma/web/router.ex | 7 +++ .../controllers/chat_controller_test.exs | 55 +++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex create mode 100644 test/web/pleroma_api/controllers/chat_controller_test.exs diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index b61bc4c0e..2475019d1 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -35,6 +35,16 @@ def get(user_id, recipient) do |> Repo.get_by(user_id: user_id, recipient: recipient) end + def get_or_create(user_id, recipient) do + %__MODULE__{} + |> creation_cng(%{user_id: user_id, recipient: recipient}) + |> Repo.insert( + on_conflict: :nothing, + returning: true, + conflict_target: [:user_id, :recipient] + ) + end + def bump_or_create(user_id, recipient) do %__MODULE__{} |> creation_cng(%{user_id: user_id, recipient: recipient, unread: 1}) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex new file mode 100644 index 000000000..0ee8bea33 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.ChatController do + use Pleroma.Web, :controller + + alias Pleroma.Chat + alias Pleroma.Repo + + import Ecto.Query + + def index(%{assigns: %{user: %{id: user_id}}} = conn, _params) do + chats = + from(c in Chat, + where: c.user_id == ^user_id, + order_by: [desc: c.updated_at] + ) + |> Repo.all() + + represented_chats = + Enum.map(chats, fn chat -> + %{ + id: chat.id, + recipient: chat.recipient, + unread: chat.unread + } + end) + + conn + |> json(represented_chats) + end + + def create(%{assigns: %{user: user}} = conn, params) do + recipient = params["ap_id"] |> URI.decode_www_form() + + with {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do + represented_chat = %{ + id: chat.id, + recipient: chat.recipient, + unread: chat.unread + } + + conn + |> json(represented_chat) + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3ecd59cd1..18ce9ee4b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -284,6 +284,13 @@ defmodule Pleroma.Web.Router do end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do + scope [] do + pipe_through(:authenticated_api) + + post("/chats/by-ap-id/:ap_id", ChatController, :create) + get("/chats", ChatController, :index) + end + scope [] do pipe_through(:authenticated_api) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs new file mode 100644 index 000000000..40c09d1cd --- /dev/null +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Chat + + import Pleroma.Factory + + describe "POST /api/v1/pleroma/chats/by-ap-id/:id" do + test "it creates or returns a chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + result = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/chats/by-ap-id/#{URI.encode_www_form(other_user.ap_id)}") + |> json_response(200) + + assert result["id"] + end + end + + describe "GET /api/v1/pleroma/chats" do + test "it return a list of chats the current user is participating in, in descending order of updates", + %{conn: conn} do + user = insert(:user) + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id) + :timer.sleep(1000) + {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id) + :timer.sleep(1000) + {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id) + :timer.sleep(1000) + + # bump the second one + {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id) + + result = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/chats") + |> json_response(200) + + ids = Enum.map(result, & &1["id"]) + + assert ids == [chat_2.id, chat_3.id, chat_1.id] + end + end +end From e8fd0dd689be0c7bbca006f7267955329279da98 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 9 Apr 2020 16:59:49 +0200 Subject: [PATCH 017/375] ChatController: Basic support for returning messages. --- .../controllers/chat_controller.ex | 40 +++++++++++++++++++ lib/pleroma/web/router.ex | 1 + .../controllers/chat_controller_test.exs | 28 +++++++++++++ 3 files changed, 69 insertions(+) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 0ee8bea33..de23b9a22 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -5,10 +5,50 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do use Pleroma.Web, :controller alias Pleroma.Chat + alias Pleroma.Object alias Pleroma.Repo import Ecto.Query + def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) do + with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do + messages = + from(o in Object, + where: fragment("?->>'type' = ?", o.data, "ChatMessage"), + where: + fragment( + """ + (?->>'actor' = ? and ?->'to' = ?) + OR (?->>'actor' = ? and ?->'to' = ?) + """, + o.data, + ^user.ap_id, + o.data, + ^[chat.recipient], + o.data, + ^chat.recipient, + o.data, + ^[user.ap_id] + ), + order_by: [desc: o.id] + ) + |> Repo.all() + + represented_messages = + messages + |> Enum.map(fn message -> + %{ + actor: message.data["actor"], + id: message.id, + content: message.data["content"] + } + end) + + conn + |> json(represented_messages) + end + end + def index(%{assigns: %{user: %{id: user_id}}} = conn, _params) do chats = from(c in Chat, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 18ce9ee4b..368e77d3e 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -289,6 +289,7 @@ defmodule Pleroma.Web.Router do post("/chats/by-ap-id/:ap_id", ChatController, :create) get("/chats", ChatController, :index) + get("/chats/:id/messages", ChatController, :messages) end scope [] do diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 40c09d1cd..6b2db5064 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -5,9 +5,37 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Chat + alias Pleroma.Web.CommonAPI import Pleroma.Factory + describe "GET /api/v1/pleroma/chats/:id/messages" do + # TODO + # - Test that statuses don't show + # - Test the case where it's not the user's chat + # - Test the returned data + test "it returns the messages for a given chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response(200) + + assert length(result) == 3 + end + end + describe "POST /api/v1/pleroma/chats/by-ap-id/:id" do test "it creates or returns a chat", %{conn: conn} do user = insert(:user) From 2cc68414245805dc3b83c200798e424f139e71fc Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 9 Apr 2020 17:18:31 +0200 Subject: [PATCH 018/375] ChatController: Basic message posting. --- .../controllers/chat_controller.ex | 26 +++++++++++++++++++ lib/pleroma/web/router.ex | 1 + .../controllers/chat_controller_test.exs | 17 ++++++++++++ 3 files changed, 44 insertions(+) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index de23b9a22..972330f4e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -7,9 +7,35 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Chat alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI import Ecto.Query + # TODO + # - Oauth stuff + # - Views / Representers + # - Error handling + + def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ + "id" => id, + "content" => content + }) do + with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), + %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), + {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, content), + message <- Object.normalize(activity) do + represented_message = %{ + actor: message.data["actor"], + id: message.id, + content: message.data["content"] + } + + conn + |> json(represented_message) + end + end + def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do messages = diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 368e77d3e..ce69725dc 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -290,6 +290,7 @@ defmodule Pleroma.Web.Router do post("/chats/by-ap-id/:ap_id", ChatController, :create) get("/chats", ChatController, :index) get("/chats/:id/messages", ChatController, :messages) + post("/chats/:id/messages", ChatController, :post_chat_message) end scope [] do diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 6b2db5064..b4230e5ad 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -9,6 +9,23 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do import Pleroma.Factory + describe "POST /api/v1/pleroma/chats/:id/messages" do + test "it posts a message to the chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) + |> json_response(200) + + assert result["content"] == "Hallo!!" + end + end + describe "GET /api/v1/pleroma/chats/:id/messages" do # TODO # - Test that statuses don't show From 64c78581fe397b6d9356c52cf3f43becd2ff3b4e Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 10 Apr 2020 14:47:56 +0200 Subject: [PATCH 019/375] Chat: Only create them for valid users for now. --- lib/pleroma/chat.ex | 7 +++++++ test/chat_test.exs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 2475019d1..c2044881f 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -26,6 +26,13 @@ defmodule Pleroma.Chat do def creation_cng(struct, params) do struct |> cast(params, [:user_id, :recipient, :unread]) + |> validate_change(:recipient, fn + :recipient, recipient -> + case User.get_cached_by_ap_id(recipient) do + nil -> [recipient: "must a an existing user"] + _ -> [] + end + end) |> validate_required([:user_id, :recipient]) |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) end diff --git a/test/chat_test.exs b/test/chat_test.exs index bb2b46d51..952598c87 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -10,6 +10,13 @@ defmodule Pleroma.ChatTest do import Pleroma.Factory describe "creation and getting" do + test "it only works if the recipient is a valid user (for now)" do + user = insert(:user) + + assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account") + assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account") + end + test "it creates a chat for a user and recipient" do user = insert(:user) other_user = insert(:user) From 6ace22b56a3ced833bd990de5715048d6bd32f80 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 15 Apr 2020 18:23:16 +0200 Subject: [PATCH 020/375] Chat: Add views, don't return them in timeline queries. --- lib/pleroma/web/activity_pub/activity_pub.ex | 13 +++ .../web/api_spec/operations/chat_operation.ex | 81 +++++++++++++++++++ lib/pleroma/web/common_api/common_api.ex | 2 +- .../controllers/chat_controller.ex | 47 +++-------- .../pleroma_api/views/chat_message_view.ex | 28 +++++++ .../web/pleroma_api/views/chat_view.ex | 21 +++++ .../controllers/timeline_controller_test.exs | 3 + .../controllers/chat_controller_test.exs | 8 +- .../views/chat_message_view_test.exs | 42 ++++++++++ 9 files changed, 207 insertions(+), 38 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/chat_operation.ex create mode 100644 lib/pleroma/web/pleroma_api/views/chat_message_view.ex create mode 100644 lib/pleroma/web/pleroma_api/views/chat_view.ex create mode 100644 test/web/pleroma_api/views/chat_message_view_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 4a56beb73..b6ba91052 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1207,6 +1207,18 @@ defp exclude_poll_votes(query, _) do end end + defp exclude_chat_messages(query, %{"include_chat_messages" => true}), do: query + + defp exclude_chat_messages(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, + where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage") + ) + else + query + end + end + defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do from(activity in query, where: activity.id != ^id) end @@ -1312,6 +1324,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_instance(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) + |> exclude_chat_messages(opts) |> exclude_visibility(opts) end diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex new file mode 100644 index 000000000..038ebb29d --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.ChatOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["chat"], + summary: "Create a chat", + responses: %{ + 200 => + Operation.response("Chat", "application/json", %Schema{ + type: :object, + description: "A created chat is returned", + properties: %{ + id: %Schema{type: :integer} + } + }) + } + } + end + + def index_operation do + %Operation{ + tags: ["chat"], + summary: "Get a list of chats that you participated in", + responses: %{ + 200 => + Operation.response("Chats", "application/json", %Schema{ + type: :array, + description: "A list of chats", + items: %Schema{ + type: :object, + description: "A chat" + } + }) + } + } + end + + def messages_operation do + %Operation{ + tags: ["chat"], + summary: "Get the most recent messages of the chat", + responses: %{ + 200 => + Operation.response("Messages", "application/json", %Schema{ + type: :array, + description: "A list of chat messages", + items: %Schema{ + type: :object, + description: "A chat message" + } + }) + } + } + end + + def post_chat_message_operation do + %Operation{ + tags: ["chat"], + summary: "Post a message to the chat", + responses: %{ + 200 => + Operation.response("Message", "application/json", %Schema{ + type: :object, + description: "A chat message" + }) + } + } + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2f13daf0c..c306c1e96 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger - def post_chat_message(user, recipient, content) do + def post_chat_message(%User{} = user, %User{} = recipient, content) do transaction = Repo.transaction(fn -> with {_, {:ok, chat_message_data, _meta}} <- diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 972330f4e..5ec546847 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -9,6 +9,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.ChatView + alias Pleroma.Web.PleromaAPI.ChatMessageView import Ecto.Query @@ -17,6 +19,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do # - Views / Representers # - Error handling + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation + def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ "id" => id, "content" => content @@ -25,14 +29,9 @@ def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, content), message <- Object.normalize(activity) do - represented_message = %{ - actor: message.data["actor"], - id: message.id, - content: message.data["content"] - } - conn - |> json(represented_message) + |> put_view(ChatMessageView) + |> render("show.json", for: user, object: message, chat: chat) end end @@ -60,18 +59,9 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) d ) |> Repo.all() - represented_messages = - messages - |> Enum.map(fn message -> - %{ - actor: message.data["actor"], - id: message.id, - content: message.data["content"] - } - end) - conn - |> json(represented_messages) + |> put_view(ChatMessageView) + |> render("index.json", for: user, objects: messages, chat: chat) end end @@ -83,31 +73,18 @@ def index(%{assigns: %{user: %{id: user_id}}} = conn, _params) do ) |> Repo.all() - represented_chats = - Enum.map(chats, fn chat -> - %{ - id: chat.id, - recipient: chat.recipient, - unread: chat.unread - } - end) - conn - |> json(represented_chats) + |> put_view(ChatView) + |> render("index.json", chats: chats) end def create(%{assigns: %{user: user}} = conn, params) do recipient = params["ap_id"] |> URI.decode_www_form() with {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do - represented_chat = %{ - id: chat.id, - recipient: chat.recipient, - unread: chat.unread - } - conn - |> json(represented_chat) + |> put_view(ChatView) + |> render("show.json", chat: chat) end end end diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex new file mode 100644 index 000000000..2df591358 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatMessageView do + use Pleroma.Web, :view + + alias Pleroma.Chat + + def render( + "show.json", + %{ + object: %{id: id, data: %{"type" => "ChatMessage"} = chat_message}, + chat: %Chat{id: chat_id} + } + ) do + %{ + id: id, + content: chat_message["content"], + chat_id: chat_id, + actor: chat_message["actor"] + } + end + + def render("index.json", opts) do + render_many(opts[:objects], __MODULE__, "show.json", Map.put(opts, :as, :object)) + end +end diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex new file mode 100644 index 000000000..ee48385bf --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatView do + use Pleroma.Web, :view + + alias Pleroma.Chat + + def render("show.json", %{chat: %Chat{} = chat}) do + %{ + id: chat.id, + recipient: chat.recipient, + unread: chat.unread + } + end + + def render("index.json", %{chats: chats}) do + render_many(chats, __MODULE__, "show.json") + end +end diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 06efdc901..a5c227991 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -51,6 +51,9 @@ test "the home timeline", %{user: user, conn: conn} do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "repeated post"}) {:ok, _, _} = CommonAPI.repeat(activity.id, following) + # This one should not show up in the TL + {:ok, _activity} = CommonAPI.post_chat_message(third_user, user, ":gun:") + ret_conn = get(conn, uri) assert Enum.empty?(json_response(ret_conn, :ok)) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index b4230e5ad..dad37a889 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -23,14 +23,13 @@ test "it posts a message to the chat", %{conn: conn} do |> json_response(200) assert result["content"] == "Hallo!!" + assert result["chat_id"] == chat.id end end describe "GET /api/v1/pleroma/chats/:id/messages" do # TODO - # - Test that statuses don't show # - Test the case where it's not the user's chat - # - Test the returned data test "it returns the messages for a given chat", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -49,6 +48,11 @@ test "it returns the messages for a given chat", %{conn: conn} do |> get("/api/v1/pleroma/chats/#{chat.id}/messages") |> json_response(200) + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id + end) + assert length(result) == 3 end end diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs new file mode 100644 index 000000000..e690da022 --- /dev/null +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.ChatMessageView + + import Pleroma.Factory + + test "it displays a chat message" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis") + + chat = Chat.get(user.id, recipient.ap_id) + + object = Object.normalize(activity) + + chat_message = ChatMessageView.render("show.json", object: object, for: user, chat: chat) + + assert chat_message[:id] == object.id + assert chat_message[:content] == "kippis" + assert chat_message[:actor] == user.ap_id + assert chat_message[:chat_id] + + {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk") + + object = Object.normalize(activity) + + chat_message_two = ChatMessageView.render("show.json", object: object, for: user, chat: chat) + + assert chat_message_two[:id] == object.id + assert chat_message_two[:content] == "gkgkgk" + assert chat_message_two[:actor] == recipient.ap_id + assert chat_message_two[:chat_id] == chat_message[:chat_id] + end +end From 3d4eca5dd4be297f03c244497d78db03e82a9d81 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 16 Apr 2020 12:56:29 +0200 Subject: [PATCH 021/375] CommonAPI: Escape HTML for chat messages. --- lib/pleroma/web/common_api/common_api.ex | 8 +++++++- test/web/common_api/common_api_test.exs | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index c306c1e96..2c25850db 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Formatter import Pleroma.Web.Gettext import Pleroma.Web.CommonAPI.Utils @@ -28,7 +29,12 @@ def post_chat_message(%User{} = user, %User{} = recipient, content) do transaction = Repo.transaction(fn -> with {_, {:ok, chat_message_data, _meta}} <- - {:build_object, Builder.chat_message(user, recipient.ap_id, content)}, + {:build_object, + Builder.chat_message( + user, + recipient.ap_id, + content |> Formatter.html_escape("text/plain") + )}, {_, {:ok, chat_message_object}} <- {:create_object, Object.create(chat_message_data)}, {_, {:ok, create_activity_data, _meta}} <- diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 168721c81..abe3e6f8d 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -27,7 +27,12 @@ test "it posts a chat message" do author = insert(:user) recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(author, recipient, "a test message") + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "a test message " + ) assert activity.data["type"] == "Create" assert activity.local @@ -35,7 +40,9 @@ test "it posts a chat message" do assert object.data["type"] == "ChatMessage" assert object.data["to"] == [recipient.ap_id] - assert object.data["content"] == "a test message" + + assert object.data["content"] == + "a test message <script>alert('uuu')</script>" assert Chat.get(author.id, recipient.ap_id) assert Chat.get(recipient.id, author.ap_id) From e2ced0491770d6260fe51d5144b81200fd97f268 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 16 Apr 2020 15:21:47 +0200 Subject: [PATCH 022/375] ChatMessages: Better validation. --- .../web/activity_pub/object_validator.ex | 6 ++- .../chat_message_validator.ex | 26 ++++++++++ .../object_validators/common_validations.ex | 6 ++- .../create_chat_message_validator.ex | 5 ++ .../transmogrifier/chat_message_handling.ex | 3 ++ test/fixtures/create-chat-message.json | 2 +- .../activity_pub/object_validator_test.exs | 52 +++++++++++++++++++ .../transmogrifier/chat_message_test.exs | 34 +++++++++++- 8 files changed, 128 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 49cc72561..259bbeb64 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -31,7 +31,8 @@ def validate(%{"type" => "Like"} = object, meta) do def validate(%{"type" => "ChatMessage"} = object, meta) do with {:ok, object} <- object - |> ChatMessageValidator.cast_and_apply() do + |> ChatMessageValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} end @@ -40,7 +41,8 @@ def validate(%{"type" => "ChatMessage"} = object, meta) do def validate(%{"type" => "Create"} = object, meta) do with {:ok, object} <- object - |> CreateChatMessageValidator.cast_and_apply() do + |> CreateChatMessageValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index ab5be3596..a4e4460cd 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do use Ecto.Schema alias Pleroma.Web.ActivityPub.ObjectValidators.Types + alias Pleroma.User import Ecto.Changeset @@ -54,5 +55,30 @@ def validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["ChatMessage"]) |> validate_required([:id, :actor, :to, :type, :content]) + |> validate_length(:to, is: 1) + |> validate_local_concern() + end + + @doc "Validates if at least one of the users in this ChatMessage is a local user, otherwise we don't want the message in our system. It also validates the presence of both users in our system." + def validate_local_concern(cng) do + with actor_ap <- get_field(cng, :actor), + {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, + {_, %User{} = recipient} <- + {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, + {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do + cng + else + {:local?, false} -> + cng + |> add_error(:actor, "actor and recipient are both remote") + + {:find_actor, _} -> + cng + |> add_error(:actor, "can't find user") + + {:find_recipient, _} -> + cng + |> add_error(:to, "can't find user") + end end end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index b479c3918..02f3a6438 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -8,7 +8,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do alias Pleroma.Object alias Pleroma.User - def validate_actor_presence(cng, field_name \\ :actor) do + def validate_actor_presence(cng) do + validate_user_presence(cng, :actor) + end + + def validate_user_presence(cng, field_name) do cng |> validate_change(field_name, fn field_name, actor -> if User.get_cached_by_ap_id(actor) do diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index 659311480..ce52d5623 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -32,4 +32,9 @@ def cast_and_apply(data) do def cast_data(data) do cast(%__MODULE__{}, data, __schema__(:fields)) end + + # No validation yet + def cast_and_validate(data) do + cast_data(data) + end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex index b5843736f..815b866c9 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -25,6 +25,9 @@ def handle_incoming( {_, {:ok, activity, _meta}} <- {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do {:ok, activity} + else + e -> + {:error, e} end end end diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json index 4aa17f4a5..2e4608f43 100644 --- a/test/fixtures/create-chat-message.json +++ b/test/fixtures/create-chat-message.json @@ -3,7 +3,7 @@ "id": "http://2hu.gensokyo/objects/1", "object": { "attributedTo": "http://2hu.gensokyo/users/raymoo", - "content": "You expected a cute girl? Too bad.", + "content": "You expected a cute girl? Too bad. ", "id": "http://2hu.gensokyo/objects/2", "published": "2020-02-12T14:08:20Z", "to": [ diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 3c5c3696e..bf0bfdfaf 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -5,9 +5,61 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI + alias Pleroma.Web.ActivityPub.Builder import Pleroma.Factory + describe "chat messages" do + setup do + user = insert(:user) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + + %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} + end + + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do + assert {:ok, _object, _meta} = ObjectValidator.validate(valid_chat_message, []) + end + + test "does not validate if the actor or the recipient is not in our system", %{ + valid_chat_message: valid_chat_message + } do + chat_message = + valid_chat_message + |> Map.put("actor", "https://raymoo.com/raymoo") + + {:error, _} = ObjectValidator.validate(chat_message, []) + + chat_message = + valid_chat_message + |> Map.put("to", ["https://raymoo.com/raymoo"]) + + {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate for a message with multiple recipients", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + chat_message = + valid_chat_message + |> Map.put("to", [user.ap_id, recipient.ap_id]) + + assert {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate if it doesn't concern local users" do + user = insert(:user, local: false) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) + end + end + describe "likes" do setup do user = insert(:user) diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index aed62c520..5b238f9c4 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -12,13 +12,43 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do alias Pleroma.Web.ActivityPub.Transmogrifier describe "handle_incoming" do - test "it insert it" do + test "it rejects messages that don't contain content" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.delete("content") + + data = + data + |> Map.put("object", object) + + _author = insert(:user, ap_id: data["actor"], local: false) + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't concern local users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = insert(:user, ap_id: data["actor"], local: false) + _recipient = insert(:user, ap_id: List.first(data["to"]), local: false) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it inserts it and creates a chat" do data = File.read!("test/fixtures/create-chat-message.json") |> Poison.decode!() author = insert(:user, ap_id: data["actor"], local: false) - recipient = insert(:user, ap_id: List.first(data["to"]), local: false) + recipient = insert(:user, ap_id: List.first(data["to"]), local: true) {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) From ca598e9c27a7a66b014523845e62046d19364f2f Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 16 Apr 2020 15:27:35 +0200 Subject: [PATCH 023/375] AccountView: Return user ap_id. --- lib/pleroma/web/mastodon_api/views/account_view.ex | 1 + test/web/mastodon_api/views/account_view_test.exs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 8fb96a22a..f20453744 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -234,6 +234,7 @@ defp do_render("show.json", %{user: user} = opts) do # Pleroma extension pleroma: %{ + ap_id: user.ap_id, confirmation_pending: user.confirmation_pending, tags: user.tags, hide_followers_count: user.hide_followers_count, diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 4435f69ff..2be0d8d0f 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -82,6 +82,7 @@ test "Represent a user account" do fields: [] }, pleroma: %{ + ap_id: user.ap_id, background_image: "https://example.com/images/asuka_hospital.png", confirmation_pending: false, tags: [], @@ -152,6 +153,7 @@ test "Represent a Service(bot) account" do fields: [] }, pleroma: %{ + ap_id: user.ap_id, background_image: nil, confirmation_pending: false, tags: [], @@ -351,6 +353,7 @@ test "represent an embedded relationship" do fields: [] }, pleroma: %{ + ap_id: user.ap_id, background_image: nil, confirmation_pending: false, tags: [], From e983f708846a5784e23b7e18734a61ed7f6e3636 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 16 Apr 2020 17:50:24 +0200 Subject: [PATCH 024/375] ChatMessagesHandling: Strip HTML of incoming messages. --- .../web/activity_pub/transmogrifier/chat_message_handling.ex | 3 +++ test/web/activity_pub/transmogrifier/chat_message_test.exs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex index 815b866c9..11bd10456 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -19,6 +19,9 @@ def handle_incoming( {_, {:ok, object_cast_data_sym}} <- {:casting_object_data, object_data |> ChatMessageValidator.cast_and_apply()}, object_cast_data = ObjectValidator.stringify_keys(object_cast_data_sym), + # For now, just strip HTML + stripped_content = Pleroma.HTML.strip_tags(object_cast_data["content"]), + object_cast_data = object_cast_data |> Map.put("content", stripped_content), {_, {:ok, validated_object, _meta}} <- {:validate_object, ObjectValidator.validate(object_cast_data, %{})}, {_, {:ok, _created_object}} <- {:persist_object, Object.create(validated_object)}, diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index 5b238f9c4..7e7f9ebec 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -56,7 +56,9 @@ test "it inserts it and creates a chat" do assert activity.recipients == [recipient.ap_id, author.ap_id] %Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object + assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')" end end end From f8c3ae7a627817789776f11497041445bb273c19 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 16 Apr 2020 18:43:31 +0200 Subject: [PATCH 025/375] ChatController: Handle pagination. --- .../controllers/chat_controller.ex | 12 ++-- .../pleroma_api/views/chat_message_view.ex | 4 +- .../web/pleroma_api/views/chat_view.ex | 2 +- .../controllers/chat_controller_test.exs | 62 ++++++++++++++++++- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 5ec546847..8cf8d82e4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Pagination import Ecto.Query @@ -35,7 +36,7 @@ def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ end end - def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) do + def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id} = params) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do messages = from(o in Object, @@ -54,10 +55,9 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) d ^chat.recipient, o.data, ^[user.ap_id] - ), - order_by: [desc: o.id] + ) ) - |> Repo.all() + |> Pagination.fetch_paginated(params) conn |> put_view(ChatMessageView) @@ -65,13 +65,13 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) d end end - def index(%{assigns: %{user: %{id: user_id}}} = conn, _params) do + def index(%{assigns: %{user: %{id: user_id}}} = conn, params) do chats = from(c in Chat, where: c.user_id == ^user_id, order_by: [desc: c.updated_at] ) - |> Repo.all() + |> Pagination.fetch_paginated(params) conn |> put_view(ChatView) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index 2df591358..fdbb9ff1b 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -15,9 +15,9 @@ def render( } ) do %{ - id: id, + id: id |> to_string(), content: chat_message["content"], - chat_id: chat_id, + chat_id: chat_id |> to_string(), actor: chat_message["actor"] } end diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index ee48385bf..7b8c6450a 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do def render("show.json", %{chat: %Chat{} = chat}) do %{ - id: chat.id, + id: chat.id |> to_string(), recipient: chat.recipient, unread: chat.unread } diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index dad37a889..f30fd6615 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -23,11 +23,38 @@ test "it posts a message to the chat", %{conn: conn} do |> json_response(200) assert result["content"] == "Hallo!!" - assert result["chat_id"] == chat.id + assert result["chat_id"] == chat.id |> to_string() end end describe "GET /api/v1/pleroma/chats/:id/messages" do + test "it paginates", %{conn: conn} do + user = insert(:user) + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + result = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response(200) + + assert length(result) == 20 + + result = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages", %{"max_id" => List.last(result)["id"]}) + |> json_response(200) + + assert length(result) == 10 + end + # TODO # - Test the case where it's not the user's chat test "it returns the messages for a given chat", %{conn: conn} do @@ -50,7 +77,7 @@ test "it returns the messages for a given chat", %{conn: conn} do result |> Enum.each(fn message -> - assert message["chat_id"] == chat.id + assert message["chat_id"] == chat.id |> to_string() end) assert length(result) == 3 @@ -73,6 +100,31 @@ test "it creates or returns a chat", %{conn: conn} do end describe "GET /api/v1/pleroma/chats" do + test "it paginates", %{conn: conn} do + user = insert(:user) + + Enum.each(1..30, fn _ -> + recipient = insert(:user) + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + end) + + result = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/chats") + |> json_response(200) + + assert length(result) == 20 + + result = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/chats", %{max_id: List.last(result)["id"]}) + |> json_response(200) + + assert length(result) == 10 + end + test "it return a list of chats the current user is participating in, in descending order of updates", %{conn: conn} do user = insert(:user) @@ -98,7 +150,11 @@ test "it return a list of chats the current user is participating in, in descend ids = Enum.map(result, & &1["id"]) - assert ids == [chat_2.id, chat_3.id, chat_1.id] + assert ids == [ + chat_2.id |> to_string(), + chat_3.id |> to_string(), + chat_1.id |> to_string() + ] end end end From d45ae6485811189e98f774ecdb46f0ccdfa8b2b3 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 17 Apr 2020 13:04:46 +0200 Subject: [PATCH 026/375] ChatController: Use OAuth scopes. --- .../controllers/chat_controller.ex | 18 +++++++- .../controllers/chat_controller_test.exs | 41 +++++++++---------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 8cf8d82e4..31c723426 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.PleromaAPI.ChatMessageView @@ -16,10 +17,18 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do import Ecto.Query # TODO - # - Oauth stuff - # - Views / Representers # - Error handling + plug( + OAuthScopesPlug, + %{scopes: ["write:statuses"]} when action in [:post_chat_message, :create] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"]} when action in [:messages, :index] + ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ @@ -62,6 +71,11 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id} = conn |> put_view(ChatMessageView) |> render("index.json", for: user, objects: messages, chat: chat) + else + _ -> + conn + |> put_status(:not_found) + |> json(%{error: "not found"}) end end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index f30fd6615..0750c7273 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -10,15 +10,15 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do import Pleroma.Factory describe "POST /api/v1/pleroma/chats/:id/messages" do - test "it posts a message to the chat", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:statuses"]) + + test "it posts a message to the chat", %{conn: conn, user: user} do other_user = insert(:user) {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) result = conn - |> assign(:user, user) |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) |> json_response(200) @@ -28,8 +28,9 @@ test "it posts a message to the chat", %{conn: conn} do end describe "GET /api/v1/pleroma/chats/:id/messages" do - test "it paginates", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:statuses"]) + + test "it paginates", %{conn: conn, user: user} do recipient = insert(:user) Enum.each(1..30, fn _ -> @@ -40,7 +41,6 @@ test "it paginates", %{conn: conn} do result = conn - |> assign(:user, user) |> get("/api/v1/pleroma/chats/#{chat.id}/messages") |> json_response(200) @@ -48,17 +48,13 @@ test "it paginates", %{conn: conn} do result = conn - |> assign(:user, user) |> get("/api/v1/pleroma/chats/#{chat.id}/messages", %{"max_id" => List.last(result)["id"]}) |> json_response(200) assert length(result) == 10 end - # TODO - # - Test the case where it's not the user's chat - test "it returns the messages for a given chat", %{conn: conn} do - user = insert(:user) + test "it returns the messages for a given chat", %{conn: conn, user: user} do other_user = insert(:user) third_user = insert(:user) @@ -71,7 +67,6 @@ test "it returns the messages for a given chat", %{conn: conn} do result = conn - |> assign(:user, user) |> get("/api/v1/pleroma/chats/#{chat.id}/messages") |> json_response(200) @@ -81,17 +76,25 @@ test "it returns the messages for a given chat", %{conn: conn} do end) assert length(result) == 3 + + # Trying to get the chat of a different user + result = + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + + assert result |> json_response(404) end end describe "POST /api/v1/pleroma/chats/by-ap-id/:id" do + setup do: oauth_access(["write:statuses"]) + test "it creates or returns a chat", %{conn: conn} do - user = insert(:user) other_user = insert(:user) result = conn - |> assign(:user, user) |> post("/api/v1/pleroma/chats/by-ap-id/#{URI.encode_www_form(other_user.ap_id)}") |> json_response(200) @@ -100,9 +103,9 @@ test "it creates or returns a chat", %{conn: conn} do end describe "GET /api/v1/pleroma/chats" do - test "it paginates", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:statuses"]) + test "it paginates", %{conn: conn, user: user} do Enum.each(1..30, fn _ -> recipient = insert(:user) {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) @@ -110,7 +113,6 @@ test "it paginates", %{conn: conn} do result = conn - |> assign(:user, user) |> get("/api/v1/pleroma/chats") |> json_response(200) @@ -118,7 +120,6 @@ test "it paginates", %{conn: conn} do result = conn - |> assign(:user, user) |> get("/api/v1/pleroma/chats", %{max_id: List.last(result)["id"]}) |> json_response(200) @@ -126,8 +127,7 @@ test "it paginates", %{conn: conn} do end test "it return a list of chats the current user is participating in, in descending order of updates", - %{conn: conn} do - user = insert(:user) + %{conn: conn, user: user} do har = insert(:user) jafnhar = insert(:user) tridi = insert(:user) @@ -144,7 +144,6 @@ test "it return a list of chats the current user is participating in, in descend result = conn - |> assign(:user, user) |> get("/api/v1/pleroma/chats") |> json_response(200) From 372614cfd3119b589c9c47619445714e8ae6c07e Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 17 Apr 2020 14:23:59 +0200 Subject: [PATCH 027/375] ChatView: Add a mastodon api representation of the recipient. --- .../transmogrifier/chat_message_handling.ex | 1 + .../web/pleroma_api/views/chat_view.ex | 7 ++++- .../transmogrifier/chat_message_test.exs | 19 ++++++++++++ test/web/pleroma_api/views/chat_view_test.exs | 29 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/web/pleroma_api/views/chat_view_test.exs diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex index 11bd10456..cfe3b767b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -22,6 +22,7 @@ def handle_incoming( # For now, just strip HTML stripped_content = Pleroma.HTML.strip_tags(object_cast_data["content"]), object_cast_data = object_cast_data |> Map.put("content", stripped_content), + {_, true} <- {:to_fields_match, cast_data["to"] == object_cast_data["to"]}, {_, {:ok, validated_object, _meta}} <- {:validate_object, ObjectValidator.validate(object_cast_data, %{})}, {_, {:ok, _created_object}} <- {:persist_object, Object.create(validated_object)}, diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 7b8c6450a..1e9ef4356 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -6,11 +6,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do use Pleroma.Web, :view alias Pleroma.Chat + alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView + + def render("show.json", %{chat: %Chat{} = chat} = opts) do + recipient = User.get_cached_by_ap_id(chat.recipient) - def render("show.json", %{chat: %Chat{} = chat}) do %{ id: chat.id |> to_string(), recipient: chat.recipient, + recipient_account: AccountView.render("show.json", Map.put(opts, :user, recipient)), unread: chat.unread } end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index 7e7f9ebec..4d6f24609 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do import Pleroma.Factory alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Object alias Pleroma.Web.ActivityPub.Transmogrifier @@ -42,6 +43,21 @@ test "it rejects messages that don't concern local users" do {:error, _} = Transmogrifier.handle_incoming(data) end + test "it rejects messages where the `to` field of activity and object don't match" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = insert(:user, ap_id: data["actor"]) + _recipient = insert(:user, ap_id: List.first(data["to"])) + + data = + data + |> Map.put("to", author.ap_id) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + test "it inserts it and creates a chat" do data = File.read!("test/fixtures/create-chat-message.json") @@ -59,6 +75,9 @@ test "it inserts it and creates a chat" do assert object assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')" + + refute Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) end end end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs new file mode 100644 index 000000000..1eb0c6241 --- /dev/null +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Web.PleromaAPI.ChatView + alias Pleroma.Web.MastodonAPI.AccountView + + import Pleroma.Factory + + test "it represents a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + assert represented_chat == %{ + id: "#{chat.id}", + recipient: recipient.ap_id, + recipient_account: AccountView.render("show.json", user: recipient), + unread: 0 + } + end +end From c8458209110ef65101f965e460329308e5843559 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 17 Apr 2020 16:55:01 +0200 Subject: [PATCH 028/375] Notifications: Create a chat notification. --- lib/pleroma/web/activity_pub/side_effects.ex | 2 ++ .../mastodon_api/views/notification_view.ex | 30 ++++++++++++++++++- test/web/activity_pub/side_effects_test.exs | 19 ++++++++++++ .../views/notification_view_test.exs | 26 ++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 594f32700..f32a99ec4 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -28,6 +28,8 @@ def handle(%{data: %{"type" => "Create", "object" => object_id}} = activity, met {:ok, _object} = handle_object_creation(object) + Notification.create_notifications(activity) + {:ok, activity, meta} end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 734ffbf39..5d231f0c4 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -8,11 +8,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.User + alias Pleroma.Object alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.PleromaAPI.ChatMessageView def render("index.json", %{notifications: notifications, for: reading_user} = opts) do activities = Enum.map(notifications, & &1.activity) @@ -81,7 +83,20 @@ def render( end end - mastodon_type = Activity.mastodon_notification_type(activity) + # This returns the notification type by activity, but both chats and statuses are in "Create" activities. + mastodon_type = + case Activity.mastodon_notification_type(activity) do + "mention" -> + object = Object.normalize(activity) + + case object do + %{data: %{"type" => "ChatMessage"}} -> "pleroma:chat_mention" + _ -> "mention" + end + + type -> + type + end render_opts = %{ relationships: opts[:relationships], @@ -125,6 +140,9 @@ def render( |> put_status(parent_activity_fn.(), reading_user, render_opts) |> put_emoji(activity) + "pleroma:chat_mention" -> + put_chat_message(response, activity, reading_user, render_opts) + _ -> nil end @@ -137,6 +155,16 @@ defp put_emoji(response, activity) do Map.put(response, :emoji, activity.data["content"]) end + defp put_chat_message(response, activity, reading_user, opts) do + object = Object.normalize(activity) + author = User.get_cached_by_ap_id(object.data["actor"]) + chat = Pleroma.Chat.get(reading_user.id, author.ap_id) + render_opts = Map.merge(opts, %{object: object, for: reading_user, chat: chat}) + chat_message_render = ChatMessageView.render("show.json", render_opts) + + Map.put(response, :chat_message, chat_message_render) + end + 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) diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index b629d0d5d..d3ad4866c 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -6,7 +6,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do use Pleroma.DataCase alias Pleroma.Chat + alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.SideEffects @@ -34,6 +36,23 @@ test "add the like to the original object", %{like: like, user: user} do end describe "creation of ChatMessages" do + test "notifies the recipient" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + {:ok, chat_message_object} = Object.create(chat_message_data) + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + + assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) + end + test "it creates a Chat for the local users and bumps the unread count" do author = insert(:user, local: false) recipient = insert(:user, local: true) diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index c3ec9dfec..a48c298f2 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -6,7 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Notification + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -14,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.PleromaAPI.ChatMessageView import Pleroma.Factory defp test_notifications_rendering(notifications, user, expected_result) do @@ -31,6 +34,29 @@ defp test_notifications_rendering(notifications, user, expected_result) do assert expected_result == result end + test "ChatMessage notification" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude") + + {:ok, [notification]} = Notification.create_notifications(activity) + + object = Object.normalize(activity) + chat = Chat.get(recipient.id, user.ap_id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "pleroma:chat_mention", + account: AccountView.render("show.json", %{user: user, for: recipient}), + chat_message: + ChatMessageView.render("show.json", %{object: object, for: recipient, chat: chat}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], recipient, [expected]) + end + test "Mention notification" do user = insert(:user) mentioned_user = insert(:user) From ce23673ca1539350802326c62d6e72bd040950f6 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Apr 2020 11:45:11 +0200 Subject: [PATCH 029/375] ChatMessageValidator: Don't validate messages that are too long. --- .../object_validators/chat_message_validator.ex | 1 + test/web/activity_pub/object_validator_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index a4e4460cd..caf2138a7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -56,6 +56,7 @@ def validate_data(data_cng) do |> validate_inclusion(:type, ["ChatMessage"]) |> validate_required([:id, :actor, :to, :type, :content]) |> validate_length(:to, is: 1) + |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit])) |> validate_local_concern() end diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index bf0bfdfaf..e416e0808 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do describe "chat messages" do setup do + clear_config([:instance, :remote_limit]) user = insert(:user) recipient = insert(:user, local: false) @@ -23,6 +24,13 @@ test "validates for a basic object we build", %{valid_chat_message: valid_chat_m assert {:ok, _object, _meta} = ObjectValidator.validate(valid_chat_message, []) end + test "does not validate if the message is longer than the remote_limit", %{ + valid_chat_message: valid_chat_message + } do + Pleroma.Config.put([:instance, :remote_limit], 2) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + test "does not validate if the actor or the recipient is not in our system", %{ valid_chat_message: valid_chat_message } do From 5b6818b3e5dc39e328f6f8d4b8f4587e5e1cef94 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Apr 2020 12:08:47 +0200 Subject: [PATCH 030/375] CommonAPI: Obey local limit for chat messages. --- lib/pleroma/web/common_api/common_api.ex | 8 +++++++- test/web/common_api/common_api_test.exs | 18 ++++++++++++++++++ .../views/chat_message_view_test.exs | 4 ++-- test/web/pleroma_api/views/chat_view_test.exs | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2b8add2fa..fcb0af4e8 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -28,7 +28,10 @@ defmodule Pleroma.Web.CommonAPI do def post_chat_message(%User{} = user, %User{} = recipient, content) do transaction = Repo.transaction(fn -> - with {_, {:ok, chat_message_data, _meta}} <- + with {_, true} <- + {:content_length, + String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])}, + {_, {:ok, chat_message_data, _meta}} <- {:build_object, Builder.chat_message( user, @@ -43,6 +46,9 @@ def post_chat_message(%User{} = user, %User{} = recipient, content) do {_, {:ok, %Activity{} = activity, _meta}} <- {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do {:ok, activity} + else + {:content_length, false} -> {:error, :content_too_long} + e -> e end end) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 1984aac8d..c17e30210 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -23,6 +23,8 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :max_pinned_statuses]) describe "posting chat messages" do + setup do: clear_config([:instance, :chat_limit]) + test "it posts a chat message" do author = insert(:user) recipient = insert(:user) @@ -47,6 +49,22 @@ test "it posts a chat message" do assert Chat.get(author.id, recipient.ap_id) assert Chat.get(recipient.id, author.ap_id) end + + test "it reject messages over the local limit" do + Pleroma.Config.put([:instance, :chat_limit], 2) + + author = insert(:user) + recipient = insert(:user) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123" + ) + + assert message == :content_too_long + end end test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index e690da022..ad8febee6 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -23,7 +23,7 @@ test "it displays a chat message" do chat_message = ChatMessageView.render("show.json", object: object, for: user, chat: chat) - assert chat_message[:id] == object.id + assert chat_message[:id] == object.id |> to_string() assert chat_message[:content] == "kippis" assert chat_message[:actor] == user.ap_id assert chat_message[:chat_id] @@ -34,7 +34,7 @@ test "it displays a chat message" do chat_message_two = ChatMessageView.render("show.json", object: object, for: user, chat: chat) - assert chat_message_two[:id] == object.id + assert chat_message_two[:id] == object.id |> to_string() assert chat_message_two[:content] == "gkgkgk" assert chat_message_two[:actor] == recipient.ap_id assert chat_message_two[:chat_id] == chat_message[:chat_id] diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index 1eb0c6241..3dca555e8 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do +defmodule Pleroma.Web.PleromaAPI.ChatViewTest do use Pleroma.DataCase alias Pleroma.Chat From 970b74383b43aa9a54c3cf59012944355e3eafbc Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Apr 2020 12:29:19 +0200 Subject: [PATCH 031/375] Credo fixes. --- lib/pleroma/chat.ex | 2 +- lib/pleroma/web/activity_pub/object_validator.ex | 2 +- .../object_validators/chat_message_validator.ex | 2 +- lib/pleroma/web/common_api/common_api.ex | 2 +- lib/pleroma/web/mastodon_api/views/notification_view.ex | 5 +++-- lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 6 +++--- test/web/activity_pub/object_validator_test.exs | 2 +- test/web/pleroma_api/views/chat_view_test.exs | 2 +- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index c2044881f..b8545063a 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Chat do use Ecto.Schema import Ecto.Changeset - alias Pleroma.User alias Pleroma.Repo + alias Pleroma.User @moduledoc """ Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet). diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 259bbeb64..03db681ec 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -11,9 +11,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User - alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index caf2138a7..6e3477cd1 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do use Ecto.Schema - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index fcb0af4e8..9e25f4c2f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation alias Pleroma.FollowingRelationship + alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.Repo alias Pleroma.ThreadMute @@ -17,7 +18,6 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Formatter import Pleroma.Web.Gettext import Pleroma.Web.CommonAPI.Utils diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 5d231f0c4..0b05d178b 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Activity alias Pleroma.Notification - alias Pleroma.User alias Pleroma.Object + alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView @@ -83,7 +83,8 @@ def render( end end - # This returns the notification type by activity, but both chats and statuses are in "Create" activities. + # This returns the notification type by activity, but both chats and statuses + # are in "Create" activities. mastodon_type = case Activity.mastodon_notification_type(activity) do "mention" -> diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 31c723426..9d8b9b3cf 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -6,13 +6,13 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Chat alias Pleroma.Object + alias Pleroma.Pagination + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.PleromaAPI.ChatMessageView - alias Pleroma.Pagination + alias Pleroma.Web.PleromaAPI.ChatView import Ecto.Query diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index e416e0808..3ac5ecaf4 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -1,11 +1,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI - alias Pleroma.Web.ActivityPub.Builder import Pleroma.Factory diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index 3dca555e8..725da5ff8 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -6,8 +6,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do use Pleroma.DataCase alias Pleroma.Chat - alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.PleromaAPI.ChatView import Pleroma.Factory From b836d3d104f75841d71f9cf7c5c8cb5c07ba7294 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Apr 2020 13:14:59 +0200 Subject: [PATCH 032/375] ChatMessageValidator: Require `published` field --- lib/pleroma/web/activity_pub/builder.ex | 6 ++++-- .../object_validators/chat_message_validator.ex | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index f0a6c1e1b..b67166a30 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -17,7 +17,8 @@ def create(actor, object_id, recipients) do "actor" => actor.ap_id, "to" => recipients, "object" => object_id, - "type" => "Create" + "type" => "Create", + "published" => DateTime.utc_now() |> DateTime.to_iso8601() }, []} end @@ -28,7 +29,8 @@ def chat_message(actor, recipient, content) do "actor" => actor.ap_id, "type" => "ChatMessage", "to" => [recipient], - "content" => content + "content" => content, + "published" => DateTime.utc_now() |> DateTime.to_iso8601() }, []} end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 6e3477cd1..9b8262553 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -54,7 +54,7 @@ def changeset(struct, data) do def validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["ChatMessage"]) - |> validate_required([:id, :actor, :to, :type, :content]) + |> validate_required([:id, :actor, :to, :type, :content, :published]) |> validate_length(:to, is: 1) |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit])) |> validate_local_concern() From 7e53da250e3b41e01073148efea0fc4f49dea9d5 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Apr 2020 14:08:54 +0200 Subject: [PATCH 033/375] ChatMessage: Support emoji. --- lib/pleroma/web/activity_pub/builder.ex | 4 ++- .../chat_message_validator.ex | 1 + test/fixtures/create-chat-message.json | 30 +++++++++---------- .../activity_pub/object_validator_test.exs | 6 ++-- test/web/common_api/common_api_test.exs | 8 +++-- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index b67166a30..7576ed278 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do This module encodes our addressing policies and general shape of our objects. """ + alias Pleroma.Emoji alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils @@ -30,7 +31,8 @@ def chat_message(actor, recipient, content) do "type" => "ChatMessage", "to" => [recipient], "content" => content, - "published" => DateTime.utc_now() |> DateTime.to_iso8601() + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "emoji" => Emoji.Formatter.get_emoji_map(content) }, []} end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 9b8262553..2feb65f29 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -20,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do field(:content, :string) field(:actor, Types.ObjectID) field(:published, Types.DateTime) + field(:emoji, :map, default: %{}) end def cast_and_apply(data) do diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json index 2e4608f43..6db5b9f5c 100644 --- a/test/fixtures/create-chat-message.json +++ b/test/fixtures/create-chat-message.json @@ -1,19 +1,19 @@ { - "actor": "http://2hu.gensokyo/users/raymoo", - "id": "http://2hu.gensokyo/objects/1", - "object": { - "attributedTo": "http://2hu.gensokyo/users/raymoo", - "content": "You expected a cute girl? Too bad. ", - "id": "http://2hu.gensokyo/objects/2", - "published": "2020-02-12T14:08:20Z", - "to": [ - "http://2hu.gensokyo/users/marisa" - ], - "type": "ChatMessage" - }, - "published": "2018-02-12T14:08:20Z", + "actor": "http://2hu.gensokyo/users/raymoo", + "id": "http://2hu.gensokyo/objects/1", + "object": { + "attributedTo": "http://2hu.gensokyo/users/raymoo", + "content": "You expected a cute girl? Too bad. ", + "id": "http://2hu.gensokyo/objects/2", + "published": "2020-02-12T14:08:20Z", "to": [ - "http://2hu.gensokyo/users/marisa" + "http://2hu.gensokyo/users/marisa" ], - "type": "Create" + "type": "ChatMessage" + }, + "published": "2018-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "Create" } diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 3ac5ecaf4..8230ae0d9 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -15,13 +15,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do user = insert(:user) recipient = insert(:user, local: false) - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} end test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do - assert {:ok, _object, _meta} = ObjectValidator.validate(valid_chat_message, []) + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object == valid_chat_message end test "does not validate if the message is longer than the remote_limit", %{ diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index c17e30210..86b3648ac 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -33,7 +33,7 @@ test "it posts a chat message" do CommonAPI.post_chat_message( author, recipient, - "a test message " + "a test message :firefox:" ) assert activity.data["type"] == "Create" @@ -44,7 +44,11 @@ test "it posts a chat message" do assert object.data["to"] == [recipient.ap_id] assert object.data["content"] == - "a test message <script>alert('uuu')</script>" + "a test message <script>alert('uuu')</script> :firefox:" + + assert object.data["emoji"] == %{ + "firefox" => "http://localhost:4001/emoji/Firefox.gif" + } assert Chat.get(author.id, recipient.ap_id) assert Chat.get(recipient.id, author.ap_id) From b5df4a98e4044cf1360f03f7dc3a0b59932ec8f5 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Apr 2020 14:38:53 +0200 Subject: [PATCH 034/375] ChatMessageView: Support emoji. --- lib/pleroma/web/pleroma_api/views/chat_message_view.ex | 6 +++++- test/web/pleroma_api/views/chat_message_view_test.exs | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index fdbb9ff1b..b40ab92a0 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageView do use Pleroma.Web, :view alias Pleroma.Chat + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.StatusView def render( "show.json", @@ -18,7 +20,9 @@ def render( id: id |> to_string(), content: chat_message["content"], chat_id: chat_id |> to_string(), - actor: chat_message["actor"] + actor: chat_message["actor"], + created_at: Utils.to_masto_date(chat_message["published"]), + emojis: StatusView.build_emojis(chat_message["emoji"]) } end diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index ad8febee6..115335f10 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do test "it displays a chat message" do user = insert(:user) recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis") + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") chat = Chat.get(user.id, recipient.ap_id) @@ -24,9 +24,11 @@ test "it displays a chat message" do chat_message = ChatMessageView.render("show.json", object: object, for: user, chat: chat) assert chat_message[:id] == object.id |> to_string() - assert chat_message[:content] == "kippis" + assert chat_message[:content] == "kippis :firefox:" assert chat_message[:actor] == user.ap_id assert chat_message[:chat_id] + assert chat_message[:created_at] + assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk") From 97ad0c45977261df3068ca4f0c3febce3173c058 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 21 Apr 2020 17:51:06 +0200 Subject: [PATCH 035/375] Chats: Add API specs. --- .../web/api_spec/operations/chat_operation.ex | 96 ++++++++++++------- .../schemas/chat_message_create_request.ex | 20 ++++ .../api_spec/schemas/chat_message_response.ex | 38 ++++++++ .../schemas/chat_messages_response.ex | 41 ++++++++ .../web/api_spec/schemas/chat_response.ex | 73 ++++++++++++++ .../web/api_spec/schemas/chats_response.ex | 69 +++++++++++++ .../controllers/chat_controller_test.exs | 42 ++++++++ 7 files changed, 346 insertions(+), 33 deletions(-) create mode 100644 lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex create mode 100644 lib/pleroma/web/api_spec/schemas/chat_message_response.ex create mode 100644 lib/pleroma/web/api_spec/schemas/chat_messages_response.ex create mode 100644 lib/pleroma/web/api_spec/schemas/chat_response.ex create mode 100644 lib/pleroma/web/api_spec/schemas/chats_response.ex diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 038ebb29d..5bd41ec4f 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -4,7 +4,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Operation - alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest + alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse @spec open_api_operation(atom) :: Operation.t() def open_api_operation(action) do @@ -16,16 +21,25 @@ def create_operation do %Operation{ tags: ["chat"], summary: "Create a chat", + parameters: [ + Operation.parameter( + :ap_id, + :path, + :string, + "The ActivityPub id of the recipient of this chat.", + required: true, + example: "https://lain.com/users/lain" + ) + ], responses: %{ 200 => - Operation.response("Chat", "application/json", %Schema{ - type: :object, - description: "A created chat is returned", - properties: %{ - id: %Schema{type: :integer} - } - }) - } + Operation.response("The created or existing chat", "application/json", ChatResponse) + }, + security: [ + %{ + "oAuth" => ["write"] + } + ] } end @@ -33,17 +47,19 @@ def index_operation do %Operation{ tags: ["chat"], summary: "Get a list of chats that you participated in", + parameters: [ + Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20), + Operation.parameter(:min_id, :query, :string, "Return only chats after this id"), + Operation.parameter(:max_id, :query, :string, "Return only chats before this id") + ], responses: %{ - 200 => - Operation.response("Chats", "application/json", %Schema{ - type: :array, - description: "A list of chats", - items: %Schema{ - type: :object, - description: "A chat" - } - }) - } + 200 => Operation.response("The chats of the user", "application/json", ChatsResponse) + }, + security: [ + %{ + "oAuth" => ["read"] + } + ] } end @@ -51,17 +67,21 @@ def messages_operation do %Operation{ tags: ["chat"], summary: "Get the most recent messages of the chat", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20), + Operation.parameter(:min_id, :query, :string, "Return only messages after this id"), + Operation.parameter(:max_id, :query, :string, "Return only messages before this id") + ], responses: %{ 200 => - Operation.response("Messages", "application/json", %Schema{ - type: :array, - description: "A list of chat messages", - items: %Schema{ - type: :object, - description: "A chat message" - } - }) - } + Operation.response("The messages in the chat", "application/json", ChatMessagesResponse) + }, + security: [ + %{ + "oAuth" => ["read"] + } + ] } end @@ -69,13 +89,23 @@ def post_chat_message_operation do %Operation{ tags: ["chat"], summary: "Post a message to the chat", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat") + ], + requestBody: Helpers.request_body("Parameters", ChatMessageCreateRequest, required: true), responses: %{ 200 => - Operation.response("Message", "application/json", %Schema{ - type: :object, - description: "A chat message" - }) - } + Operation.response( + "The newly created ChatMessage", + "application/json", + ChatMessageResponse + ) + }, + security: [ + %{ + "oAuth" => ["write"] + } + ] } end end diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex b/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex new file mode 100644 index 000000000..4dafcda43 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ChatMessageCreateRequest", + description: "POST body for creating an chat message", + type: :object, + properties: %{ + content: %Schema{type: :string, description: "The content of your message"} + }, + example: %{ + "content" => "Hey wanna buy feet pics?" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex new file mode 100644 index 000000000..e94c00369 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ChatMessageResponse", + description: "Response schema for a ChatMessage", + type: :object, + properties: %{ + id: %Schema{type: :string}, + actor: %Schema{type: :string, description: "The ActivityPub id of the actor"}, + chat_id: %Schema{type: :string}, + content: %Schema{type: :string}, + created_at: %Schema{type: :string, format: :datetime}, + emojis: %Schema{type: :array} + }, + example: %{ + "actor" => "https://dontbulling.me/users/lain", + "chat_id" => "1", + "content" => "hey you again", + "created_at" => "2020-04-21T15:06:45.000Z", + "emojis" => [ + %{ + "static_url" => "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker" => false, + "shortcode" => "firefox", + "url" => "https://dontbulling.me/emoji/Firefox.gif" + } + ], + "id" => "14" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex b/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex new file mode 100644 index 000000000..302bdec95 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse do + alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ChatMessagesResponse", + description: "Response schema for multiple ChatMessages", + type: :array, + items: ChatMessageResponse, + example: [ + %{ + "emojis" => [ + %{ + "static_url" => "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker" => false, + "shortcode" => "firefox", + "url" => "https://dontbulling.me/emoji/Firefox.gif" + } + ], + "created_at" => "2020-04-21T15:11:46.000Z", + "content" => "Check this out :firefox:", + "id" => "13", + "chat_id" => "1", + "actor" => "https://dontbulling.me/users/lain" + }, + %{ + "actor" => "https://dontbulling.me/users/lain", + "content" => "Whats' up?", + "id" => "12", + "chat_id" => "1", + "emojis" => [], + "created_at" => "2020-04-21T15:06:45.000Z" + } + ] + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/chat_response.ex b/lib/pleroma/web/api_spec/schemas/chat_response.ex new file mode 100644 index 000000000..a80f4d173 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chat_response.ex @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.ChatResponse do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ChatResponse", + description: "Response schema for a Chat", + type: :object, + properties: %{ + id: %Schema{type: :string}, + recipient: %Schema{type: :string}, + # TODO: Make this reference the account structure. + recipient_account: %Schema{type: :object}, + unread: %Schema{type: :integer} + }, + example: %{ + "recipient" => "https://dontbulling.me/users/lain", + "recipient_account" => %{ + "pleroma" => %{ + "is_admin" => false, + "confirmation_pending" => false, + "hide_followers_count" => false, + "is_moderator" => false, + "hide_favorites" => true, + "ap_id" => "https://dontbulling.me/users/lain", + "hide_follows_count" => false, + "hide_follows" => false, + "background_image" => nil, + "skip_thread_containment" => false, + "hide_followers" => false, + "relationship" => %{}, + "tags" => [] + }, + "avatar" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "following_count" => 0, + "header_static" => "https://originalpatchou.li/images/banner.png", + "source" => %{ + "sensitive" => false, + "note" => "lain", + "pleroma" => %{ + "discoverable" => false, + "actor_type" => "Person" + }, + "fields" => [] + }, + "statuses_count" => 1, + "locked" => false, + "created_at" => "2020-04-16T13:40:15.000Z", + "display_name" => "lain", + "fields" => [], + "acct" => "lain@dontbulling.me", + "id" => "9u6Qw6TAZANpqokMkK", + "emojis" => [], + "avatar_static" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "username" => "lain", + "followers_count" => 0, + "header" => "https://originalpatchou.li/images/banner.png", + "bot" => false, + "note" => "lain", + "url" => "https://dontbulling.me/users/lain" + }, + "id" => "1", + "unread" => 2 + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/chats_response.ex b/lib/pleroma/web/api_spec/schemas/chats_response.ex new file mode 100644 index 000000000..3349e0691 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chats_response.ex @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.ChatsResponse do + alias Pleroma.Web.ApiSpec.Schemas.ChatResponse + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ChatsResponse", + description: "Response schema for multiple Chats", + type: :array, + items: ChatResponse, + example: [ + %{ + "recipient" => "https://dontbulling.me/users/lain", + "recipient_account" => %{ + "pleroma" => %{ + "is_admin" => false, + "confirmation_pending" => false, + "hide_followers_count" => false, + "is_moderator" => false, + "hide_favorites" => true, + "ap_id" => "https://dontbulling.me/users/lain", + "hide_follows_count" => false, + "hide_follows" => false, + "background_image" => nil, + "skip_thread_containment" => false, + "hide_followers" => false, + "relationship" => %{}, + "tags" => [] + }, + "avatar" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "following_count" => 0, + "header_static" => "https://originalpatchou.li/images/banner.png", + "source" => %{ + "sensitive" => false, + "note" => "lain", + "pleroma" => %{ + "discoverable" => false, + "actor_type" => "Person" + }, + "fields" => [] + }, + "statuses_count" => 1, + "locked" => false, + "created_at" => "2020-04-16T13:40:15.000Z", + "display_name" => "lain", + "fields" => [], + "acct" => "lain@dontbulling.me", + "id" => "9u6Qw6TAZANpqokMkK", + "emojis" => [], + "avatar_static" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "username" => "lain", + "followers_count" => 0, + "header" => "https://originalpatchou.li/images/banner.png", + "bot" => false, + "note" => "lain", + "url" => "https://dontbulling.me/users/lain" + }, + "id" => "1", + "unread" => 2 + } + ] + }) +end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 0750c7273..52a34d23f 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -5,8 +5,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Chat + alias Pleroma.Web.ApiSpec + alias Pleroma.Web.ApiSpec.Schemas.ChatResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse alias Pleroma.Web.CommonAPI + import OpenApiSpex.TestAssertions import Pleroma.Factory describe "POST /api/v1/pleroma/chats/:id/messages" do @@ -24,6 +30,7 @@ test "it posts a message to the chat", %{conn: conn, user: user} do assert result["content"] == "Hallo!!" assert result["chat_id"] == chat.id |> to_string() + assert_schema(result, "ChatMessageResponse", ApiSpec.spec()) end end @@ -45,6 +52,7 @@ test "it paginates", %{conn: conn, user: user} do |> json_response(200) assert length(result) == 20 + assert_schema(result, "ChatMessagesResponse", ApiSpec.spec()) result = conn @@ -52,6 +60,7 @@ test "it paginates", %{conn: conn, user: user} do |> json_response(200) assert length(result) == 10 + assert_schema(result, "ChatMessagesResponse", ApiSpec.spec()) end test "it returns the messages for a given chat", %{conn: conn, user: user} do @@ -76,6 +85,7 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do end) assert length(result) == 3 + assert_schema(result, "ChatMessagesResponse", ApiSpec.spec()) # Trying to get the chat of a different user result = @@ -99,6 +109,7 @@ test "it creates or returns a chat", %{conn: conn} do |> json_response(200) assert result["id"] + assert_schema(result, "ChatResponse", ApiSpec.spec()) end end @@ -117,6 +128,7 @@ test "it paginates", %{conn: conn, user: user} do |> json_response(200) assert length(result) == 20 + assert_schema(result, "ChatsResponse", ApiSpec.spec()) result = conn @@ -124,6 +136,8 @@ test "it paginates", %{conn: conn, user: user} do |> json_response(200) assert length(result) == 10 + + assert_schema(result, "ChatsResponse", ApiSpec.spec()) end test "it return a list of chats the current user is participating in, in descending order of updates", @@ -154,6 +168,34 @@ test "it return a list of chats the current user is participating in, in descend chat_3.id |> to_string(), chat_1.id |> to_string() ] + + assert_schema(result, "ChatsResponse", ApiSpec.spec()) + end + end + + describe "schemas" do + test "Chat example matches schema" do + api_spec = ApiSpec.spec() + schema = ChatResponse.schema() + assert_schema(schema.example, "ChatResponse", api_spec) + end + + test "Chats example matches schema" do + api_spec = ApiSpec.spec() + schema = ChatsResponse.schema() + assert_schema(schema.example, "ChatsResponse", api_spec) + end + + test "ChatMessage example matches schema" do + api_spec = ApiSpec.spec() + schema = ChatMessageResponse.schema() + assert_schema(schema.example, "ChatMessageResponse", api_spec) + end + + test "ChatsMessage example matches schema" do + api_spec = ApiSpec.spec() + schema = ChatMessagesResponse.schema() + assert_schema(schema.example, "ChatMessagesResponse", api_spec) end end end From 66c2eb670b273d808f0a9c1ae087df064718ca3d Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 21 Apr 2020 18:23:00 +0200 Subject: [PATCH 036/375] ChatController: Validate parameters. --- .../web/api_spec/operations/chat_operation.ex | 4 ++++ .../controllers/chat_controller.ex | 22 ++++++++++++------- .../controllers/chat_controller_test.exs | 5 +++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 5bd41ec4f..dc99bd773 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -21,6 +21,7 @@ def create_operation do %Operation{ tags: ["chat"], summary: "Create a chat", + operationId: "ChatController.create", parameters: [ Operation.parameter( :ap_id, @@ -47,6 +48,7 @@ def index_operation do %Operation{ tags: ["chat"], summary: "Get a list of chats that you participated in", + operationId: "ChatController.index", parameters: [ Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20), Operation.parameter(:min_id, :query, :string, "Return only chats after this id"), @@ -67,6 +69,7 @@ def messages_operation do %Operation{ tags: ["chat"], summary: "Get the most recent messages of the chat", + operationId: "ChatController.messages", parameters: [ Operation.parameter(:id, :path, :string, "The ID of the Chat"), Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20), @@ -89,6 +92,7 @@ def post_chat_message_operation do %Operation{ tags: ["chat"], summary: "Post a message to the chat", + operationId: "ChatController.post_chat_message", parameters: [ Operation.parameter(:id, :path, :string, "The ID of the Chat") ], diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 9d8b9b3cf..771ad6217 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -14,6 +14,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Web.PleromaAPI.ChatMessageView alias Pleroma.Web.PleromaAPI.ChatView + import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] + import Ecto.Query # TODO @@ -29,12 +31,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do %{scopes: ["read:statuses"]} when action in [:messages, :index] ) + plug(OpenApiSpex.Plug.CastAndValidate) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation - def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ - "id" => id, - "content" => content - }) do + def post_chat_message( + %{body_params: %{content: content}, assigns: %{user: %{id: user_id} = user}} = conn, + %{ + id: id + } + ) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, content), @@ -45,7 +51,7 @@ def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ end end - def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id} = params) do + def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do messages = from(o in Object, @@ -66,7 +72,7 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id} = ^[user.ap_id] ) ) - |> Pagination.fetch_paginated(params) + |> Pagination.fetch_paginated(params |> stringify_keys()) conn |> put_view(ChatMessageView) @@ -85,7 +91,7 @@ def index(%{assigns: %{user: %{id: user_id}}} = conn, params) do where: c.user_id == ^user_id, order_by: [desc: c.updated_at] ) - |> Pagination.fetch_paginated(params) + |> Pagination.fetch_paginated(params |> stringify_keys) conn |> put_view(ChatView) @@ -93,7 +99,7 @@ def index(%{assigns: %{user: %{id: user_id}}} = conn, params) do end def create(%{assigns: %{user: user}} = conn, params) do - recipient = params["ap_id"] |> URI.decode_www_form() + recipient = params[:ap_id] with {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do conn diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 52a34d23f..84610e511 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -25,6 +25,7 @@ test "it posts a message to the chat", %{conn: conn, user: user} do result = conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) |> json_response(200) @@ -56,7 +57,7 @@ test "it paginates", %{conn: conn, user: user} do result = conn - |> get("/api/v1/pleroma/chats/#{chat.id}/messages", %{"max_id" => List.last(result)["id"]}) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") |> json_response(200) assert length(result) == 10 @@ -132,7 +133,7 @@ test "it paginates", %{conn: conn, user: user} do result = conn - |> get("/api/v1/pleroma/chats", %{max_id: List.last(result)["id"]}) + |> get("/api/v1/pleroma/chats?max_id=#{List.last(result)["id"]}") |> json_response(200) assert length(result) == 10 From 6c8390fa4d47a86c34bcc71681ba30f04d14eae9 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 21 Apr 2020 18:32:30 +0200 Subject: [PATCH 037/375] ChatControllerTest: Credo fixes. --- test/web/pleroma_api/controllers/chat_controller_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 84610e511..07b698013 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -6,10 +6,10 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do alias Pleroma.Chat alias Pleroma.Web.ApiSpec - alias Pleroma.Web.ApiSpec.Schemas.ChatResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse alias Pleroma.Web.CommonAPI import OpenApiSpex.TestAssertions From 2e62a63749e040b108b8afe2c8839c470f89fa04 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 22 Apr 2020 12:48:52 +0200 Subject: [PATCH 038/375] ChatMessageValidator: Validation changes Don't validate if the recipient is blocking the actor. --- .../object_validators/chat_message_validator.ex | 12 +++++++++++- test/web/activity_pub/object_validator_test.exs | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 2feb65f29..8b5bb4fdc 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -61,15 +61,25 @@ def validate_data(data_cng) do |> validate_local_concern() end - @doc "Validates if at least one of the users in this ChatMessage is a local user, otherwise we don't want the message in our system. It also validates the presence of both users in our system." + @doc """ + Validates the following + - If both users are in our system + - If at least one of the users in this ChatMessage is a local user + - If the recipient is not blocking the actor + """ def validate_local_concern(cng) do with actor_ap <- get_field(cng, :actor), {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, {_, %User{} = recipient} <- {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, + {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)}, {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do cng else + {:blocking_actor?, true} -> + cng + |> add_error(:actor, "actor is blocked by recipient") + {:local?, false} -> cng |> add_error(:actor, "actor and recipient are both remote") diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 8230ae0d9..bc2317e55 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -33,6 +33,15 @@ test "does not validate if the message is longer than the remote_limit", %{ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) end + test "does not validate if the recipient is blocking the actor", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + Pleroma.User.block(recipient, user) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + test "does not validate if the actor or the recipient is not in our system", %{ valid_chat_message: valid_chat_message } do From 1d6338f2d38284e94e17be58c21c7f34b5621ab7 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 22 Apr 2020 12:52:39 +0200 Subject: [PATCH 039/375] Litepub: Add ChatMessage. --- priv/static/schemas/litepub-0.1.jsonld | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 278ad2f96..7cc3fee40 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -30,6 +30,7 @@ "@type": "@id" }, "EmojiReact": "litepub:EmojiReact", + "ChatMessage": "litepub:ChatMessage", "alsoKnownAs": { "@id": "as:alsoKnownAs", "@type": "@id" From 1e28d34592a5fae0f3403763f1ff86cc393a52b0 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 23 Apr 2020 16:19:49 +0200 Subject: [PATCH 040/375] ChatMessage: Correctly ingest emoji tags. --- .../object_validators/chat_message_validator.ex | 2 ++ test/fixtures/create-chat-message.json | 12 ++++++++++++ .../transmogrifier/chat_message_test.exs | 1 + 3 files changed, 15 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 8b5bb4fdc..f07045d9d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset + import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] @primary_key false @derive Jason.Encoder @@ -42,6 +43,7 @@ def cast_data(data) do def fix(data) do data + |> fix_emoji() |> Map.put_new("actor", data["attributedTo"]) end diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json index 6db5b9f5c..9c23a1c9b 100644 --- a/test/fixtures/create-chat-message.json +++ b/test/fixtures/create-chat-message.json @@ -9,6 +9,18 @@ "to": [ "http://2hu.gensokyo/users/marisa" ], + "tag": [ + { + "icon": { + "type": "Image", + "url": "http://2hu.gensokyo/emoji/Firefox.gif" + }, + "id": "http://2hu.gensokyo/emoji/Firefox.gif", + "name": ":firefox:", + "type": "Emoji", + "updated": "1970-01-01T00:00:00Z" + } + ], "type": "ChatMessage" }, "published": "2018-02-12T14:08:20Z", diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index 4d6f24609..a63a31e6e 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -75,6 +75,7 @@ test "it inserts it and creates a chat" do assert object assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')" + assert match?(%{"firefox" => _}, object.data["emoji"]) refute Chat.get(author.id, recipient.ap_id) assert Chat.get(recipient.id, author.ap_id) From a51cdafc0192b66ce75659b424a690f52c9b2a49 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 23 Apr 2020 16:55:00 +0200 Subject: [PATCH 041/375] Docs: Add documentation about chatmessages --- docs/ap_extensions.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/ap_extensions.md diff --git a/docs/ap_extensions.md b/docs/ap_extensions.md new file mode 100644 index 000000000..c4550a1ac --- /dev/null +++ b/docs/ap_extensions.md @@ -0,0 +1,35 @@ +# ChatMessages + +ChatMessages are the messages sent in 1-on-1 chats. They are similar to +`Note`s, but the addresing is done by having a single AP actor in the `to` +field. Addressing multiple actors is not allowed. These messages are always +private, there is no public version of them. They are created with a `Create` +activity. + +Example: + +```json +{ + "actor": "http://2hu.gensokyo/users/raymoo", + "id": "http://2hu.gensokyo/objects/1", + "object": { + "attributedTo": "http://2hu.gensokyo/users/raymoo", + "content": "You expected a cute girl? Too bad.", + "id": "http://2hu.gensokyo/objects/2", + "published": "2020-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "ChatMessage" + }, + "published": "2018-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "Create" +} +``` + +This setup does not prevent multi-user chats, but these will have to go through +a `Group`, which will be the recipient of the messages and then `Announce` them +to the users in the `Group`. From b429a49504b1df6fa085cccbb3e461cd378b15c4 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 23 Apr 2020 23:44:03 +0200 Subject: [PATCH 042/375] mix.exs: Fix for MacOS --- mix.exs | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/mix.exs b/mix.exs index b76aef180..021217400 100644 --- a/mix.exs +++ b/mix.exs @@ -220,32 +220,39 @@ defp aliases do defp version(version) do identifier_filter = ~r/[^0-9a-z\-]+/i - # Pre-release version, denoted from patch version with a hyphen - {tag, tag_err} = - System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true) - - {describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"]) - {commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) + {_gitpath, git_present} = System.cmd("sh", ["-c", "command -v git"]) git_pre_release = - cond do - tag_err == 0 and describe_err == 0 -> - describe - |> String.trim() - |> String.replace(String.trim(tag), "") - |> String.trim_leading("-") - |> String.trim() + if git_present do + {tag, tag_err} = + System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true) - commit_hash_err == 0 -> - "0-g" <> String.trim(commit_hash) + {describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"]) + {commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) - true -> - "" + # Pre-release version, denoted from patch version with a hyphen + cond do + tag_err == 0 and describe_err == 0 -> + describe + |> String.trim() + |> String.replace(String.trim(tag), "") + |> String.trim_leading("-") + |> String.trim() + + commit_hash_err == 0 -> + "0-g" <> String.trim(commit_hash) + + true -> + "" + end + else + "" end # Branch name as pre-release version component, denoted with a dot branch_name = - with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), + with true <- git_present, + {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), branch_name <- String.trim(branch_name), branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name, true <- From c63d6ba0b2686db847f70cf251f92bfed57c4e5f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 23 Apr 2020 23:44:30 +0200 Subject: [PATCH 043/375] mix.exs: branch_name fallbacks to "" --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 021217400..15a65c0fb 100644 --- a/mix.exs +++ b/mix.exs @@ -266,7 +266,7 @@ defp version(version) do branch_name else - _ -> "stable" + _ -> "" end build_name = From 053c46153076f351c5273c2d6b1fb0843e7b6a6d Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 24 Apr 2020 00:26:24 +0200 Subject: [PATCH 044/375] mix.exs: proper check on 0, remove else in git_pre_release --- mix.exs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mix.exs b/mix.exs index 15a65c0fb..ebb8bdb08 100644 --- a/mix.exs +++ b/mix.exs @@ -220,10 +220,10 @@ defp aliases do defp version(version) do identifier_filter = ~r/[^0-9a-z\-]+/i - {_gitpath, git_present} = System.cmd("sh", ["-c", "command -v git"]) + {_cmdgit, cmdgit_err} = System.cmd("sh", ["-c", "command -v git"]) git_pre_release = - if git_present do + if cmdgit_err == 0 do {tag, tag_err} = System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true) @@ -243,15 +243,13 @@ defp version(version) do "0-g" <> String.trim(commit_hash) true -> - "" + nil end - else - "" end # Branch name as pre-release version component, denoted with a dot branch_name = - with true <- git_present, + with 0 <- cmdgit_err, {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), branch_name <- String.trim(branch_name), branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name, From d2bbea1a8076401645600ceb953dd66ec023b3ad Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 12:19:27 +0200 Subject: [PATCH 045/375] ChatControllerTest: Use new schema testing functions. --- .../controllers/chat_controller_test.exs | 58 +++---------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 07b698013..84d7b543e 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -5,14 +5,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Chat - alias Pleroma.Web.ApiSpec - alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse alias Pleroma.Web.CommonAPI - import OpenApiSpex.TestAssertions import Pleroma.Factory describe "POST /api/v1/pleroma/chats/:id/messages" do @@ -27,11 +21,10 @@ test "it posts a message to the chat", %{conn: conn, user: user} do conn |> put_req_header("content-type", "application/json") |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) - |> json_response(200) + |> json_response_and_validate_schema(200) assert result["content"] == "Hallo!!" assert result["chat_id"] == chat.id |> to_string() - assert_schema(result, "ChatMessageResponse", ApiSpec.spec()) end end @@ -50,18 +43,16 @@ test "it paginates", %{conn: conn, user: user} do result = conn |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response(200) + |> json_response_and_validate_schema(200) assert length(result) == 20 - assert_schema(result, "ChatMessagesResponse", ApiSpec.spec()) result = conn |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") - |> json_response(200) + |> json_response_and_validate_schema(200) assert length(result) == 10 - assert_schema(result, "ChatMessagesResponse", ApiSpec.spec()) end test "it returns the messages for a given chat", %{conn: conn, user: user} do @@ -78,7 +69,7 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do result = conn |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response(200) + |> json_response_and_validate_schema(200) result |> Enum.each(fn message -> @@ -86,7 +77,6 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do end) assert length(result) == 3 - assert_schema(result, "ChatMessagesResponse", ApiSpec.spec()) # Trying to get the chat of a different user result = @@ -107,10 +97,9 @@ test "it creates or returns a chat", %{conn: conn} do result = conn |> post("/api/v1/pleroma/chats/by-ap-id/#{URI.encode_www_form(other_user.ap_id)}") - |> json_response(200) + |> json_response_and_validate_schema(200) assert result["id"] - assert_schema(result, "ChatResponse", ApiSpec.spec()) end end @@ -126,19 +115,16 @@ test "it paginates", %{conn: conn, user: user} do result = conn |> get("/api/v1/pleroma/chats") - |> json_response(200) + |> json_response_and_validate_schema(200) assert length(result) == 20 - assert_schema(result, "ChatsResponse", ApiSpec.spec()) result = conn |> get("/api/v1/pleroma/chats?max_id=#{List.last(result)["id"]}") - |> json_response(200) + |> json_response_and_validate_schema(200) assert length(result) == 10 - - assert_schema(result, "ChatsResponse", ApiSpec.spec()) end test "it return a list of chats the current user is participating in, in descending order of updates", @@ -160,7 +146,7 @@ test "it return a list of chats the current user is participating in, in descend result = conn |> get("/api/v1/pleroma/chats") - |> json_response(200) + |> json_response_and_validate_schema(200) ids = Enum.map(result, & &1["id"]) @@ -169,34 +155,6 @@ test "it return a list of chats the current user is participating in, in descend chat_3.id |> to_string(), chat_1.id |> to_string() ] - - assert_schema(result, "ChatsResponse", ApiSpec.spec()) - end - end - - describe "schemas" do - test "Chat example matches schema" do - api_spec = ApiSpec.spec() - schema = ChatResponse.schema() - assert_schema(schema.example, "ChatResponse", api_spec) - end - - test "Chats example matches schema" do - api_spec = ApiSpec.spec() - schema = ChatsResponse.schema() - assert_schema(schema.example, "ChatsResponse", api_spec) - end - - test "ChatMessage example matches schema" do - api_spec = ApiSpec.spec() - schema = ChatMessageResponse.schema() - assert_schema(schema.example, "ChatMessageResponse", api_spec) - end - - test "ChatsMessage example matches schema" do - api_spec = ApiSpec.spec() - schema = ChatMessagesResponse.schema() - assert_schema(schema.example, "ChatMessagesResponse", api_spec) end end end From 15ba3700af76c44e63bf8881021f3ee2a5a7dafd Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 12:45:59 +0200 Subject: [PATCH 046/375] Chat Schemas: Inline unimportant Schemas. --- .../web/api_spec/operations/chat_operation.ex | 113 +++++++++++++++++- .../schemas/chat_messages_response.ex | 41 ------- .../web/api_spec/schemas/chats_response.ex | 69 ----------- 3 files changed, 108 insertions(+), 115 deletions(-) delete mode 100644 lib/pleroma/web/api_spec/schemas/chat_messages_response.ex delete mode 100644 lib/pleroma/web/api_spec/schemas/chats_response.ex diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index dc99bd773..6f55cbd59 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -7,9 +7,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse alias Pleroma.Web.ApiSpec.Schemas.ChatResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse + alias OpenApiSpex.Schema @spec open_api_operation(atom) :: Operation.t() def open_api_operation(action) do @@ -34,7 +33,11 @@ def create_operation do ], responses: %{ 200 => - Operation.response("The created or existing chat", "application/json", ChatResponse) + Operation.response( + "The created or existing chat", + "application/json", + ChatResponse + ) }, security: [ %{ @@ -55,7 +58,7 @@ def index_operation do Operation.parameter(:max_id, :query, :string, "Return only chats before this id") ], responses: %{ - 200 => Operation.response("The chats of the user", "application/json", ChatsResponse) + 200 => Operation.response("The chats of the user", "application/json", chats_response()) }, security: [ %{ @@ -78,7 +81,11 @@ def messages_operation do ], responses: %{ 200 => - Operation.response("The messages in the chat", "application/json", ChatMessagesResponse) + Operation.response( + "The messages in the chat", + "application/json", + chat_messages_response() + ) }, security: [ %{ @@ -112,4 +119,100 @@ def post_chat_message_operation do ] } end + + def chats_response() do + %Schema{ + title: "ChatsResponse", + description: "Response schema for multiple Chats", + type: :array, + items: ChatResponse, + example: [ + %{ + "recipient" => "https://dontbulling.me/users/lain", + "recipient_account" => %{ + "pleroma" => %{ + "is_admin" => false, + "confirmation_pending" => false, + "hide_followers_count" => false, + "is_moderator" => false, + "hide_favorites" => true, + "ap_id" => "https://dontbulling.me/users/lain", + "hide_follows_count" => false, + "hide_follows" => false, + "background_image" => nil, + "skip_thread_containment" => false, + "hide_followers" => false, + "relationship" => %{}, + "tags" => [] + }, + "avatar" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "following_count" => 0, + "header_static" => "https://originalpatchou.li/images/banner.png", + "source" => %{ + "sensitive" => false, + "note" => "lain", + "pleroma" => %{ + "discoverable" => false, + "actor_type" => "Person" + }, + "fields" => [] + }, + "statuses_count" => 1, + "locked" => false, + "created_at" => "2020-04-16T13:40:15.000Z", + "display_name" => "lain", + "fields" => [], + "acct" => "lain@dontbulling.me", + "id" => "9u6Qw6TAZANpqokMkK", + "emojis" => [], + "avatar_static" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "username" => "lain", + "followers_count" => 0, + "header" => "https://originalpatchou.li/images/banner.png", + "bot" => false, + "note" => "lain", + "url" => "https://dontbulling.me/users/lain" + }, + "id" => "1", + "unread" => 2 + } + ] + } + end + + def chat_messages_response() do + %Schema{ + title: "ChatMessagesResponse", + description: "Response schema for multiple ChatMessages", + type: :array, + items: ChatMessageResponse, + example: [ + %{ + "emojis" => [ + %{ + "static_url" => "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker" => false, + "shortcode" => "firefox", + "url" => "https://dontbulling.me/emoji/Firefox.gif" + } + ], + "created_at" => "2020-04-21T15:11:46.000Z", + "content" => "Check this out :firefox:", + "id" => "13", + "chat_id" => "1", + "actor" => "https://dontbulling.me/users/lain" + }, + %{ + "actor" => "https://dontbulling.me/users/lain", + "content" => "Whats' up?", + "id" => "12", + "chat_id" => "1", + "emojis" => [], + "created_at" => "2020-04-21T15:06:45.000Z" + } + ] + } + end end diff --git a/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex b/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex deleted file mode 100644 index 302bdec95..000000000 --- a/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse do - alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse - - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "ChatMessagesResponse", - description: "Response schema for multiple ChatMessages", - type: :array, - items: ChatMessageResponse, - example: [ - %{ - "emojis" => [ - %{ - "static_url" => "https://dontbulling.me/emoji/Firefox.gif", - "visible_in_picker" => false, - "shortcode" => "firefox", - "url" => "https://dontbulling.me/emoji/Firefox.gif" - } - ], - "created_at" => "2020-04-21T15:11:46.000Z", - "content" => "Check this out :firefox:", - "id" => "13", - "chat_id" => "1", - "actor" => "https://dontbulling.me/users/lain" - }, - %{ - "actor" => "https://dontbulling.me/users/lain", - "content" => "Whats' up?", - "id" => "12", - "chat_id" => "1", - "emojis" => [], - "created_at" => "2020-04-21T15:06:45.000Z" - } - ] - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/chats_response.ex b/lib/pleroma/web/api_spec/schemas/chats_response.ex deleted file mode 100644 index 3349e0691..000000000 --- a/lib/pleroma/web/api_spec/schemas/chats_response.ex +++ /dev/null @@ -1,69 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.ChatsResponse do - alias Pleroma.Web.ApiSpec.Schemas.ChatResponse - - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "ChatsResponse", - description: "Response schema for multiple Chats", - type: :array, - items: ChatResponse, - example: [ - %{ - "recipient" => "https://dontbulling.me/users/lain", - "recipient_account" => %{ - "pleroma" => %{ - "is_admin" => false, - "confirmation_pending" => false, - "hide_followers_count" => false, - "is_moderator" => false, - "hide_favorites" => true, - "ap_id" => "https://dontbulling.me/users/lain", - "hide_follows_count" => false, - "hide_follows" => false, - "background_image" => nil, - "skip_thread_containment" => false, - "hide_followers" => false, - "relationship" => %{}, - "tags" => [] - }, - "avatar" => - "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", - "following_count" => 0, - "header_static" => "https://originalpatchou.li/images/banner.png", - "source" => %{ - "sensitive" => false, - "note" => "lain", - "pleroma" => %{ - "discoverable" => false, - "actor_type" => "Person" - }, - "fields" => [] - }, - "statuses_count" => 1, - "locked" => false, - "created_at" => "2020-04-16T13:40:15.000Z", - "display_name" => "lain", - "fields" => [], - "acct" => "lain@dontbulling.me", - "id" => "9u6Qw6TAZANpqokMkK", - "emojis" => [], - "avatar_static" => - "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", - "username" => "lain", - "followers_count" => 0, - "header" => "https://originalpatchou.li/images/banner.png", - "bot" => false, - "note" => "lain", - "url" => "https://dontbulling.me/users/lain" - }, - "id" => "1", - "unread" => 2 - } - ] - }) -end From e62f8542a1933ba71dfd236741ad3afc76b89f22 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 13:48:09 +0200 Subject: [PATCH 047/375] Docs: Add chat motivation and api description. --- docs/API/chats.md | 197 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 docs/API/chats.md diff --git a/docs/API/chats.md b/docs/API/chats.md new file mode 100644 index 000000000..39f493b47 --- /dev/null +++ b/docs/API/chats.md @@ -0,0 +1,197 @@ +# Chats + +Chats are a way to represent an IM-style conversation between two actors. They are not the same as direct messages and they are not `Status`es, even though they have a lot in common. + +## Why Chats? + +There are no 'visibility levels' in ActivityPub, their definition is purely a Mastodon convention. Direct Messaging between users on the fediverse has mostly been modeled by using ActivityPub addressing following Mastodon conventions on normal `Note` objects. In this case, a 'direct message' would be a message that has no followers addressed and also does not address the special public actor, but just the recipients in the `to` field. It would still be a `Note` and is presented with other `Note`s as a `Status` in the API. + +This is an awkward setup for a few reasons: + +- As DMs generally still follow the usual `Status` conventions, it is easy to accidentally pull somebody into a DM thread by mentioning them. (e.g. "I hate @badguy so much") +- It is possible to go from a publicly addressed `Status` to a DM reply, back to public, then to a 'followers only' reply, and so on. This can be become very confusing, as it is unclear which user can see which part of the conversation. +- The standard `Status` format of implicit addressing also leads to rather ugly results if you try to display the messages as a chat, because all the recipients are always mentioned by name in the message. +- As direct messages are posted with the same api call (and usually same frontend component) as public messages, accidentally making a public message private or vice versa can happen easily. Client bugs can also lead to this, accidentally making private messages public. + +As a measure to improve this situation, the `Conversation` concept and related Pleroma extensions were introduced. While it made it possible to work around a few of the issues, many of the problems remained and it didn't see much adoption because it was too complicated to use correctly. + +## Chats explained +For this reasons, Chats are a new and different entity, both in the API as well as in ActivityPub. A quick overview: + +- Chats are meant to represent an instant message conversation between two actors. For now these are only 1-on-1 conversations, but the other actor can be a group in the future. +- Chat messages have the ActivityPub type `ChatMessage`. They are not `Note`s. Servers that don't understand them will just drop them. +- The only addressing allowed in `ChatMessage`s is one single ActivityPub actor in the `to` field. +- There's always only one Chat between two actors. If you start chatting with someone and later start a 'new' Chat, the old Chat will be continued. +- `ChatMessage`s are posted with a different api, making it very hard to accidentally send a message to the wrong person. +- `ChatMessage`s don't show up in the existing timelines. +- Chats can never go from private to public. They are always private between the two actors. + +## Caveats + +- Chats are NOT E2E encrypted (yet). Security is still the same as email. + +## API + +In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. The actors in the API are generally given by their ActivityPub id to make it easier to support later `Group` scenarios. + +This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`. + +### Creating or getting a chat. + +To create or get an existing Chat for a certain recipient (identified by AP ID) +you can call: + +`POST /api/v1/pleroma/chats/by-ap-id/{ap_id}` + +The ap_id of the recipients needs to be www-form encoded, so + +``` +https://originalpatchou.li/user/lambda +``` + +would become + +``` +https%3A%2F%2Foriginalpatchou.li%2Fuser%2Flambda +``` + +The full call would then be + +``` +POST /api/v1/pleroma/chats/by-ap-id/https%3A%2F%2Foriginalpatchou.li%2Fuser%2Flambda +``` + +There will only ever be ONE Chat for you and a given recipient, so this call +will return the same Chat if you already have one with that user. + +Returned data: + +```json +{ + "recipient" : "https://dontbulling.me/users/lain", + "recipient_account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 2 +} +``` + +### Getting a list of Chats + +`GET /api/v1/pleroma/chats` + +This will return a list of chats that you have been involved in, sorted by their +last update (so new chats will be at the top). + +Returned data: + +```json +[ + { + "recipient" : "https://dontbulling.me/users/lain", + "recipient_account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 2 + } +] +``` + +The recipient of messages that are sent to this chat is given by their AP ID. +The usual pagination options are implemented. + +### Getting the messages for a Chat + +For a given Chat id, you can get the associated messages with + +`GET /api/v1/pleroma/chats/{id}/messages` + +This will return all messages, sorted by most recent to least recent. The usual +pagination options are implemented + +Returned data: + +```json +[ + { + "actor": "https://dontbulling.me/users/lain", + "chat_id": "1", + "content": "Check this out :firefox:", + "created_at": "2020-04-21T15:11:46.000Z", + "emojis": [ + { + "shortcode": "firefox", + "static_url": "https://dontbulling.me/emoji/Firefox.gif", + "url": "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker": false + } + ], + "id": "13" + }, + { + "actor": "https://dontbulling.me/users/lain", + "chat_id": "1", + "content": "Whats' up?", + "created_at": "2020-04-21T15:06:45.000Z", + "emojis": [], + "id": "12" + } +] +``` + +### Posting a chat message + +Posting a chat message for given Chat id works like this: + +`POST /api/v1/pleroma/chats/{id}/messages` + +Parameters: +- content: The text content of the message + +Currently, no formatting beyond basic escaping and emoji is implemented, as well as no +attachments. This will most probably change. + +Returned data: + +```json +{ + "actor": "https://dontbulling.me/users/lain", + "chat_id": "1", + "content": "Check this out :firefox:", + "created_at": "2020-04-21T15:11:46.000Z", + "emojis": [ + { + "shortcode": "firefox", + "static_url": "https://dontbulling.me/emoji/Firefox.gif", + "url": "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker": false + } + ], + "id": "13" +} +``` + +### Notifications + +There's a new `pleroma:chat_mention` notification, which has this form: + +```json +{ + "id": "someid", + "type": "pleroma:chat_mention", + "account": { ... } // User account of the sender, + "chat_message": { + "chat_id": "1", + "id": "10", + "content": "Hello", + "actor": "https://dontbulling.me/users/lain" + }, + "created_at": "somedate" +} +``` From 00e956528b392689326d5f5527543a422a874bcc Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 14:02:11 +0200 Subject: [PATCH 048/375] Credo fixes. --- lib/pleroma/web/api_spec/operations/chat_operation.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 6f55cbd59..546bc4d9b 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -4,11 +4,11 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Operation + alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse alias Pleroma.Web.ApiSpec.Schemas.ChatResponse - alias OpenApiSpex.Schema @spec open_api_operation(atom) :: Operation.t() def open_api_operation(action) do @@ -120,7 +120,7 @@ def post_chat_message_operation do } end - def chats_response() do + def chats_response do %Schema{ title: "ChatsResponse", description: "Response schema for multiple Chats", @@ -182,7 +182,7 @@ def chats_response() do } end - def chat_messages_response() do + def chat_messages_response do %Schema{ title: "ChatMessagesResponse", description: "Response schema for multiple ChatMessages", From 49e673dfea0a0cc94bba9691ce171b60f8a2fd75 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 16:08:03 +0200 Subject: [PATCH 049/375] ChatView: Add actor_account_id --- lib/pleroma/web/api_spec/schemas/chat_message_response.ex | 2 ++ lib/pleroma/web/pleroma_api/views/chat_message_view.ex | 2 ++ test/web/pleroma_api/views/chat_message_view_test.exs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex index e94c00369..9459d210b 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do properties: %{ id: %Schema{type: :string}, actor: %Schema{type: :string, description: "The ActivityPub id of the actor"}, + actor_account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"}, chat_id: %Schema{type: :string}, content: %Schema{type: :string}, created_at: %Schema{type: :string, format: :datetime}, @@ -21,6 +22,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do }, example: %{ "actor" => "https://dontbulling.me/users/lain", + "actor_account_id" => "someflakeid", "chat_id" => "1", "content" => "hey you again", "created_at" => "2020-04-21T15:06:45.000Z", diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index b40ab92a0..5b740cc44 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageView do alias Pleroma.Chat alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.User def render( "show.json", @@ -21,6 +22,7 @@ def render( content: chat_message["content"], chat_id: chat_id |> to_string(), actor: chat_message["actor"], + actor_account_id: User.get_cached_by_ap_id(chat_message["actor"]).id, created_at: Utils.to_masto_date(chat_message["published"]), emojis: StatusView.build_emojis(chat_message["emoji"]) } diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index 115335f10..7e3aeefab 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -26,6 +26,7 @@ test "it displays a chat message" do assert chat_message[:id] == object.id |> to_string() assert chat_message[:content] == "kippis :firefox:" assert chat_message[:actor] == user.ap_id + assert chat_message[:actor_account_id] == user.id assert chat_message[:chat_id] assert chat_message[:created_at] assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) @@ -39,6 +40,7 @@ test "it displays a chat message" do assert chat_message_two[:id] == object.id |> to_string() assert chat_message_two[:content] == "gkgkgk" assert chat_message_two[:actor] == recipient.ap_id + assert chat_message_two[:actor_account_id] == recipient.id assert chat_message_two[:chat_id] == chat_message[:chat_id] end end From ad82a216ff0676507a118e610209bd4259456b3c Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 17:48:34 +0200 Subject: [PATCH 050/375] Chat API: Align more to Pleroma/Mastodon API. --- .../web/api_spec/operations/chat_operation.ex | 13 ++++++------- .../web/api_spec/schemas/chat_message_response.ex | 6 ++---- lib/pleroma/web/api_spec/schemas/chat_response.ex | 11 ++++------- .../web/pleroma_api/controllers/chat_controller.ex | 5 ++--- .../web/pleroma_api/views/chat_message_view.ex | 3 +-- lib/pleroma/web/pleroma_api/views/chat_view.ex | 3 +-- lib/pleroma/web/router.ex | 2 +- .../controllers/chat_controller_test.exs | 4 ++-- .../pleroma_api/views/chat_message_view_test.exs | 6 ++---- test/web/pleroma_api/views/chat_view_test.exs | 3 +-- 10 files changed, 22 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 546bc4d9b..59539e890 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -23,12 +23,12 @@ def create_operation do operationId: "ChatController.create", parameters: [ Operation.parameter( - :ap_id, + :id, :path, :string, - "The ActivityPub id of the recipient of this chat.", + "The account id of the recipient of this chat", required: true, - example: "https://lain.com/users/lain" + example: "someflakeid" ) ], responses: %{ @@ -128,8 +128,7 @@ def chats_response do items: ChatResponse, example: [ %{ - "recipient" => "https://dontbulling.me/users/lain", - "recipient_account" => %{ + "account" => %{ "pleroma" => %{ "is_admin" => false, "confirmation_pending" => false, @@ -202,10 +201,10 @@ def chat_messages_response do "content" => "Check this out :firefox:", "id" => "13", "chat_id" => "1", - "actor" => "https://dontbulling.me/users/lain" + "actor_id" => "someflakeid" }, %{ - "actor" => "https://dontbulling.me/users/lain", + "actor_id" => "someflakeid", "content" => "Whats' up?", "id" => "12", "chat_id" => "1", diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex index 9459d210b..b7a662cbb 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex @@ -13,16 +13,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do type: :object, properties: %{ id: %Schema{type: :string}, - actor: %Schema{type: :string, description: "The ActivityPub id of the actor"}, - actor_account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"}, + account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"}, chat_id: %Schema{type: :string}, content: %Schema{type: :string}, created_at: %Schema{type: :string, format: :datetime}, emojis: %Schema{type: :array} }, example: %{ - "actor" => "https://dontbulling.me/users/lain", - "actor_account_id" => "someflakeid", + "account_id" => "someflakeid", "chat_id" => "1", "content" => "hey you again", "created_at" => "2020-04-21T15:06:45.000Z", diff --git a/lib/pleroma/web/api_spec/schemas/chat_response.ex b/lib/pleroma/web/api_spec/schemas/chat_response.ex index a80f4d173..aa435165d 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_response.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_response.ex @@ -12,15 +12,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatResponse do description: "Response schema for a Chat", type: :object, properties: %{ - id: %Schema{type: :string}, - recipient: %Schema{type: :string}, - # TODO: Make this reference the account structure. - recipient_account: %Schema{type: :object}, - unread: %Schema{type: :integer} + id: %Schema{type: :string, nullable: false}, + account: %Schema{type: :object, nullable: false}, + unread: %Schema{type: :integer, nullable: false} }, example: %{ - "recipient" => "https://dontbulling.me/users/lain", - "recipient_account" => %{ + "account" => %{ "pleroma" => %{ "is_admin" => false, "confirmation_pending" => false, diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 771ad6217..8654f4295 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -99,9 +99,8 @@ def index(%{assigns: %{user: %{id: user_id}}} = conn, params) do end def create(%{assigns: %{user: user}} = conn, params) do - recipient = params[:ap_id] - - with {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do + with %User{ap_id: recipient} <- User.get_by_id(params[:id]), + {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do conn |> put_view(ChatView) |> render("show.json", chat: chat) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index 5b740cc44..28f12d9b0 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -21,8 +21,7 @@ def render( id: id |> to_string(), content: chat_message["content"], chat_id: chat_id |> to_string(), - actor: chat_message["actor"], - actor_account_id: User.get_cached_by_ap_id(chat_message["actor"]).id, + account_id: User.get_cached_by_ap_id(chat_message["actor"]).id, created_at: Utils.to_masto_date(chat_message["published"]), emojis: StatusView.build_emojis(chat_message["emoji"]) } diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 1e9ef4356..bc3af5ef5 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -14,8 +14,7 @@ def render("show.json", %{chat: %Chat{} = chat} = opts) do %{ id: chat.id |> to_string(), - recipient: chat.recipient, - recipient_account: AccountView.render("show.json", Map.put(opts, :user, recipient)), + account: AccountView.render("show.json", Map.put(opts, :user, recipient)), unread: chat.unread } end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 0c56318ee..aad2e3b98 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -275,7 +275,7 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:authenticated_api) - post("/chats/by-ap-id/:ap_id", ChatController, :create) + post("/chats/by-account-id/:id", ChatController, :create) get("/chats", ChatController, :index) get("/chats/:id/messages", ChatController, :messages) post("/chats/:id/messages", ChatController, :post_chat_message) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 84d7b543e..b1044574b 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -88,7 +88,7 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do end end - describe "POST /api/v1/pleroma/chats/by-ap-id/:id" do + describe "POST /api/v1/pleroma/chats/by-account-id/:id" do setup do: oauth_access(["write:statuses"]) test "it creates or returns a chat", %{conn: conn} do @@ -96,7 +96,7 @@ test "it creates or returns a chat", %{conn: conn} do result = conn - |> post("/api/v1/pleroma/chats/by-ap-id/#{URI.encode_www_form(other_user.ap_id)}") + |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}") |> json_response_and_validate_schema(200) assert result["id"] diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index 7e3aeefab..5c4c8b0d5 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -25,8 +25,7 @@ test "it displays a chat message" do assert chat_message[:id] == object.id |> to_string() assert chat_message[:content] == "kippis :firefox:" - assert chat_message[:actor] == user.ap_id - assert chat_message[:actor_account_id] == user.id + assert chat_message[:account_id] == user.id assert chat_message[:chat_id] assert chat_message[:created_at] assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) @@ -39,8 +38,7 @@ test "it displays a chat message" do assert chat_message_two[:id] == object.id |> to_string() assert chat_message_two[:content] == "gkgkgk" - assert chat_message_two[:actor] == recipient.ap_id - assert chat_message_two[:actor_account_id] == recipient.id + assert chat_message_two[:account_id] == recipient.id assert chat_message_two[:chat_id] == chat_message[:chat_id] end end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index 725da5ff8..1ac3483d1 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -21,8 +21,7 @@ test "it represents a chat" do assert represented_chat == %{ id: "#{chat.id}", - recipient: recipient.ap_id, - recipient_account: AccountView.render("show.json", user: recipient), + account: AccountView.render("show.json", user: recipient), unread: 0 } end From b550ef56119b9f735cf3fe279a5457e36ab92951 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 17:52:16 +0200 Subject: [PATCH 051/375] Docs: Align chat api changes with docs. --- docs/API/chats.md | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 39f493b47..24c4b4d06 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -32,33 +32,21 @@ For this reasons, Chats are a new and different entity, both in the API as well ## API -In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. The actors in the API are generally given by their ActivityPub id to make it easier to support later `Group` scenarios. +In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. `Group`s will later be supported by making them a sub-type of `Account`. This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`. ### Creating or getting a chat. -To create or get an existing Chat for a certain recipient (identified by AP ID) +To create or get an existing Chat for a certain recipient (identified by Account ID) you can call: -`POST /api/v1/pleroma/chats/by-ap-id/{ap_id}` +`POST /api/v1/pleroma/chats/by-account-id/{account_id}` -The ap_id of the recipients needs to be www-form encoded, so +The account id is the normal FlakeId of the usre ``` -https://originalpatchou.li/user/lambda -``` - -would become - -``` -https%3A%2F%2Foriginalpatchou.li%2Fuser%2Flambda -``` - -The full call would then be - -``` -POST /api/v1/pleroma/chats/by-ap-id/https%3A%2F%2Foriginalpatchou.li%2Fuser%2Flambda +POST /api/v1/pleroma/chats/by-account-id/someflakeid ``` There will only ever be ONE Chat for you and a given recipient, so this call @@ -68,8 +56,7 @@ Returned data: ```json { - "recipient" : "https://dontbulling.me/users/lain", - "recipient_account": { + "account": { "id": "someflakeid", "username": "somenick", ... @@ -91,8 +78,7 @@ Returned data: ```json [ { - "recipient" : "https://dontbulling.me/users/lain", - "recipient_account": { + "account": { "id": "someflakeid", "username": "somenick", ... @@ -120,7 +106,7 @@ Returned data: ```json [ { - "actor": "https://dontbulling.me/users/lain", + "account_id": "someflakeid", "chat_id": "1", "content": "Check this out :firefox:", "created_at": "2020-04-21T15:11:46.000Z", @@ -135,7 +121,7 @@ Returned data: "id": "13" }, { - "actor": "https://dontbulling.me/users/lain", + "account_id": "someflakeid", "chat_id": "1", "content": "Whats' up?", "created_at": "2020-04-21T15:06:45.000Z", @@ -161,7 +147,7 @@ Returned data: ```json { - "actor": "https://dontbulling.me/users/lain", + "account_id": "someflakeid", "chat_id": "1", "content": "Check this out :firefox:", "created_at": "2020-04-21T15:11:46.000Z", @@ -190,7 +176,7 @@ There's a new `pleroma:chat_mention` notification, which has this form: "chat_id": "1", "id": "10", "content": "Hello", - "actor": "https://dontbulling.me/users/lain" + "account_id": "someflakeid" }, "created_at": "somedate" } From 3d040b1a87da66ed53a763f781477bd4f5a146d3 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 27 Apr 2020 17:55:29 +0200 Subject: [PATCH 052/375] Credo fixes. --- lib/pleroma/web/pleroma_api/views/chat_message_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index 28f12d9b0..a821479ab 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -6,9 +6,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageView do use Pleroma.Web, :view alias Pleroma.Chat + alias Pleroma.User alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.User def render( "show.json", From 906cf53ab94742327d073f56255f695c91339295 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Apr 2020 13:38:02 +0200 Subject: [PATCH 053/375] Recipient Type: Cast all elements as ObjectIDs. --- .../object_validators/types/recipients.ex | 15 +++++++++++++-- .../object_validators/types/recipients_test.exs | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex index 5a3040842..48fe61e1a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex +++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex @@ -1,13 +1,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do use Ecto.Type - def type, do: {:array, :string} + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID + + def type, do: {:array, ObjectID} def cast(object) when is_binary(object) do cast([object]) end - def cast([_ | _] = data), do: {:ok, data} + def cast(data) when is_list(data) do + data + |> Enum.reduce({:ok, []}, fn element, acc -> + case {acc, ObjectID.cast(element)} do + {:error, _} -> :error + {_, :error} -> :error + {{:ok, list}, {:ok, id}} -> {:ok, [id | list]} + end + end) + end def cast(_) do :error diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs index 2f9218774..f278f039b 100644 --- a/test/web/activity_pub/object_validators/types/recipients_test.exs +++ b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -2,11 +2,23 @@ defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients use Pleroma.DataCase + test "it asserts that all elements of the list are object ids" do + list = ["https://lain.com/users/lain", "invalid"] + + assert :error == Recipients.cast(list) + end + test "it works with a list" do list = ["https://lain.com/users/lain"] assert {:ok, list} == Recipients.cast(list) end + test "it works with a list with whole objects" do + list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}] + resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"] + assert {:ok, resulting_list} == Recipients.cast(list) + end + test "it turns a single string into a list" do recipient = "https://lain.com/users/lain" From f8e56d4271f8c495316d304dd0de7f0a63eb0645 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Apr 2020 13:43:58 +0200 Subject: [PATCH 054/375] SideEffects: Use Object.normalize to get the object. --- lib/pleroma/web/activity_pub/side_effects.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index ebe3071b0..a2b4da8d6 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -30,8 +30,8 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do result end - def handle(%{data: %{"type" => "Create", "object" => object_id}} = activity, meta) do - object = Object.get_by_ap_id(object_id) + def handle(%{data: %{"type" => "Create"}} = activity, meta) do + object = Object.normalize(activity, false) {:ok, _object} = handle_object_creation(object) From 6aa116eca7d6ef6567dcef03b8c776bd2134bf3f Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Apr 2020 16:26:19 +0200 Subject: [PATCH 055/375] Create activity handling: Flip it and reverse it Both objects and create activities will now go through the common pipeline and will be validated. Objects are now created as a side effect of the Create activity, rolling back a transaction if it's not possible to insert the object. --- lib/pleroma/notification.ex | 2 +- lib/pleroma/web/activity_pub/activity_pub.ex | 7 ++++ .../web/activity_pub/object_validator.ex | 4 +- .../chat_message_validator.ex | 2 +- .../create_chat_message_validator.ex | 27 +++++++++++- .../object_validators/types/safe_text.ex | 25 +++++++++++ lib/pleroma/web/activity_pub/pipeline.ex | 12 ++++-- lib/pleroma/web/activity_pub/side_effects.ex | 41 +++++++++++-------- .../transmogrifier/chat_message_handling.ex | 28 ++++++++----- lib/pleroma/web/common_api/common_api.ex | 10 +++-- lib/pleroma/web/common_api/utils.ex | 2 +- .../types/safe_text_test.exs | 23 +++++++++++ test/web/activity_pub/side_effects_test.exs | 21 ++++++---- .../transmogrifier/chat_message_test.exs | 3 +- 14 files changed, 155 insertions(+), 52 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex create mode 100644 test/web/activity_pub/object_validators/types/safe_text_test.exs diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 73e19bf97..d96c12440 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -275,7 +275,7 @@ def dismiss(%{id: user_id} = _user, id) do end def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do - object = Object.normalize(activity) + object = Object.normalize(activity, false) if object && object.data["type"] == "Answer" do {:ok, []} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 69ac06f6b..ecb13d76a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -126,7 +126,14 @@ def increase_poll_votes_if_vote(%{ def increase_poll_votes_if_vote(_create_data), do: :noop + @object_types ["ChatMessage"] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} + def persist(%{"type" => type} = object, meta) when type in @object_types do + with {:ok, object} <- Object.create(object) do + {:ok, object, meta} + end + end + def persist(object, meta) do with local <- Keyword.fetch!(meta, :local), {recipients, _, _} <- get_recipients(object), diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 03db681ec..a4da9242a 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -23,7 +23,7 @@ def validate(%{"type" => "Like"} = object, meta) do object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object |> Map.from_struct()) + object = stringify_keys(object) {:ok, object, meta} end end @@ -41,7 +41,7 @@ def validate(%{"type" => "ChatMessage"} = object, meta) do def validate(%{"type" => "Create"} = object, meta) do with {:ok, object} <- object - |> CreateChatMessageValidator.cast_and_validate() + |> CreateChatMessageValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index f07045d9d..e87c1ac2e 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do field(:id, Types.ObjectID, primary_key: true) field(:to, Types.Recipients, default: []) field(:type, :string) - field(:content, :string) + field(:content, Types.SafeText) field(:actor, Types.ObjectID) field(:published, Types.DateTime) field(:emoji, :map, default: %{}) diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index ce52d5623..21c7a5ba4 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -33,8 +33,31 @@ def cast_data(data) do cast(%__MODULE__{}, data, __schema__(:fields)) end - # No validation yet - def cast_and_validate(data) do + def cast_and_validate(data, meta \\ []) do cast_data(data) + |> validate_data(meta) + end + + def validate_data(cng, meta \\ []) do + cng + |> validate_required([:id, :actor, :to, :type, :object]) + |> validate_inclusion(:type, ["Create"]) + |> validate_recipients_match(meta) + end + + def validate_recipients_match(cng, meta) do + object_recipients = meta[:object_data]["to"] || [] + + cng + |> validate_change(:to, fn :to, recipients -> + activity_set = MapSet.new(recipients) + object_set = MapSet.new(object_recipients) + + if MapSet.equal?(activity_set, object_set) do + [] + else + [{:to, "Recipients don't match with object recipients"}] + end + end) end end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex new file mode 100644 index 000000000..822e8d2c1 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText do + use Ecto.Type + + alias Pleroma.HTML + + def type, do: :string + + def cast(str) when is_binary(str) do + {:ok, HTML.strip_tags(str)} + end + + def cast(_), do: :error + + def dump(data) do + {:ok, data} + end + + def load(data) do + {:ok, data} + end +end diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 7ccee54c9..4213ba751 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -4,20 +4,22 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do alias Pleroma.Activity + alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.Federator - @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()} + @spec common_pipeline(map(), keyword()) :: + {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} def common_pipeline(object, meta) do with {_, {:ok, validated_object, meta}} <- {:validate_object, ObjectValidator.validate(object, meta)}, {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)}, - {_, {:ok, %Activity{} = activity, meta}} <- + {_, {:ok, activity, meta}} <- {:persist_object, ActivityPub.persist(mrfd_object, meta)}, - {_, {:ok, %Activity{} = activity, meta}} <- + {_, {:ok, activity, meta}} <- {:execute_side_effects, SideEffects.handle(activity, meta)}, {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do {:ok, activity, meta} @@ -27,7 +29,9 @@ def common_pipeline(object, meta) do end end - defp maybe_federate(activity, meta) do + defp maybe_federate(%Object{}, _), do: {:ok, :not_federated} + + defp maybe_federate(%Activity{} = activity, meta) do with {:ok, local} <- Keyword.fetch(meta, :local) do if local do Federator.publish(activity) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index a2b4da8d6..794a46267 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -8,7 +8,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Chat alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@ -30,14 +32,17 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do result end + # Tasks this handles + # - Actually create object + # - Rollback if we couldn't create it + # - Set up notifications def handle(%{data: %{"type" => "Create"}} = activity, meta) do - object = Object.normalize(activity, false) - - {:ok, _object} = handle_object_creation(object) - - Notification.create_notifications(activity) - - {:ok, activity, meta} + with {:ok, _object, _meta} <- handle_object_creation(meta[:object_data], meta) do + Notification.create_notifications(activity) + {:ok, activity, meta} + else + e -> Repo.rollback(e) + end end # Nothing to do @@ -45,18 +50,20 @@ def handle(object, meta) do {:ok, object, meta} end - def handle_object_creation(%{data: %{"type" => "ChatMessage"}} = object) do - actor = User.get_cached_by_ap_id(object.data["actor"]) - recipient = User.get_cached_by_ap_id(hd(object.data["to"])) + def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do + actor = User.get_cached_by_ap_id(object.data["actor"]) + recipient = User.get_cached_by_ap_id(hd(object.data["to"])) - [[actor, recipient], [recipient, actor]] - |> Enum.each(fn [user, other_user] -> - if user.local do - Chat.bump_or_create(user.id, other_user.ap_id) - end - end) + [[actor, recipient], [recipient, actor]] + |> Enum.each(fn [user, other_user] -> + if user.local do + Chat.bump_or_create(user.id, other_user.ap_id) + end + end) - {:ok, object} + {:ok, object, meta} + end end # Nothing to do diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex index cfe3b767b..043d847d1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -3,31 +3,39 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling do - alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.Pipeline def handle_incoming( - %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object_data} = data, + %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, _options ) do + # Create has to be run inside a transaction because the object is created as a side effect. + # If this does not work, we need to roll back creating the activity. + case Repo.transaction(fn -> do_handle_incoming(data) end) do + {:ok, value} -> + value + + {:error, e} -> + {:error, e} + end + end + + def do_handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object_data} = data + ) do with {_, {:ok, cast_data_sym}} <- {:casting_data, data |> CreateChatMessageValidator.cast_and_apply()}, cast_data = ObjectValidator.stringify_keys(cast_data_sym), {_, {:ok, object_cast_data_sym}} <- {:casting_object_data, object_data |> ChatMessageValidator.cast_and_apply()}, object_cast_data = ObjectValidator.stringify_keys(object_cast_data_sym), - # For now, just strip HTML - stripped_content = Pleroma.HTML.strip_tags(object_cast_data["content"]), - object_cast_data = object_cast_data |> Map.put("content", stripped_content), - {_, true} <- {:to_fields_match, cast_data["to"] == object_cast_data["to"]}, - {_, {:ok, validated_object, _meta}} <- - {:validate_object, ObjectValidator.validate(object_cast_data, %{})}, - {_, {:ok, _created_object}} <- {:persist_object, Object.create(validated_object)}, {_, {:ok, activity, _meta}} <- - {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do + {:common_pipeline, + Pipeline.common_pipeline(cast_data, local: false, object_data: object_cast_data)} do {:ok, activity} else e -> diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 5eb221668..c39d1cee6 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -38,13 +38,15 @@ def post_chat_message(%User{} = user, %User{} = recipient, content) do recipient.ap_id, content |> Formatter.html_escape("text/plain") )}, - {_, {:ok, chat_message_object}} <- - {:create_object, Object.create(chat_message_data)}, {_, {:ok, create_activity_data, _meta}} <- {:build_create_activity, - Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])}, + Builder.create(user, chat_message_data["id"], [recipient.ap_id])}, {_, {:ok, %Activity{} = activity, _meta}} <- - {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do + {:common_pipeline, + Pipeline.common_pipeline(create_activity_data, + local: true, + object_data: chat_message_data + )} do {:ok, activity} else {:content_length, false} -> {:error, :content_too_long} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 945e63e22..4afdf80af 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -425,7 +425,7 @@ def maybe_notify_mentioned_recipients( %Activity{data: %{"to" => _to, "type" => type} = data} = activity ) when type == "Create" do - object = Object.normalize(activity) + object = Object.normalize(activity, false) object_data = cond do diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs new file mode 100644 index 000000000..59ed0a1fe --- /dev/null +++ b/test/web/activity_pub/object_validators/types/safe_text_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeTextTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText + + test "it lets normal text go through" do + text = "hey how are you" + assert {:ok, text} == SafeText.cast(text) + end + + test "it removes html tags from text" do + text = "hey look xss " + assert {:ok, "hey look xss alert('foo')"} == SafeText.cast(text) + end + + test "errors for non-text" do + assert :error == SafeText.cast(1) + end +end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 2889a577c..19abac6a6 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -47,14 +47,14 @@ test "notifies the recipient" do recipient = insert(:user, local: true) {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - {:ok, chat_message_object} = Object.create(chat_message_data) {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) end @@ -64,14 +64,17 @@ test "it creates a Chat for the local users and bumps the unread count" do recipient = insert(:user, local: true) {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - {:ok, chat_message_object} = Object.create(chat_message_data) {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # An object is created + assert Object.get_by_ap_id(chat_message_data["id"]) # The remote user won't get a chat chat = Chat.get(author.id, recipient.ap_id) @@ -85,14 +88,14 @@ test "it creates a Chat for the local users and bumps the unread count" do recipient = insert(:user, local: true) {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - {:ok, chat_message_object} = Object.create(chat_message_data) {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) # Both users are local and get the chat chat = Chat.get(author.id, recipient.ap_id) diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index a63a31e6e..ceaee614c 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -55,7 +55,8 @@ test "it rejects messages where the `to` field of activity and object don't matc data |> Map.put("to", author.ap_id) - {:error, _} = Transmogrifier.handle_incoming(data) + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + refute Object.get_by_ap_id(data["object"]["id"]) end test "it inserts it and creates a chat" do From abd09282292f7e902c77b158ae3d86e9bfd5b986 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Apr 2020 16:45:28 +0200 Subject: [PATCH 056/375] CreateChatMessageValidator: Validate object existence --- .../create_chat_message_validator.ex | 14 +++++++++++++- test/web/activity_pub/object_validator_test.exs | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index 21c7a5ba4..dfc91bf71 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -5,10 +5,10 @@ # NOTES # - Can probably be a generic create validator # - doesn't embed, will only get the object id -# - object has to be validated first, maybe with some meta info from the surrounding create defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do use Ecto.Schema + alias Pleroma.Object alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset @@ -43,6 +43,18 @@ def validate_data(cng, meta \\ []) do |> validate_required([:id, :actor, :to, :type, :object]) |> validate_inclusion(:type, ["Create"]) |> validate_recipients_match(meta) + |> validate_object_nonexistence() + end + + def validate_object_nonexistence(cng) do + cng + |> validate_change(:object, fn :object, object_id -> + if Object.get_cached_by_ap_id(object_id) do + [{:object, "The object to create already exists"}] + else + [] + end + end) end def validate_recipients_match(cng, meta) do diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index bc2317e55..baa4b2585 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -1,6 +1,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do use Pleroma.DataCase + alias Pleroma.Object alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator @@ -9,6 +10,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do import Pleroma.Factory + describe "chat message create activities" do + test "it is invalid if the object already exists" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(activity, false) + + {:ok, create_data, _} = Builder.create(user, object.data["id"], [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:object, {"The object to create already exists", []}} in cng.errors + end + end + describe "chat messages" do setup do clear_config([:instance, :remote_limit]) From dedffd100c231aa69d7a7f7cd7126b90a84fc1ec Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 28 Apr 2020 17:29:54 +0200 Subject: [PATCH 057/375] Pipeline: Unify, refactor, DRY. --- lib/pleroma/web/activity_pub/builder.ex | 4 +-- .../web/activity_pub/object_validator.ex | 18 ++++++++--- .../transmogrifier/chat_message_handling.ex | 31 ++++--------------- lib/pleroma/web/common_api/common_api.ex | 5 ++- .../activity_pub/object_validator_test.exs | 2 +- 5 files changed, 24 insertions(+), 36 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 7576ed278..7f9c071b3 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -11,13 +11,13 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility - def create(actor, object_id, recipients) do + def create(actor, object, recipients) do {:ok, %{ "id" => Utils.generate_activity_id(), "actor" => actor.ap_id, "to" => recipients, - "object" => object_id, + "object" => object, "type" => "Create", "published" => DateTime.utc_now() |> DateTime.to_iso8601() }, []} diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index a4da9242a..bada3509d 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -38,16 +38,24 @@ def validate(%{"type" => "ChatMessage"} = object, meta) do end end - def validate(%{"type" => "Create"} = object, meta) do - with {:ok, object} <- - object + def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do + with {:ok, object_data} <- cast_and_apply(object), + meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), + {:ok, create_activity} <- + create_activity |> CreateChatMessageValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object) - {:ok, object, meta} + create_activity = stringify_keys(create_activity) + {:ok, create_activity, meta} end end + def cast_and_apply(%{"type" => "ChatMessage"} = object) do + ChatMessageValidator.cast_and_apply(object) + end + + def cast_and_apply(o), do: {:error, {:validator_not_set, o}} + def stringify_keys(%{__struct__: _} = object) do object |> Map.from_struct() diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex index 043d847d1..d9c36e313 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -4,9 +4,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling do alias Pleroma.Repo - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.Pipeline def handle_incoming( @@ -15,31 +12,15 @@ def handle_incoming( ) do # Create has to be run inside a transaction because the object is created as a side effect. # If this does not work, we need to roll back creating the activity. - case Repo.transaction(fn -> do_handle_incoming(data) end) do - {:ok, value} -> - value + case Repo.transaction(fn -> Pipeline.common_pipeline(data, local: false) end) do + {:ok, {:ok, activity, _}} -> + {:ok, activity} + + {:ok, e} -> + e {:error, e} -> {:error, e} end end - - def do_handle_incoming( - %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object_data} = data - ) do - with {_, {:ok, cast_data_sym}} <- - {:casting_data, data |> CreateChatMessageValidator.cast_and_apply()}, - cast_data = ObjectValidator.stringify_keys(cast_data_sym), - {_, {:ok, object_cast_data_sym}} <- - {:casting_object_data, object_data |> ChatMessageValidator.cast_and_apply()}, - object_cast_data = ObjectValidator.stringify_keys(object_cast_data_sym), - {_, {:ok, activity, _meta}} <- - {:common_pipeline, - Pipeline.common_pipeline(cast_data, local: false, object_data: object_cast_data)} do - {:ok, activity} - else - e -> - {:error, e} - end - end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index c39d1cee6..ef86ec1e4 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -40,12 +40,11 @@ def post_chat_message(%User{} = user, %User{} = recipient, content) do )}, {_, {:ok, create_activity_data, _meta}} <- {:build_create_activity, - Builder.create(user, chat_message_data["id"], [recipient.ap_id])}, + Builder.create(user, chat_message_data, [recipient.ap_id])}, {_, {:ok, %Activity{} = activity, _meta}} <- {:common_pipeline, Pipeline.common_pipeline(create_activity_data, - local: true, - object_data: chat_message_data + local: true )} do {:ok, activity} else diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index baa4b2585..41f67964a 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -17,7 +17,7 @@ test "it is invalid if the object already exists" do {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") object = Object.normalize(activity, false) - {:ok, create_data, _} = Builder.create(user, object.data["id"], [recipient.ap_id]) + {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) {:error, cng} = ObjectValidator.validate(create_data, []) From 67659afe487def6bd4e0ccfbf8d015fda2a8ac61 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 13:34:43 +0200 Subject: [PATCH 058/375] ChatOperation: Refactor. --- .../web/api_spec/operations/chat_operation.ex | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 59539e890..88b9db048 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -5,11 +5,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema - alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse alias Pleroma.Web.ApiSpec.Schemas.ChatResponse + import Pleroma.Web.ApiSpec.Helpers + @spec open_api_operation(atom) :: Operation.t() def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -52,11 +53,7 @@ def index_operation do tags: ["chat"], summary: "Get a list of chats that you participated in", operationId: "ChatController.index", - parameters: [ - Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20), - Operation.parameter(:min_id, :query, :string, "Return only chats after this id"), - Operation.parameter(:max_id, :query, :string, "Return only chats before this id") - ], + parameters: pagination_params(), responses: %{ 200 => Operation.response("The chats of the user", "application/json", chats_response()) }, @@ -73,12 +70,9 @@ def messages_operation do tags: ["chat"], summary: "Get the most recent messages of the chat", operationId: "ChatController.messages", - parameters: [ - Operation.parameter(:id, :path, :string, "The ID of the Chat"), - Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20), - Operation.parameter(:min_id, :query, :string, "Return only messages after this id"), - Operation.parameter(:max_id, :query, :string, "Return only messages before this id") - ], + parameters: + [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++ + pagination_params(), responses: %{ 200 => Operation.response( @@ -103,7 +97,7 @@ def post_chat_message_operation do parameters: [ Operation.parameter(:id, :path, :string, "The ID of the Chat") ], - requestBody: Helpers.request_body("Parameters", ChatMessageCreateRequest, required: true), + requestBody: request_body("Parameters", ChatMessageCreateRequest, required: true), responses: %{ 200 => Operation.response( From e055b8d2036e18a95d84f6f1db08fc465fe9975d Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 13:45:50 +0200 Subject: [PATCH 059/375] Pipeline: Always run common_pipeline in a transaction for now. --- lib/pleroma/web/activity_pub/pipeline.ex | 11 ++++ .../transmogrifier/chat_message_handling.ex | 12 ++--- lib/pleroma/web/common_api/common_api.ex | 52 ++++++++----------- .../transmogrifier/chat_message_test.exs | 1 + 4 files changed, 36 insertions(+), 40 deletions(-) diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 4213ba751..d5abb7567 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.ObjectValidator @@ -14,6 +15,16 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} def common_pipeline(object, meta) do + case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do + {:ok, value} -> + value + + {:error, e} -> + {:error, e} + end + end + + def do_common_pipeline(object, meta) do with {_, {:ok, validated_object, meta}} <- {:validate_object, ObjectValidator.validate(object, meta)}, {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)}, diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex index d9c36e313..b1cc93481 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -3,24 +3,18 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling do - alias Pleroma.Repo alias Pleroma.Web.ActivityPub.Pipeline def handle_incoming( %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, _options ) do - # Create has to be run inside a transaction because the object is created as a side effect. - # If this does not work, we need to roll back creating the activity. - case Repo.transaction(fn -> Pipeline.common_pipeline(data, local: false) end) do - {:ok, {:ok, activity, _}} -> + case Pipeline.common_pipeline(data, local: false) do + {:ok, activity, _} -> {:ok, activity} - {:ok, e} -> + e -> e - - {:error, e} -> - {:error, e} end end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index ef86ec1e4..359045f48 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.FollowingRelationship alias Pleroma.Formatter alias Pleroma.Object - alias Pleroma.Repo alias Pleroma.ThreadMute alias Pleroma.User alias Pleroma.UserRelationship @@ -26,36 +25,27 @@ defmodule Pleroma.Web.CommonAPI do require Logger def post_chat_message(%User{} = user, %User{} = recipient, content) do - transaction = - Repo.transaction(fn -> - with {_, true} <- - {:content_length, - String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])}, - {_, {:ok, chat_message_data, _meta}} <- - {:build_object, - Builder.chat_message( - user, - recipient.ap_id, - content |> Formatter.html_escape("text/plain") - )}, - {_, {:ok, create_activity_data, _meta}} <- - {:build_create_activity, - Builder.create(user, chat_message_data, [recipient.ap_id])}, - {_, {:ok, %Activity{} = activity, _meta}} <- - {:common_pipeline, - Pipeline.common_pipeline(create_activity_data, - local: true - )} do - {:ok, activity} - else - {:content_length, false} -> {:error, :content_too_long} - e -> e - end - end) - - case transaction do - {:ok, value} -> value - error -> error + with {_, true} <- + {:content_length, + String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])}, + {_, {:ok, chat_message_data, _meta}} <- + {:build_object, + Builder.chat_message( + user, + recipient.ap_id, + content |> Formatter.html_escape("text/plain") + )}, + {_, {:ok, create_activity_data, _meta}} <- + {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(create_activity_data, + local: true + )} do + {:ok, activity} + else + {:content_length, false} -> {:error, :content_too_long} + e -> e end end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index ceaee614c..c5600e84e 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -68,6 +68,7 @@ test "it inserts it and creates a chat" do recipient = insert(:user, ap_id: List.first(data["to"]), local: true) {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) + assert activity.local == false assert activity.actor == author.ap_id assert activity.recipients == [recipient.ap_id, author.ap_id] From 53e3063bd041409da83483e8f5c47030bf346123 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 13:52:23 +0200 Subject: [PATCH 060/375] Transmogrifier: Remove ChatMessageHandling module. --- lib/pleroma/web/activity_pub/side_effects.ex | 13 ++++-------- .../web/activity_pub/transmogrifier.ex | 14 +++++++++---- .../transmogrifier/chat_message_handling.ex | 20 ------------------- 3 files changed, 14 insertions(+), 33 deletions(-) delete mode 100644 lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 794a46267..e394c75d7 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -19,17 +19,12 @@ def handle(object, meta \\ []) # - Add like to object # - Set up notification def handle(%{data: %{"type" => "Like"}} = object, meta) do - {:ok, result} = - Pleroma.Repo.transaction(fn -> - liked_object = Object.get_by_ap_id(object.data["object"]) - Utils.add_like_to_object(object, liked_object) + liked_object = Object.get_by_ap_id(object.data["object"]) + Utils.add_like_to_object(object, liked_object) - Notification.create_notifications(object) + Notification.create_notifications(object) - {:ok, object, meta} - end) - - result + {:ok, object, meta} end # Tasks this handles diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 66975cf7d..3c2fe73a3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -16,7 +16,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Pipeline - alias Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator @@ -646,9 +645,16 @@ def handle_incoming( def handle_incoming( %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, - options - ), - do: ChatMessageHandling.handle_incoming(data, options) + _options + ) do + case Pipeline.common_pipeline(data, local: false) do + {:ok, activity, _} -> + {:ok, activity} + + e -> + e + end + end def handle_incoming(%{"type" => "Like"} = data, _options) do with {_, {:ok, cast_data_sym}} <- diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex deleted file mode 100644 index b1cc93481..000000000 --- a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling do - alias Pleroma.Web.ActivityPub.Pipeline - - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, - _options - ) do - case Pipeline.common_pipeline(data, local: false) do - {:ok, activity, _} -> - {:ok, activity} - - e -> - e - end - end -end From a88734a0a22810bcc47c17fc9120ef7881d670d8 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 14:25:33 +0200 Subject: [PATCH 061/375] Transmogrifier: Fetch missing actors for chatmessages. --- .../web/activity_pub/object_validator.ex | 9 ++++- .../create_chat_message_validator.ex | 2 + .../web/activity_pub/transmogrifier.ex | 8 ++-- .../transmogrifier/chat_message_test.exs | 40 ++++++++++++++++--- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index bada3509d..50904ed59 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator @@ -67,8 +68,14 @@ def stringify_keys(object) do |> Map.new(fn {key, val} -> {to_string(key), val} end) end + def fetch_actor(object) do + with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do + User.get_or_fetch_by_ap_id(actor) + end + end + def fetch_actor_and_object(object) do - User.get_or_fetch_by_ap_id(object["actor"]) + fetch_actor(object) Object.normalize(object["object"]) :ok end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index dfc91bf71..88e903182 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @primary_key false @@ -42,6 +43,7 @@ def validate_data(cng, meta \\ []) do cng |> validate_required([:id, :actor, :to, :type, :object]) |> validate_inclusion(:type, ["Create"]) + |> validate_actor_presence() |> validate_recipients_match(meta) |> validate_object_nonexistence() end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3c2fe73a3..6dbd3f588 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -647,10 +647,10 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, _options ) do - case Pipeline.common_pipeline(data, local: false) do - {:ok, activity, _} -> - {:ok, activity} - + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), + {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity} + else e -> e end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index c5600e84e..85644d787 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -26,8 +26,15 @@ test "it rejects messages that don't contain content" do data |> Map.put("object", object) - _author = insert(:user, ap_id: data["actor"], local: false) - _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: true, + last_refreshed_at: DateTime.utc_now() + ) {:error, _} = Transmogrifier.handle_incoming(data) end @@ -37,8 +44,15 @@ test "it rejects messages that don't concern local users" do File.read!("test/fixtures/create-chat-message.json") |> Poison.decode!() - _author = insert(:user, ap_id: data["actor"], local: false) - _recipient = insert(:user, ap_id: List.first(data["to"]), local: false) + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: false, + last_refreshed_at: DateTime.utc_now() + ) {:error, _} = Transmogrifier.handle_incoming(data) end @@ -59,12 +73,28 @@ test "it rejects messages where the `to` field of activity and object don't matc refute Object.get_by_ap_id(data["object"]["id"]) end + test "it fetches the actor if they aren't in our system" do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + |> Map.put("actor", "http://mastodon.example.org/users/admin") + |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin") + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) + end + test "it inserts it and creates a chat" do data = File.read!("test/fixtures/create-chat-message.json") |> Poison.decode!() - author = insert(:user, ap_id: data["actor"], local: false) + author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + recipient = insert(:user, ap_id: List.first(data["to"]), local: true) {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) From 20587aa931262a5479c98f13450311a135c5d356 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 14:53:53 +0200 Subject: [PATCH 062/375] Chat message creation: Check actor. --- .../create_chat_message_validator.ex | 14 ++++++++++++++ test/web/activity_pub/object_validator_test.exs | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index 88e903182..fc582400b 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -45,6 +45,7 @@ def validate_data(cng, meta \\ []) do |> validate_inclusion(:type, ["Create"]) |> validate_actor_presence() |> validate_recipients_match(meta) + |> validate_actors_match(meta) |> validate_object_nonexistence() end @@ -59,6 +60,19 @@ def validate_object_nonexistence(cng) do end) end + def validate_actors_match(cng, meta) do + object_actor = meta[:object_data]["actor"] + + cng + |> validate_change(:actor, fn :actor, actor -> + if actor == object_actor do + [] + else + [{:actor, "Actor doesn't match with object actor"}] + end + end) + end + def validate_recipients_match(cng, meta) do object_recipients = meta[:object_data]["to"] || [] diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 41f67964a..475b7bb21 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -23,6 +23,19 @@ test "it is invalid if the object already exists" do assert {:object, {"The object to create already exists", []}} in cng.errors end + + test "it is invalid if the object data has a different `to` or `actor` field" do + user = insert(:user) + recipient = insert(:user) + {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") + + {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors + assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors + end end describe "chat messages" do From 528ea779a61d12f74ee9669bbd28783bf66aa5cc Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 17:56:24 +0000 Subject: [PATCH 063/375] Apply suggestion to docs/API/chats.md --- docs/API/chats.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 24c4b4d06..26e83570e 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -99,7 +99,7 @@ For a given Chat id, you can get the associated messages with `GET /api/v1/pleroma/chats/{id}/messages` This will return all messages, sorted by most recent to least recent. The usual -pagination options are implemented +pagination options are implemented. Returned data: From 89a6c340812a53daf00a203dacd8e12a25eb7ad2 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 18:14:34 +0000 Subject: [PATCH 064/375] Apply suggestion to lib/pleroma/chat.ex --- lib/pleroma/chat.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index b8545063a..6b1f832ce 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -29,7 +29,7 @@ def creation_cng(struct, params) do |> validate_change(:recipient, fn :recipient, recipient -> case User.get_cached_by_ap_id(recipient) do - nil -> [recipient: "must a an existing user"] + nil -> [recipient: "must be an existing user"] _ -> [] end end) From 589ce1e96bcaba0bd2d864d3528992f10f4cf5f7 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 19:47:16 +0000 Subject: [PATCH 065/375] Apply suggestion to lib/pleroma/web/activity_pub/transmogrifier.ex --- lib/pleroma/web/activity_pub/transmogrifier.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6dbd3f588..d3a2e0362 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -650,9 +650,6 @@ def handle_incoming( with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} - else - e -> - e end end From 145d35ff70a59efcff881315d5f1f7a0248a34be Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 19:49:03 +0000 Subject: [PATCH 066/375] Apply suggestion to lib/pleroma/web/pleroma_api/controllers/chat_controller.ex --- lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 8654f4295..175257921 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do %{scopes: ["read:statuses"]} when action in [:messages, :index] ) - plug(OpenApiSpex.Plug.CastAndValidate) + plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation From b68d56c8168f27f63e157d43558e22f7c221c0e2 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 29 Apr 2020 19:49:13 +0000 Subject: [PATCH 067/375] Apply suggestion to lib/pleroma/web/api_spec/schemas/chat_message_response.ex --- lib/pleroma/web/api_spec/schemas/chat_message_response.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex index b7a662cbb..707c9808b 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"}, chat_id: %Schema{type: :string}, content: %Schema{type: :string}, - created_at: %Schema{type: :string, format: :datetime}, + created_at: %Schema{type: :string, format: :"date-time"}, emojis: %Schema{type: :array} }, example: %{ From ad2182bbd231b475c5bfc70485f35ad1f8841912 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 30 Apr 2020 11:38:26 +0000 Subject: [PATCH 068/375] Apply suggestion to lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex --- lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex b/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex index 4dafcda43..8e1b7af14 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest do properties: %{ content: %Schema{type: :string, description: "The content of your message"} }, + required: [:content], example: %{ "content" => "Hey wanna buy feet pics?" } From a35b76431ce7c7bd7ed62374d781778922f0fe2f Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 3 May 2020 14:58:24 +0200 Subject: [PATCH 069/375] Credo fixes. --- lib/pleroma/web/activity_pub/object_validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 50904ed59..20c7cceb6 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -11,10 +11,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.Types @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) From 9249742f13445f47167d4b352751c49caf48aa8f Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 3 May 2020 15:28:24 +0200 Subject: [PATCH 070/375] Types.Recipients: Simplify reducer. --- .../object_validators/types/recipients.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex index 48fe61e1a..408e0f6ee 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex +++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex @@ -11,11 +11,13 @@ def cast(object) when is_binary(object) do def cast(data) when is_list(data) do data - |> Enum.reduce({:ok, []}, fn element, acc -> - case {acc, ObjectID.cast(element)} do - {:error, _} -> :error - {_, :error} -> :error - {{:ok, list}, {:ok, id}} -> {:ok, [id | list]} + |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} -> + case ObjectID.cast(element) do + {:ok, id} -> + {:cont, {:ok, [id | list]}} + + _ -> + {:halt, :error} end end) end From 651935f1379a1ed3c89e473803251310c13ea571 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 11:08:00 +0200 Subject: [PATCH 071/375] Schemas: Refactor to our naming scheme. --- .../web/api_spec/operations/chat_operation.ex | 12 ++++++------ .../api_spec/schemas/{chat_response.ex => chat.ex} | 4 ++-- .../{chat_message_response.ex => chat_message.ex} | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) rename lib/pleroma/web/api_spec/schemas/{chat_response.ex => chat.ex} (96%) rename lib/pleroma/web/api_spec/schemas/{chat_message_response.ex => chat_message.ex} (91%) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 88b9db048..fc9d4608a 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest - alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse - alias Pleroma.Web.ApiSpec.Schemas.ChatResponse + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage + alias Pleroma.Web.ApiSpec.Schemas.Chat import Pleroma.Web.ApiSpec.Helpers @@ -37,7 +37,7 @@ def create_operation do Operation.response( "The created or existing chat", "application/json", - ChatResponse + Chat ) }, security: [ @@ -103,7 +103,7 @@ def post_chat_message_operation do Operation.response( "The newly created ChatMessage", "application/json", - ChatMessageResponse + ChatMessage ) }, security: [ @@ -119,7 +119,7 @@ def chats_response do title: "ChatsResponse", description: "Response schema for multiple Chats", type: :array, - items: ChatResponse, + items: Chat, example: [ %{ "account" => %{ @@ -180,7 +180,7 @@ def chat_messages_response do title: "ChatMessagesResponse", description: "Response schema for multiple ChatMessages", type: :array, - items: ChatMessageResponse, + items: ChatMessage, example: [ %{ "emojis" => [ diff --git a/lib/pleroma/web/api_spec/schemas/chat_response.ex b/lib/pleroma/web/api_spec/schemas/chat.ex similarity index 96% rename from lib/pleroma/web/api_spec/schemas/chat_response.ex rename to lib/pleroma/web/api_spec/schemas/chat.ex index aa435165d..4d385d6ab 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_response.ex +++ b/lib/pleroma/web/api_spec/schemas/chat.ex @@ -2,13 +2,13 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.Schemas.ChatResponse do +defmodule Pleroma.Web.ApiSpec.Schemas.Chat do alias OpenApiSpex.Schema require OpenApiSpex OpenApiSpex.schema(%{ - title: "ChatResponse", + title: "Chat", description: "Response schema for a Chat", type: :object, properties: %{ diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex similarity index 91% rename from lib/pleroma/web/api_spec/schemas/chat_message_response.ex rename to lib/pleroma/web/api_spec/schemas/chat_message.ex index 707c9808b..7c93b0c83 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -2,13 +2,13 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do +defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do alias OpenApiSpex.Schema require OpenApiSpex OpenApiSpex.schema(%{ - title: "ChatMessageResponse", + title: "ChatMessage", description: "Response schema for a ChatMessage", type: :object, properties: %{ From dcf535fe770b638b68928f238f4d8d1cfd410524 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 11:32:11 +0200 Subject: [PATCH 072/375] Credo fixes. --- lib/pleroma/web/api_spec/operations/chat_operation.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index fc9d4608a..ad05f5ac7 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema - alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest - alias Pleroma.Web.ApiSpec.Schemas.ChatMessage alias Pleroma.Web.ApiSpec.Schemas.Chat + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage + alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest import Pleroma.Web.ApiSpec.Helpers From 57e6f2757afef8941fe3576dbe5e2014d2569c33 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 12:47:23 +0200 Subject: [PATCH 073/375] ChatOperation: Make simple schema into inline schema --- .../web/api_spec/operations/chat_operation.ex | 18 ++++++++++++++-- .../schemas/chat_message_create_request.ex | 21 ------------------- 2 files changed, 16 insertions(+), 23 deletions(-) delete mode 100644 lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index ad05f5ac7..e8b5eff1f 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.Chat alias Pleroma.Web.ApiSpec.Schemas.ChatMessage - alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest import Pleroma.Web.ApiSpec.Helpers @@ -97,7 +96,7 @@ def post_chat_message_operation do parameters: [ Operation.parameter(:id, :path, :string, "The ID of the Chat") ], - requestBody: request_body("Parameters", ChatMessageCreateRequest, required: true), + requestBody: request_body("Parameters", chat_message_create(), required: true), responses: %{ 200 => Operation.response( @@ -208,4 +207,19 @@ def chat_messages_response do ] } end + + def chat_message_create do + %Schema{ + title: "ChatMessageCreateRequest", + description: "POST body for creating an chat message", + type: :object, + properties: %{ + content: %Schema{type: :string, description: "The content of your message"} + }, + required: [:content], + example: %{ + "content" => "Hey wanna buy feet pics?" + } + } + end end diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex b/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex deleted file mode 100644 index 8e1b7af14..000000000 --- a/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest do - alias OpenApiSpex.Schema - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "ChatMessageCreateRequest", - description: "POST body for creating an chat message", - type: :object, - properties: %{ - content: %Schema{type: :string, description: "The content of your message"} - }, - required: [:content], - example: %{ - "content" => "Hey wanna buy feet pics?" - } - }) -end From 30590cf46b88d0008c9a7163b8339aa9376f2378 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 12:53:40 +0200 Subject: [PATCH 074/375] CommonAPI: Refactor for readability --- lib/pleroma/web/common_api/common_api.ex | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 1eda0b2f2..e428cc17d 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -26,9 +26,7 @@ defmodule Pleroma.Web.CommonAPI do require Logger def post_chat_message(%User{} = user, %User{} = recipient, content) do - with {_, true} <- - {:content_length, - String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])}, + with :ok <- validate_chat_content_length(content), {_, {:ok, chat_message_data, _meta}} <- {:build_object, Builder.chat_message( @@ -44,9 +42,14 @@ def post_chat_message(%User{} = user, %User{} = recipient, content) do local: true )} do {:ok, activity} + end + end + + defp validate_chat_content_length(content) do + if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do + :ok else - {:content_length, false} -> {:error, :content_too_long} - e -> e + {:error, :content_too_long} end end From b04328c3dec4812dbaf3cd89baa2b888d7bb7fbf Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 13:10:36 +0200 Subject: [PATCH 075/375] ChatController: Add mark_as_read --- lib/pleroma/chat.ex | 6 +++++ .../web/api_spec/operations/chat_operation.ex | 22 ++++++++++++++++++ .../controllers/chat_controller.ex | 11 ++++++++- lib/pleroma/web/router.ex | 1 + .../controllers/chat_controller_test.exs | 23 +++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 6b1f832ce..6008196e4 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -60,4 +60,10 @@ def bump_or_create(user_id, recipient) do conflict_target: [:user_id, :recipient] ) end + + def mark_as_read(chat) do + chat + |> change(%{unread: 0}) + |> Repo.update() + end end diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index e8b5eff1f..0fe0e07b2 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -16,6 +16,28 @@ def open_api_operation(action) do apply(__MODULE__, operation, []) end + def mark_as_read_operation do + %Operation{ + tags: ["chat"], + summary: "Mark all messages in the chat as read", + operationId: "ChatController.mark_as_read", + parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")], + responses: %{ + 200 => + Operation.response( + "The updated chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["write"] + } + ] + } + end + def create_operation do %Operation{ tags: ["chat"], diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 175257921..bedae73bd 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} when action in [:post_chat_message, :create] + %{scopes: ["write:statuses"]} when action in [:post_chat_message, :create, :mark_as_read] ) plug( @@ -51,6 +51,15 @@ def post_chat_message( end end + def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do + with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), + {:ok, chat} <- Chat.mark_as_read(chat) do + conn + |> put_view(ChatView) + |> render("show.json", chat: chat) + end + end + def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do messages = diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3a5063d4a..d6803e8ac 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -293,6 +293,7 @@ defmodule Pleroma.Web.Router do get("/chats", ChatController, :index) get("/chats/:id/messages", ChatController, :messages) post("/chats/:id/messages", ChatController, :post_chat_message) + post("/chats/:id/read", ChatController, :mark_as_read) end scope [] do diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index b1044574b..cdb2683c8 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -9,6 +9,29 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do import Pleroma.Factory + describe "POST /api/v1/pleroma/chats/:id/read" do + setup do: oauth_access(["write:statuses"]) + + test "it marks all messages in a chat as read", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.unread == 1 + + result = + conn + |> post("/api/v1/pleroma/chats/#{chat.id}/read") + |> json_response_and_validate_schema(200) + + assert result["unread"] == 0 + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + assert chat.unread == 0 + end + end + describe "POST /api/v1/pleroma/chats/:id/messages" do setup do: oauth_access(["write:statuses"]) From 7ff2a7dae2fa651cea579aeca40e2c030d19fcd5 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 13:12:21 +0200 Subject: [PATCH 076/375] Docs: Add Chat mark_as_read docs --- docs/API/chats.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/API/chats.md b/docs/API/chats.md index 26e83570e..8d925989c 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -66,6 +66,27 @@ Returned data: } ``` +### Marking a chat as read + +To set the `unread` count of a chat to 0, call + +`POST /api/v1/pleroma/chats/:id/read` + +Returned data: + +```json +{ + "account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 0 +} +``` + + ### Getting a list of Chats `GET /api/v1/pleroma/chats` From 9637cded21cef1e6c531dd46d5f5245c4c3ed03c Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 5 May 2020 20:07:47 +0200 Subject: [PATCH 077/375] Chat: Fix missing chat id on second 'get' --- lib/pleroma/chat.ex | 3 ++- test/chat_test.exs | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 6008196e4..1a092b992 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -46,7 +46,8 @@ def get_or_create(user_id, recipient) do %__MODULE__{} |> creation_cng(%{user_id: user_id, recipient: recipient}) |> Repo.insert( - on_conflict: :nothing, + # Need to set something, otherwise we get nothing back at all + on_conflict: [set: [recipient: recipient]], returning: true, conflict_target: [:user_id, :recipient] ) diff --git a/test/chat_test.exs b/test/chat_test.exs index 952598c87..943e48111 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -26,13 +26,24 @@ test "it creates a chat for a user and recipient" do assert chat.id end - test "it returns a chat for a user and recipient if it already exists" do + test "it returns and bumps a chat for a user and recipient if it already exists" do user = insert(:user) other_user = insert(:user) {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + assert chat.id == chat_two.id + assert chat_two.unread == 2 + end + + test "it returns a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + assert chat.id == chat_two.id end From 20baa2eaf04425cf0a2eebc84760be6c12ee7f51 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 6 May 2020 16:12:36 +0200 Subject: [PATCH 078/375] ChatMessages: Add attachments. --- lib/pleroma/web/activity_pub/builder.ex | 33 ++++++--- .../web/activity_pub/object_validator.ex | 11 ++- .../object_validators/attachment_validator.ex | 72 +++++++++++++++++++ .../chat_message_validator.ex | 6 +- .../object_validators/url_object_validator.ex | 20 ++++++ .../web/api_spec/operations/chat_operation.ex | 3 +- .../web/api_spec/schemas/chat_message.ex | 6 +- lib/pleroma/web/common_api/common_api.ex | 6 +- .../controllers/chat_controller.ex | 6 +- .../pleroma_api/views/chat_message_view.ex | 5 +- .../activity_pub/object_validator_test.exs | 50 ++++++++++++- .../types/object_id_test.exs | 4 ++ .../controllers/chat_controller_test.exs | 27 +++++++ .../views/chat_message_view_test.exs | 12 +++- 14 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex create mode 100644 lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 7f9c071b3..67e65c7b9 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -23,17 +23,28 @@ def create(actor, object, recipients) do }, []} end - def chat_message(actor, recipient, content) do - {:ok, - %{ - "id" => Utils.generate_object_id(), - "actor" => actor.ap_id, - "type" => "ChatMessage", - "to" => [recipient], - "content" => content, - "published" => DateTime.utc_now() |> DateTime.to_iso8601(), - "emoji" => Emoji.Formatter.get_emoji_map(content) - }, []} + def chat_message(actor, recipient, content, opts \\ []) do + basic = %{ + "id" => Utils.generate_object_id(), + "actor" => actor.ap_id, + "type" => "ChatMessage", + "to" => [recipient], + "content" => content, + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "emoji" => Emoji.Formatter.get_emoji_map(content) + } + + case opts[:attachment] do + %Object{data: attachment_data} -> + { + :ok, + Map.put(basic, "attachment", attachment_data), + [] + } + + _ -> + {:ok, basic, []} + end end @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 20c7cceb6..d6c14f7b8 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -63,11 +63,18 @@ def stringify_keys(%{__struct__: _} = object) do |> stringify_keys end - def stringify_keys(object) do + def stringify_keys(object) when is_map(object) do object - |> Map.new(fn {key, val} -> {to_string(key), val} end) + |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end) end + def stringify_keys(object) when is_list(object) do + object + |> Enum.map(&stringify_keys/1) + end + + def stringify_keys(object), do: object + def fetch_actor(object) do with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do User.get_or_fetch_by_ap_id(actor) diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex new file mode 100644 index 000000000..16ed49051 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field(:type, :string) + field(:mediaType, :string) + field(:name, :string) + + embeds_many(:url, UrlObjectValidator) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def changeset(struct, data) do + data = + data + |> fix_media_type() + |> fix_url() + + struct + |> cast(data, [:type, :mediaType, :name]) + |> cast_embed(:url, required: true) + end + + def fix_media_type(data) do + data + |> Map.put_new("mediaType", data["mimeType"]) + end + + def fix_url(data) do + case data["url"] do + url when is_binary(url) -> + data + |> Map.put( + "url", + [ + %{ + "href" => url, + "type" => "Link", + "mediaType" => data["mediaType"] + } + ] + ) + + _ -> + data + end + end + + def validate_data(cng) do + cng + |> validate_required([:mediaType, :url, :type]) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index e87c1ac2e..99ffeba28 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.Types + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator import Ecto.Changeset import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] @@ -22,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do field(:actor, Types.ObjectID) field(:published, Types.DateTime) field(:emoji, :map, default: %{}) + + embeds_one(:attachment, AttachmentValidator) end def cast_and_apply(data) do @@ -51,7 +54,8 @@ def changeset(struct, data) do data = fix(data) struct - |> cast(data, __schema__(:fields)) + |> cast(data, List.delete(__schema__(:fields), :attachment)) + |> cast_embed(:attachment) end def validate_data(data_cng) do diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex new file mode 100644 index 000000000..47e231150 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex @@ -0,0 +1,20 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + @primary_key false + + embedded_schema do + field(:type, :string) + field(:href, Types.Uri) + field(:mediaType, :string) + end + + def changeset(struct, data) do + struct + |> cast(data, __schema__(:fields)) + |> validate_required([:type, :href, :mediaType]) + end +end diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 0fe0e07b2..8b9dc2e44 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -236,7 +236,8 @@ def chat_message_create do description: "POST body for creating an chat message", type: :object, properties: %{ - content: %Schema{type: :string, description: "The content of your message"} + content: %Schema{type: :string, description: "The content of your message"}, + media_id: %Schema{type: :string, description: "The id of an upload"} }, required: [:content], example: %{ diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex index 7c93b0c83..89e062ddd 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do chat_id: %Schema{type: :string}, content: %Schema{type: :string}, created_at: %Schema{type: :string, format: :"date-time"}, - emojis: %Schema{type: :array} + emojis: %Schema{type: :array}, + attachment: %Schema{type: :object, nullable: true} }, example: %{ "account_id" => "someflakeid", @@ -32,7 +33,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do "url" => "https://dontbulling.me/emoji/Firefox.gif" } ], - "id" => "14" + "id" => "14", + "attachment" => nil } }) end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index e428cc17d..38b5c6f7c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -25,14 +25,16 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger - def post_chat_message(%User{} = user, %User{} = recipient, content) do + def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do with :ok <- validate_chat_content_length(content), + maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), {_, {:ok, chat_message_data, _meta}} <- {:build_object, Builder.chat_message( user, recipient.ap_id, - content |> Formatter.html_escape("text/plain") + content |> Formatter.html_escape("text/plain"), + attachment: maybe_attachment )}, {_, {:ok, create_activity_data, _meta}} <- {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index bedae73bd..450d85332 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -36,14 +36,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation def post_chat_message( - %{body_params: %{content: content}, assigns: %{user: %{id: user_id} = user}} = conn, + %{body_params: %{content: content} = params, assigns: %{user: %{id: user_id} = user}} = + conn, %{ id: id } ) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), - {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, content), + {:ok, activity} <- + CommonAPI.post_chat_message(user, recipient, content, media_id: params[:media_id]), message <- Object.normalize(activity) do conn |> put_view(ChatMessageView) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index a821479ab..b088a8734 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -23,7 +23,10 @@ def render( chat_id: chat_id |> to_string(), account_id: User.get_cached_by_ap_id(chat_message["actor"]).id, created_at: Utils.to_masto_date(chat_message["published"]), - emojis: StatusView.build_emojis(chat_message["emoji"]) + emojis: StatusView.build_emojis(chat_message["emoji"]), + attachment: + chat_message["attachment"] && + StatusView.render("attachment.json", attachment: chat_message["attachment"]) } end diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 60db7187f..951ed7800 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -2,14 +2,41 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do use Pleroma.DataCase alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI import Pleroma.Factory + describe "attachments" do + test "it turns mastodon attachments into our attachments" do + attachment = %{ + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type" => "Document", + "name" => nil, + "mediaType" => "image/jpeg" + } + + {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert [ + %{ + href: + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + type: "Link", + mediaType: "image/jpeg" + } + ] = attachment.url + end + end + describe "chat message create activities" do test "it is invalid if the object already exists" do user = insert(:user) @@ -52,7 +79,28 @@ test "it is invalid if the object data has a different `to` or `actor` field" do test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - assert object == valid_chat_message + assert Map.put(valid_chat_message, "attachment", nil) == object + end + + test "validates for a basic object with an attachment", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] end test "does not validate if the message is longer than the remote_limit", %{ diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs index 834213182..c8911948e 100644 --- a/test/web/activity_pub/object_validators/types/object_id_test.exs +++ b/test/web/activity_pub/object_validators/types/object_id_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID use Pleroma.DataCase diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index cdb2683c8..72a9a91ff 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do alias Pleroma.Chat alias Pleroma.Web.CommonAPI + alias Pleroma.Web.ActivityPub.ActivityPub import Pleroma.Factory @@ -49,6 +50,32 @@ test "it posts a message to the chat", %{conn: conn, user: user} do assert result["content"] == "Hallo!!" assert result["chat_id"] == chat.id |> to_string() end + + test "it works with an attachment", %{conn: conn, user: user} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ + "content" => "Hallo!!", + "media_id" => to_string(upload.id) + }) + |> json_response_and_validate_schema(200) + + assert result["content"] == "Hallo!!" + assert result["chat_id"] == chat.id |> to_string() + end end describe "GET /api/v1/pleroma/chats/:id/messages" do diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index 5c4c8b0d5..a13a41daa 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -9,12 +9,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.ActivityPub.ActivityPub import Pleroma.Factory test "it displays a chat message" do user = insert(:user) recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") chat = Chat.get(user.id, recipient.ap_id) @@ -30,7 +39,7 @@ test "it displays a chat message" do assert chat_message[:created_at] assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) - {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk") + {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id) object = Object.normalize(activity) @@ -40,5 +49,6 @@ test "it displays a chat message" do assert chat_message_two[:content] == "gkgkgk" assert chat_message_two[:account_id] == recipient.id assert chat_message_two[:chat_id] == chat_message[:chat_id] + assert chat_message_two[:attachment] end end From fc9d0b6eec1b206a27f4ec19f7939b3318a209ef Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 6 May 2020 16:31:21 +0200 Subject: [PATCH 079/375] Credo fixes. --- .../activity_pub/object_validators/chat_message_validator.ex | 2 +- test/web/activity_pub/object_validator_test.exs | 2 +- test/web/pleroma_api/controllers/chat_controller_test.exs | 2 +- test/web/pleroma_api/views/chat_message_view_test.exs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 99ffeba28..e40c80ab4 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do use Ecto.Schema alias Pleroma.User - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 951ed7800..fcc54c8a1 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 72a9a91ff..b4b73da90 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Chat - alias Pleroma.Web.CommonAPI alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI import Pleroma.Factory diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index a13a41daa..d7a2d10a5 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -7,9 +7,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do alias Pleroma.Chat alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.ChatMessageView - alias Pleroma.Web.ActivityPub.ActivityPub import Pleroma.Factory From d0bf8cfb8f852a16259af4b808565cdfd58f5e61 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 8 May 2020 14:11:58 +0200 Subject: [PATCH 080/375] Credo fixes. --- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 28b519432..c8b675d54 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do liked object, a `Follow` activity will add the user to the follower collection, and so on. """ - alias Pleroma.Chat alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo From 03529f6a0528ed01c7a956bb80628910584a9580 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 8 May 2020 18:26:35 +0200 Subject: [PATCH 081/375] Transmogrifier: Don't modify attachments for chats. --- lib/pleroma/web/activity_pub/transmogrifier.ex | 3 +++ test/web/common_api/common_api_test.exs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 29f668cad..f04dec6be 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -1114,6 +1114,9 @@ def add_attributed_to(object) do Map.put(object, "attributedTo", attributed_to) end + # TODO: Revisit this + def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object + def prepare_attachments(object) do attachments = object diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 61affda5d..5501ba18b 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -54,6 +54,8 @@ test "it posts a chat message" do assert Chat.get(author.id, recipient.ap_id) assert Chat.get(recipient.id, author.ap_id) + + assert :ok == Pleroma.Web.Federator.perform(:publish, activity) end test "it reject messages over the local limit" do From 0c2b09a9ba771b3b04a0a08ed940823bd8601a9f Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 8 May 2020 22:08:11 +0300 Subject: [PATCH 082/375] Add migration for counter_cache table update --- ...00508092434_update_counter_cache_table.exs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 priv/repo/migrations/20200508092434_update_counter_cache_table.exs diff --git a/priv/repo/migrations/20200508092434_update_counter_cache_table.exs b/priv/repo/migrations/20200508092434_update_counter_cache_table.exs new file mode 100644 index 000000000..81a8d6397 --- /dev/null +++ b/priv/repo/migrations/20200508092434_update_counter_cache_table.exs @@ -0,0 +1,144 @@ +defmodule Pleroma.Repo.Migrations.UpdateCounterCacheTable do + use Ecto.Migration + + @function_name "update_status_visibility_counter_cache" + @trigger_name "status_visibility_counter_cache_trigger" + + def up do + execute("drop trigger if exists #{@trigger_name} on activities") + execute("drop function if exists #{@function_name}()") + drop_if_exists(unique_index(:counter_cache, [:name])) + drop_if_exists(table(:counter_cache)) + + create_if_not_exists table(:counter_cache) do + add(:instance, :string, null: false) + add(:direct, :bigint, null: false, default: 0) + add(:private, :bigint, null: false, default: 0) + add(:unlisted, :bigint, null: false, default: 0) + add(:public, :bigint, null: false, default: 0) + end + + create_if_not_exists(unique_index(:counter_cache, [:instance])) + + """ + CREATE OR REPLACE FUNCTION #{@function_name}() + RETURNS TRIGGER AS + $$ + DECLARE + token_id smallint; + hostname character varying(255); + visibility_new character varying(64); + visibility_old character varying(64); + actor character varying(255); + BEGIN + SELECT "tokid" INTO "token_id" FROM ts_token_type('default') WHERE "alias" = 'host'; + IF TG_OP = 'DELETE' THEN + actor := OLD.actor; + ELSE + actor := NEW.actor; + END IF; + SELECT "token" INTO "hostname" FROM ts_parse('default', actor) WHERE "tokid" = token_id; + IF hostname IS NULL THEN + hostname := split_part(actor, '/', 3); + END IF; + IF TG_OP = 'INSERT' THEN + visibility_new := activity_visibility(NEW.actor, NEW.recipients, NEW.data); + IF NEW.data->>'type' = 'Create' THEN + EXECUTE format('INSERT INTO "counter_cache" ("instance", %1$I) VALUES ($1, 1) + ON CONFLICT ("instance") DO + UPDATE SET %1$I = "counter_cache".%1$I + 1', visibility_new) + USING hostname; + END IF; + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + visibility_new := activity_visibility(NEW.actor, NEW.recipients, NEW.data); + visibility_old := activity_visibility(OLD.actor, OLD.recipients, OLD.data); + IF (NEW.data->>'type' = 'Create') and (OLD.data->>'type' = 'Create') and visibility_new != visibility_old THEN + EXECUTE format('UPDATE "counter_cache" SET + %1$I = greatest("counter_cache".%1$I - 1, 0), + %2$I = "counter_cache".%2$I + 1 + WHERE "instance" = $1', visibility_old, visibility_new) + USING hostname; + END IF; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + IF OLD.data->>'type' = 'Create' THEN + visibility_old := activity_visibility(OLD.actor, OLD.recipients, OLD.data); + EXECUTE format('UPDATE "counter_cache" SET + %1$I = greatest("counter_cache".%1$I - 1, 0) + WHERE "instance" = $1', visibility_old) + USING hostname; + END IF; + RETURN OLD; + END IF; + END; + $$ + LANGUAGE 'plpgsql'; + """ + |> execute() + + execute("DROP TRIGGER IF EXISTS #{@trigger_name} ON activities") + + """ + CREATE TRIGGER #{@trigger_name} + BEFORE + INSERT + OR UPDATE of recipients, data + OR DELETE + ON activities + FOR EACH ROW + EXECUTE PROCEDURE #{@function_name}(); + """ + |> execute() + end + + def down do + execute("DROP TRIGGER IF EXISTS #{@trigger_name} ON activities") + execute("DROP FUNCTION IF EXISTS #{@function_name}()") + drop_if_exists(unique_index(:counter_cache, [:instance])) + drop_if_exists(table(:counter_cache)) + + create_if_not_exists table(:counter_cache) do + add(:name, :string, null: false) + add(:count, :bigint, null: false, default: 0) + end + + create_if_not_exists(unique_index(:counter_cache, [:name])) + + """ + CREATE OR REPLACE FUNCTION #{@function_name}() + RETURNS TRIGGER AS + $$ + DECLARE + BEGIN + IF TG_OP = 'INSERT' THEN + IF NEW.data->>'type' = 'Create' THEN + EXECUTE 'INSERT INTO counter_cache (name, count) VALUES (''status_visibility_' || activity_visibility(NEW.actor, NEW.recipients, NEW.data) || ''', 1) ON CONFLICT (name) DO UPDATE SET count = counter_cache.count + 1'; + END IF; + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + IF (NEW.data->>'type' = 'Create') and (OLD.data->>'type' = 'Create') and activity_visibility(NEW.actor, NEW.recipients, NEW.data) != activity_visibility(OLD.actor, OLD.recipients, OLD.data) THEN + EXECUTE 'INSERT INTO counter_cache (name, count) VALUES (''status_visibility_' || activity_visibility(NEW.actor, NEW.recipients, NEW.data) || ''', 1) ON CONFLICT (name) DO UPDATE SET count = counter_cache.count + 1'; + EXECUTE 'update counter_cache SET count = counter_cache.count - 1 where count > 0 and name = ''status_visibility_' || activity_visibility(OLD.actor, OLD.recipients, OLD.data) || ''';'; + END IF; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + IF OLD.data->>'type' = 'Create' THEN + EXECUTE 'update counter_cache SET count = counter_cache.count - 1 where count > 0 and name = ''status_visibility_' || activity_visibility(OLD.actor, OLD.recipients, OLD.data) || ''';'; + END IF; + RETURN OLD; + END IF; + END; + $$ + LANGUAGE 'plpgsql'; + """ + |> execute() + + """ + CREATE TRIGGER #{@trigger_name} BEFORE INSERT OR UPDATE of recipients, data OR DELETE ON activities + FOR EACH ROW + EXECUTE PROCEDURE #{@function_name}(); + """ + |> execute() + end +end From 39d2f2118aed7906cb352d8a37f22da73f3a3aa3 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 01:20:50 +0300 Subject: [PATCH 083/375] update counter_cache logic --- .../tasks/pleroma/refresh_counter_cache.ex | 42 ++++++++---- lib/pleroma/counter_cache.ex | 66 +++++++++++++++---- lib/pleroma/stats.ex | 8 +-- 3 files changed, 82 insertions(+), 34 deletions(-) diff --git a/lib/mix/tasks/pleroma/refresh_counter_cache.ex b/lib/mix/tasks/pleroma/refresh_counter_cache.ex index 15b4dbfa6..280201bef 100644 --- a/lib/mix/tasks/pleroma/refresh_counter_cache.ex +++ b/lib/mix/tasks/pleroma/refresh_counter_cache.ex @@ -17,30 +17,46 @@ defmodule Mix.Tasks.Pleroma.RefreshCounterCache do def run([]) do Mix.Pleroma.start_pleroma() - ["public", "unlisted", "private", "direct"] - |> Enum.each(fn visibility -> - count = status_visibility_count_query(visibility) - name = "status_visibility_#{visibility}" - CounterCache.set(name, count) - Mix.Pleroma.shell_info("Set #{name} to #{count}") + Activity + |> distinct([a], true) + |> select([a], fragment("split_part(?, '/', 3)", a.actor)) + |> Repo.all() + |> Enum.each(fn instance -> + counters = instance_counters(instance) + CounterCache.set(instance, counters) + Mix.Pleroma.shell_info("Setting #{instance} counters: #{inspect(counters)}") end) Mix.Pleroma.shell_info("Done") end - defp status_visibility_count_query(visibility) do + defp instance_counters(instance) do + counters = %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0} + Activity - |> where( + |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) + |> where([a], like(a.actor, ^"%#{instance}%")) + |> select( + [a], + {fragment( + "activity_visibility(?, ?, ?)", + a.actor, + a.recipients, + a.data + ), count(a.id)} + ) + |> group_by( [a], fragment( - "activity_visibility(?, ?, ?) = ?", + "activity_visibility(?, ?, ?)", a.actor, a.recipients, - a.data, - ^visibility + a.data ) ) - |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) - |> Repo.aggregate(:count, :id, timeout: :timer.minutes(30)) + |> Repo.all(timeout: :timer.minutes(30)) + |> Enum.reduce(counters, fn {visibility, count}, acc -> + Map.put(acc, visibility, count) + end) end end diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex index 4d348a413..b469e7b50 100644 --- a/lib/pleroma/counter_cache.ex +++ b/lib/pleroma/counter_cache.ex @@ -10,32 +10,70 @@ defmodule Pleroma.CounterCache do import Ecto.Query schema "counter_cache" do - field(:name, :string) - field(:count, :integer) + field(:instance, :string) + field(:public, :integer) + field(:unlisted, :integer) + field(:private, :integer) + field(:direct, :integer) end def changeset(struct, params) do struct - |> cast(params, [:name, :count]) - |> validate_required([:name]) - |> unique_constraint(:name) + |> cast(params, [:instance, :public, :unlisted, :private, :direct]) + |> validate_required([:instance]) + |> unique_constraint(:instance) end - def get_as_map(names) when is_list(names) do + def get_by_instance(instance) do CounterCache - |> where([cc], cc.name in ^names) - |> Repo.all() - |> Enum.group_by(& &1.name, & &1.count) - |> Map.new(fn {k, v} -> {k, hd(v)} end) + |> select([c], %{ + "public" => c.public, + "unlisted" => c.unlisted, + "private" => c.private, + "direct" => c.direct + }) + |> where([c], c.instance == ^instance) + |> Repo.one() + |> case do + nil -> %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0} + val -> val + end end - def set(name, count) do + def get_as_map() do + CounterCache + |> select([c], %{ + "public" => sum(c.public), + "unlisted" => sum(c.unlisted), + "private" => sum(c.private), + "direct" => sum(c.direct) + }) + |> Repo.one() + end + + def set(instance, values) do + params = + Enum.reduce( + ["public", "private", "unlisted", "direct"], + %{"instance" => instance}, + fn param, acc -> + Map.put_new(acc, param, Map.get(values, param, 0)) + end + ) + %CounterCache{} - |> changeset(%{"name" => name, "count" => count}) + |> changeset(params) |> Repo.insert( - on_conflict: [set: [count: count]], + on_conflict: [ + set: [ + public: params["public"], + private: params["private"], + unlisted: params["unlisted"], + direct: params["direct"] + ] + ], returning: true, - conflict_target: :name + conflict_target: :instance ) end end diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 6b3a8a41f..4e355bd5c 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -98,13 +98,7 @@ def calculate_stat_data do end def get_status_visibility_count do - counter_cache = - CounterCache.get_as_map([ - "status_visibility_public", - "status_visibility_private", - "status_visibility_unlisted", - "status_visibility_direct" - ]) + counter_cache = CounterCache.get_as_map() %{ public: counter_cache["status_visibility_public"] || 0, From cbe383ae832f13d5d2a20ee8fb5e85498205fbc3 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 11:30:37 +0300 Subject: [PATCH 084/375] Update stats admin endpoint --- lib/pleroma/counter_cache.ex | 6 +- lib/pleroma/stats.ex | 15 ++--- .../web/admin_api/admin_api_controller.ex | 7 +-- ...00508092434_update_counter_cache_table.exs | 8 ++- test/stats_test.exs | 55 ++++++++++++++++--- .../admin_api/admin_api_controller_test.exs | 20 +++++++ 6 files changed, 87 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex index b469e7b50..a940b5e50 100644 --- a/lib/pleroma/counter_cache.ex +++ b/lib/pleroma/counter_cache.ex @@ -40,7 +40,7 @@ def get_by_instance(instance) do end end - def get_as_map() do + def get_sum() do CounterCache |> select([c], %{ "public" => sum(c.public), @@ -49,6 +49,10 @@ def get_as_map() do "direct" => sum(c.direct) }) |> Repo.one() + |> Enum.map(fn {visibility, dec_count} -> + {visibility, Decimal.to_integer(dec_count)} + end) + |> Enum.into(%{}) end def set(instance, values) do diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 4e355bd5c..9a03f01db 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -97,14 +97,11 @@ def calculate_stat_data do } end - def get_status_visibility_count do - counter_cache = CounterCache.get_as_map() - - %{ - public: counter_cache["status_visibility_public"] || 0, - unlisted: counter_cache["status_visibility_unlisted"] || 0, - private: counter_cache["status_visibility_private"] || 0, - direct: counter_cache["status_visibility_direct"] || 0 - } + def get_status_visibility_count(instance \\ nil) do + if is_nil(instance) do + CounterCache.get_sum() + else + CounterCache.get_by_instance(instance) + end end end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 9f1fd3aeb..4db9f4cac 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -1122,11 +1122,10 @@ def oauth_app_delete(conn, params) do end end - def stats(conn, _) do - count = Stats.get_status_visibility_count() + def stats(conn, params) do + counters = Stats.get_status_visibility_count(params["instance"]) - conn - |> json(%{"status_visibility" => count}) + json(conn, %{"status_visibility" => counters}) end defp errors(conn, {:error, :not_found}) do diff --git a/priv/repo/migrations/20200508092434_update_counter_cache_table.exs b/priv/repo/migrations/20200508092434_update_counter_cache_table.exs index 81a8d6397..3d9bfc877 100644 --- a/priv/repo/migrations/20200508092434_update_counter_cache_table.exs +++ b/priv/repo/migrations/20200508092434_update_counter_cache_table.exs @@ -43,7 +43,8 @@ def up do END IF; IF TG_OP = 'INSERT' THEN visibility_new := activity_visibility(NEW.actor, NEW.recipients, NEW.data); - IF NEW.data->>'type' = 'Create' THEN + IF NEW.data->>'type' = 'Create' + AND visibility_new IN ('public', 'unlisted', 'private', 'direct') THEN EXECUTE format('INSERT INTO "counter_cache" ("instance", %1$I) VALUES ($1, 1) ON CONFLICT ("instance") DO UPDATE SET %1$I = "counter_cache".%1$I + 1', visibility_new) @@ -53,7 +54,10 @@ def up do ELSIF TG_OP = 'UPDATE' THEN visibility_new := activity_visibility(NEW.actor, NEW.recipients, NEW.data); visibility_old := activity_visibility(OLD.actor, OLD.recipients, OLD.data); - IF (NEW.data->>'type' = 'Create') and (OLD.data->>'type' = 'Create') and visibility_new != visibility_old THEN + IF (NEW.data->>'type' = 'Create') + AND (OLD.data->>'type' = 'Create') + AND visibility_new != visibility_old + AND visibility_new IN ('public', 'unlisted', 'private', 'direct') THEN EXECUTE format('UPDATE "counter_cache" SET %1$I = greatest("counter_cache".%1$I - 1, 0), %2$I = "counter_cache".%2$I + 1 diff --git a/test/stats_test.exs b/test/stats_test.exs index c1aeb2c7f..33ed0b7dd 100644 --- a/test/stats_test.exs +++ b/test/stats_test.exs @@ -17,10 +17,11 @@ test "it ignores internal users" do end end - describe "status visibility count" do + describe "status visibility sum count" do test "on new status" do + instance2 = "instance2.tld" user = insert(:user) - other_user = insert(:user) + other_user = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"}) @@ -45,24 +46,24 @@ test "on new status" do }) end) - assert %{direct: 3, private: 4, public: 1, unlisted: 2} = + assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = Pleroma.Stats.get_status_visibility_count() end test "on status delete" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"}) - assert %{public: 1} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 1} = Pleroma.Stats.get_status_visibility_count() CommonAPI.delete(activity.id, user) - assert %{public: 0} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 0} = Pleroma.Stats.get_status_visibility_count() end test "on status visibility update" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"}) - assert %{public: 1, private: 0} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 1, "private" => 0} = Pleroma.Stats.get_status_visibility_count() {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{"visibility" => "private"}) - assert %{public: 0, private: 1} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 0, "private" => 1} = Pleroma.Stats.get_status_visibility_count() end test "doesn't count unrelated activities" do @@ -73,8 +74,46 @@ test "doesn't count unrelated activities" do CommonAPI.favorite(other_user, activity.id) CommonAPI.repeat(activity.id, other_user) - assert %{direct: 0, private: 0, public: 1, unlisted: 0} = + assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} = Pleroma.Stats.get_status_visibility_count() end end + + describe "status visibility by instance count" do + test "single instance" do + local_instance = Pleroma.Web.Endpoint.url() |> String.split("//") |> Enum.at(1) + instance2 = "instance2.tld" + user1 = insert(:user) + user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user1, %{"visibility" => "public", "status" => "hey"}) + + Enum.each(1..5, fn _ -> + CommonAPI.post(user1, %{ + "visibility" => "unlisted", + "status" => "hey" + }) + end) + + Enum.each(1..10, fn _ -> + CommonAPI.post(user1, %{ + "visibility" => "direct", + "status" => "hey @#{user2.nickname}" + }) + end) + + Enum.each(1..20, fn _ -> + CommonAPI.post(user2, %{ + "visibility" => "private", + "status" => "hey" + }) + end) + + assert %{"direct" => 10, "private" => 0, "public" => 1, "unlisted" => 5} = + Pleroma.Stats.get_status_visibility_count(local_instance) + + assert %{"direct" => 0, "private" => 20, "public" => 0, "unlisted" => 0} = + Pleroma.Stats.get_status_visibility_count(instance2) + end + end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 4697af50e..c3de89ac0 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -3612,6 +3612,26 @@ test "status visibility count", %{conn: conn} do assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} = response["status_visibility"] end + + test "by instance", %{conn: conn} do + admin = insert(:user, is_admin: true) + user1 = insert(:user) + instance2 = "instance2.tld" + user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user1, %{"visibility" => "public", "status" => "hey"}) + CommonAPI.post(user2, %{"visibility" => "unlisted", "status" => "hey"}) + CommonAPI.post(user2, %{"visibility" => "private", "status" => "hey"}) + + response = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/stats", instance: instance2) + |> json_response(200) + + assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} = + response["status_visibility"] + end end describe "POST /api/pleroma/admin/oauth_app" do From 01b06d6dbfdeff7e1733d575fb94eee4dafbb56a Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 11:43:31 +0300 Subject: [PATCH 085/375] Show progress in refresh_counter_cache task --- .../tasks/pleroma/refresh_counter_cache.ex | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/mix/tasks/pleroma/refresh_counter_cache.ex b/lib/mix/tasks/pleroma/refresh_counter_cache.ex index 280201bef..b44e2545d 100644 --- a/lib/mix/tasks/pleroma/refresh_counter_cache.ex +++ b/lib/mix/tasks/pleroma/refresh_counter_cache.ex @@ -17,14 +17,21 @@ defmodule Mix.Tasks.Pleroma.RefreshCounterCache do def run([]) do Mix.Pleroma.start_pleroma() - Activity - |> distinct([a], true) - |> select([a], fragment("split_part(?, '/', 3)", a.actor)) - |> Repo.all() - |> Enum.each(fn instance -> + instances = + Activity + |> distinct([a], true) + |> select([a], fragment("split_part(?, '/', 3)", a.actor)) + |> Repo.all() + + instances + |> Enum.with_index(1) + |> Enum.each(fn {instance, i} -> counters = instance_counters(instance) CounterCache.set(instance, counters) - Mix.Pleroma.shell_info("Setting #{instance} counters: #{inspect(counters)}") + + Mix.Pleroma.shell_info( + "[#{i}/#{length(instances)}] Setting #{instance} counters: #{inspect(counters)}" + ) end) Mix.Pleroma.shell_info("Done") From 5c368b004b1a736836d4bc9f68c54714a33056cd Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 11:49:54 +0300 Subject: [PATCH 086/375] Fix refresh_counter_cache test --- test/tasks/refresh_counter_cache_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tasks/refresh_counter_cache_test.exs b/test/tasks/refresh_counter_cache_test.exs index b63f44c08..378664148 100644 --- a/test/tasks/refresh_counter_cache_test.exs +++ b/test/tasks/refresh_counter_cache_test.exs @@ -37,7 +37,7 @@ test "counts statuses" do assert capture_io(fn -> Mix.Tasks.Pleroma.RefreshCounterCache.run([]) end) =~ "Done\n" - assert %{direct: 3, private: 4, public: 1, unlisted: 2} = + assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = Pleroma.Stats.get_status_visibility_count() end end From 4f265397179e7286f27fafaf8365a0edc6972448 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 11:59:49 +0300 Subject: [PATCH 087/375] Fix credo warning --- lib/pleroma/counter_cache.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex index a940b5e50..aa6d38687 100644 --- a/lib/pleroma/counter_cache.ex +++ b/lib/pleroma/counter_cache.ex @@ -40,7 +40,7 @@ def get_by_instance(instance) do end end - def get_sum() do + def get_sum do CounterCache |> select([c], %{ "public" => sum(c.public), From 56819f7f0604e5d4eb69edba1d6828256acbc7fe Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 13:13:26 +0300 Subject: [PATCH 088/375] Use index on refresh_counter_cache --- lib/mix/tasks/pleroma/refresh_counter_cache.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/refresh_counter_cache.ex b/lib/mix/tasks/pleroma/refresh_counter_cache.ex index b44e2545d..efcbaa3b1 100644 --- a/lib/mix/tasks/pleroma/refresh_counter_cache.ex +++ b/lib/mix/tasks/pleroma/refresh_counter_cache.ex @@ -42,7 +42,7 @@ defp instance_counters(instance) do Activity |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) - |> where([a], like(a.actor, ^"%#{instance}%")) + |> where([a], fragment("split_part(?, '/', 3) = ?", a.actor, ^instance)) |> select( [a], {fragment( From 4c197023903a183790fb2dc67b5637bfabd53bcb Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 9 May 2020 14:32:08 +0300 Subject: [PATCH 089/375] Add docs --- CHANGELOG.md | 12 ++++++++++++ docs/API/admin_api.md | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d469793f0..f2c9e106e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed
API Changes + - **Breaking:** Emoji API: changed methods and renamed routes.
+
+ Admin API Changes + +- Status visibility stats: now can return stats per instance. + +- Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`) +
+ ### Removed - **Breaking:** removed `with_move` parameter from notifications timeline. @@ -76,6 +85,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 2. Run database migrations (inside Pleroma directory): - OTP: `./bin/pleroma_ctl migrate` - From Source: `mix ecto.migrate` +3. Reset status visibility counters (inside Pleroma directory): + - OTP: `./bin/pleroma_ctl refresh_counter_cache` + - From Source: `mix pleroma.refresh_counter_cache` ## [2.0.2] - 2020-04-08 diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index c455047cc..fa74e7460 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1096,6 +1096,10 @@ Loads json generated from `config/descriptions.exs`. ### Stats +- Query Params: + - *optional* `instance`: **string** instance hostname (without protocol) to get stats for +- Example: `https://mypleroma.org/api/pleroma/admin/stats?instance=lain.com` + - Response: ```json From f3f8ed9e19f1ab8863141ba8b31773c54f4771fb Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 10 May 2020 09:13:24 +0300 Subject: [PATCH 090/375] Set sum types in query --- lib/pleroma/counter_cache.ex | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex index aa6d38687..ebd1f603d 100644 --- a/lib/pleroma/counter_cache.ex +++ b/lib/pleroma/counter_cache.ex @@ -43,16 +43,12 @@ def get_by_instance(instance) do def get_sum do CounterCache |> select([c], %{ - "public" => sum(c.public), - "unlisted" => sum(c.unlisted), - "private" => sum(c.private), - "direct" => sum(c.direct) + "public" => type(sum(c.public), :integer), + "unlisted" => type(sum(c.unlisted), :integer), + "private" => type(sum(c.private), :integer), + "direct" => type(sum(c.direct), :integer) }) |> Repo.one() - |> Enum.map(fn {visibility, dec_count} -> - {visibility, Decimal.to_integer(dec_count)} - end) - |> Enum.into(%{}) end def set(instance, values) do From 1054e897622d0a0727f30169d64c83a253a3d11e Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 12:30:24 +0200 Subject: [PATCH 091/375] ChatOperation: Add media id to example --- lib/pleroma/web/api_spec/operations/chat_operation.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 8b9dc2e44..16d3d5e22 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -241,7 +241,8 @@ def chat_message_create do }, required: [:content], example: %{ - "content" => "Hey wanna buy feet pics?" + "content" => "Hey wanna buy feet pics?", + "media_id" => "134234" } } end From e297d8c649a03510023cff61dc6e0c7131bc29dc Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 12:34:12 +0200 Subject: [PATCH 092/375] Documentation: Add attachment docs --- docs/API/chats.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/API/chats.md b/docs/API/chats.md index 8d925989c..3ddc13541 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -160,6 +160,7 @@ Posting a chat message for given Chat id works like this: Parameters: - content: The text content of the message +- media_id: The id of an upload that will be attached to the message. Currently, no formatting beyond basic escaping and emoji is implemented, as well as no attachments. This will most probably change. From f335e1404a9cd19451b531e32e3591aa323761ff Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 13:00:01 +0200 Subject: [PATCH 093/375] ChatView: Add the last message to the view. --- lib/pleroma/chat.ex | 36 ++++++++++++++++++- .../controllers/chat_controller.ex | 23 ++---------- .../web/pleroma_api/views/chat_view.ex | 7 +++- test/web/pleroma_api/views/chat_view_test.exs | 17 ++++++++- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 1a092b992..6a03ee3c1 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -4,8 +4,11 @@ defmodule Pleroma.Chat do use Ecto.Schema - import Ecto.Changeset + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -23,6 +26,37 @@ defmodule Pleroma.Chat do timestamps() end + def last_message_for_chat(chat) do + messages_for_chat_query(chat) + |> order_by(desc: :id) + |> Repo.one() + end + + def messages_for_chat_query(chat) do + chat = + chat + |> Repo.preload(:user) + + from(o in Object, + where: fragment("?->>'type' = ?", o.data, "ChatMessage"), + where: + fragment( + """ + (?->>'actor' = ? and ?->'to' = ?) + OR (?->>'actor' = ? and ?->'to' = ?) + """, + o.data, + ^chat.user.ap_id, + o.data, + ^[chat.recipient], + o.data, + ^chat.recipient, + o.data, + ^[chat.user.ap_id] + ) + ) + end + def creation_cng(struct, params) do struct |> cast(params, [:user_id, :recipient, :unread]) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 450d85332..1ef3477c8 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -14,9 +14,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Web.PleromaAPI.ChatMessageView alias Pleroma.Web.PleromaAPI.ChatView - import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] - import Ecto.Query + import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] # TODO # - Error handling @@ -65,24 +64,8 @@ def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do messages = - from(o in Object, - where: fragment("?->>'type' = ?", o.data, "ChatMessage"), - where: - fragment( - """ - (?->>'actor' = ? and ?->'to' = ?) - OR (?->>'actor' = ? and ?->'to' = ?) - """, - o.data, - ^user.ap_id, - o.data, - ^[chat.recipient], - o.data, - ^chat.recipient, - o.data, - ^[user.ap_id] - ) - ) + chat + |> Chat.messages_for_chat_query() |> Pagination.fetch_paginated(params |> stringify_keys()) conn diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index bc3af5ef5..21f0612ff 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -8,14 +8,19 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do alias Pleroma.Chat alias Pleroma.User alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.PleromaAPI.ChatMessageView def render("show.json", %{chat: %Chat{} = chat} = opts) do recipient = User.get_cached_by_ap_id(chat.recipient) + last_message = Chat.last_message_for_chat(chat) + %{ id: chat.id |> to_string(), account: AccountView.render("show.json", Map.put(opts, :user, recipient)), - unread: chat.unread + unread: chat.unread, + last_message: + last_message && ChatMessageView.render("show.json", chat: chat, object: last_message) } end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index 1ac3483d1..8568d98c6 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -6,8 +6,11 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do use Pleroma.DataCase alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.PleromaAPI.ChatView + alias Pleroma.Web.PleromaAPI.ChatMessageView import Pleroma.Factory @@ -22,7 +25,19 @@ test "it represents a chat" do assert represented_chat == %{ id: "#{chat.id}", account: AccountView.render("show.json", user: recipient), - unread: 0 + unread: 0, + last_message: nil } + + {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") + + chat_message = Object.normalize(chat_message_creation, false) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + assert represented_chat[:last_message] == + ChatMessageView.render("show.json", chat: chat, object: chat_message) end end From 17be3ff669865102848df034045eb2889eed3976 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 13:01:20 +0200 Subject: [PATCH 094/375] Documentation: Add last_message to chat docs. --- docs/API/chats.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 3ddc13541..1f6175f77 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -62,7 +62,8 @@ Returned data: ... }, "id" : "1", - "unread" : 2 + "unread" : 2, + "last_message" : {...} // The last message in that chat } ``` @@ -105,7 +106,8 @@ Returned data: ... }, "id" : "1", - "unread" : 2 + "unread" : 2, + "last_message" : {...} // The last message in that chat } ] ``` From 172d9b11936bb029093eac430c58c89a81592c08 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 13:08:01 +0200 Subject: [PATCH 095/375] Chat: Add last_message to schema. --- lib/pleroma/web/api_spec/schemas/chat.ex | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex index 4d385d6ab..8aaa4a792 100644 --- a/lib/pleroma/web/api_spec/schemas/chat.ex +++ b/lib/pleroma/web/api_spec/schemas/chat.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage require OpenApiSpex @@ -12,9 +13,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do description: "Response schema for a Chat", type: :object, properties: %{ - id: %Schema{type: :string, nullable: false}, - account: %Schema{type: :object, nullable: false}, - unread: %Schema{type: :integer, nullable: false} + id: %Schema{type: :string}, + account: %Schema{type: :object}, + unread: %Schema{type: :integer}, + last_message: %Schema{type: ChatMessage, nullable: true} }, example: %{ "account" => %{ @@ -64,7 +66,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do "url" => "https://dontbulling.me/users/lain" }, "id" => "1", - "unread" => 2 + "unread" => 2, + "last_message" => ChatMessage.schema().example() } }) end From 8d5597ff68de22ee7b126730467649ada248aaf7 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 13:26:14 +0200 Subject: [PATCH 096/375] ChatController: Add GET /chats/:id --- .../web/api_spec/operations/chat_operation.ex | 31 +++++++++++++++++++ lib/pleroma/web/api_spec/schemas/chat.ex | 2 +- .../web/api_spec/schemas/chat_message.ex | 1 + .../controllers/chat_controller.ex | 10 +++++- lib/pleroma/web/router.ex | 1 + .../controllers/chat_controller_test.exs | 17 ++++++++++ 6 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 16d3d5e22..fe6c2f52f 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -38,6 +38,37 @@ def mark_as_read_operation do } end + def show_operation do + %Operation{ + tags: ["chat"], + summary: "Create a chat", + operationId: "ChatController.show", + parameters: [ + Operation.parameter( + :id, + :path, + :string, + "The id of the chat", + required: true, + example: "1234" + ) + ], + responses: %{ + 200 => + Operation.response( + "The existing chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["read"] + } + ] + } + end + def create_operation do %Operation{ tags: ["chat"], diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex index 8aaa4a792..c6ec07c88 100644 --- a/lib/pleroma/web/api_spec/schemas/chat.ex +++ b/lib/pleroma/web/api_spec/schemas/chat.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do id: %Schema{type: :string}, account: %Schema{type: :object}, unread: %Schema{type: :integer}, - last_message: %Schema{type: ChatMessage, nullable: true} + last_message: ChatMessage }, example: %{ "account" => %{ diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex index 89e062ddd..6e8f1a10a 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do OpenApiSpex.schema(%{ title: "ChatMessage", description: "Response schema for a ChatMessage", + nullable: true, type: :object, properties: %{ id: %Schema{type: :string}, diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 1ef3477c8..04f136dcd 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do plug( OAuthScopesPlug, - %{scopes: ["read:statuses"]} when action in [:messages, :index] + %{scopes: ["read:statuses"]} when action in [:messages, :index, :show] ) plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) @@ -100,4 +100,12 @@ def create(%{assigns: %{user: user}} = conn, params) do |> render("show.json", chat: chat) end end + + def show(%{assigns: %{user: user}} = conn, params) do + with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do + conn + |> put_view(ChatView) + |> render("show.json", chat: chat) + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 4b264c43e..3b1834d97 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -307,6 +307,7 @@ defmodule Pleroma.Web.Router do post("/chats/by-account-id/:id", ChatController, :create) get("/chats", ChatController, :index) + get("/chats/:id", ChatController, :show) get("/chats/:id/messages", ChatController, :messages) post("/chats/:id/messages", ChatController, :post_chat_message) post("/chats/:id/read", ChatController, :mark_as_read) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index b4b73da90..dda4f9e5b 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -153,6 +153,23 @@ test "it creates or returns a chat", %{conn: conn} do end end + describe "GET /api/v1/pleroma/chats/:id" do + setup do: oauth_access(["read:statuses"]) + + test "it returns a chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + end + end + describe "GET /api/v1/pleroma/chats" do setup do: oauth_access(["read:statuses"]) From 8cc8d960af87cdc1e2398a8470155b0930f43f5c Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 13:27:40 +0200 Subject: [PATCH 097/375] Documentation: Add GET /chats/:id --- docs/API/chats.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 1f6175f77..ed160abd9 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -43,12 +43,17 @@ you can call: `POST /api/v1/pleroma/chats/by-account-id/{account_id}` -The account id is the normal FlakeId of the usre - +The account id is the normal FlakeId of the user ``` POST /api/v1/pleroma/chats/by-account-id/someflakeid ``` +If you already have the id of a chat, you can also use + +``` +GET /api/v1/pleroma/chats/:id +``` + There will only ever be ONE Chat for you and a given recipient, so this call will return the same Chat if you already have one with that user. From 1b1dfb54eb092921fe9dab2c49928e5b04fa049b Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 10 May 2020 13:28:05 +0200 Subject: [PATCH 098/375] Credo fixes. --- test/web/pleroma_api/views/chat_view_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index 8568d98c6..e24e29835 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -9,8 +9,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.PleromaAPI.ChatView import Pleroma.Factory From fdb98715b8e6ced7c4037b1292fb10980a994803 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 11 May 2020 10:58:14 +0200 Subject: [PATCH 099/375] Chat: Fix wrong query. --- lib/pleroma/chat.ex | 1 + test/chat_test.exs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 6a03ee3c1..4c92a58c7 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -29,6 +29,7 @@ defmodule Pleroma.Chat do def last_message_for_chat(chat) do messages_for_chat_query(chat) |> order_by(desc: :id) + |> limit(1) |> Repo.one() end diff --git a/test/chat_test.exs b/test/chat_test.exs index 943e48111..dfcb6422e 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -6,9 +6,26 @@ defmodule Pleroma.ChatTest do use Pleroma.DataCase, async: true alias Pleroma.Chat + alias Pleroma.Web.CommonAPI import Pleroma.Factory + describe "messages" do + test "it returns the last message in a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey") + {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho") + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + message = Chat.last_message_for_chat(chat) + + assert message.data["content"] == "ho" + end + end + describe "creation and getting" do test "it only works if the recipient is a valid user (for now)" do user = insert(:user) From b5aa204eb8bf3f737d3d807a9924c0153d1b6d3e Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 12 May 2020 13:13:03 +0200 Subject: [PATCH 100/375] ChatController: Support deletion of chat messages. --- .../object_validators/delete_validator.ex | 3 ++- .../web/api_spec/operations/chat_operation.ex | 25 +++++++++++++++++++ .../controllers/chat_controller.ex | 24 +++++++++++++++++- lib/pleroma/web/router.ex | 1 + .../controllers/chat_controller_test.exs | 24 ++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index f42c03510..e5d08eb5c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -46,12 +46,13 @@ def add_deleted_activity_id(cng) do Answer Article Audio + ChatMessage Event Note Page Question - Video Tombstone + Video } def validate_data(cng) do cng diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index fe6c2f52f..8ba10c603 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -166,6 +166,31 @@ def post_chat_message_operation do } end + def delete_message_operation do + %Operation{ + tags: ["chat"], + summary: "delete_message", + operationId: "ChatController.delete_message", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:message_id, :path, :string, "The ID of the message") + ], + responses: %{ + 200 => + Operation.response( + "The deleted ChatMessage", + "application/json", + ChatMessage + ) + }, + security: [ + %{ + "oAuth" => ["write"] + } + ] + } + end + def chats_response do %Schema{ title: "ChatsResponse", diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 04f136dcd..8eed88752 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do use Pleroma.Web, :controller + alias Pleroma.Activity alias Pleroma.Chat alias Pleroma.Object alias Pleroma.Pagination @@ -22,7 +23,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} when action in [:post_chat_message, :create, :mark_as_read] + %{scopes: ["write:statuses"]} + when action in [:post_chat_message, :create, :mark_as_read, :delete_message] ) plug( @@ -34,6 +36,26 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation + def delete_message(%{assigns: %{user: %{ap_id: actor} = user}} = conn, %{ + message_id: id + }) do + with %Object{ + data: %{ + "actor" => ^actor, + "id" => object, + "to" => [recipient], + "type" => "ChatMessage" + } + } = message <- Object.get_by_id(id), + %Chat{} = chat <- Chat.get(user.id, recipient), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object), + {:ok, _delete} <- CommonAPI.delete(activity.id, user) do + conn + |> put_view(ChatMessageView) + |> render("show.json", for: user, object: message, chat: chat) + end + end + def post_chat_message( %{body_params: %{content: content} = params, assigns: %{user: %{id: user_id} = user}} = conn, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3b1834d97..0e4f45869 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -310,6 +310,7 @@ defmodule Pleroma.Web.Router do get("/chats/:id", ChatController, :show) get("/chats/:id/messages", ChatController, :messages) post("/chats/:id/messages", ChatController, :post_chat_message) + delete("/chats/:id/messages/:message_id", ChatController, :delete_message) post("/chats/:id/read", ChatController, :mark_as_read) end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index dda4f9e5b..86ccbb117 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true + alias Pleroma.Object alias Pleroma.Chat alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI @@ -78,6 +79,29 @@ test "it works with an attachment", %{conn: conn, user: user} do end end + describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do + setup do: oauth_access(["write:statuses"]) + + test "it deletes a message for the author of the message", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{object.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(object.id) + end + end + describe "GET /api/v1/pleroma/chats/:id/messages" do setup do: oauth_access(["read:statuses"]) From ec72cba43ec4f45faadf1b06a6d014cd4136707e Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 12 May 2020 13:23:09 +0200 Subject: [PATCH 101/375] Chat Controller: Add basic error handling. --- .../web/pleroma_api/controllers/chat_controller.ex | 5 +++-- .../pleroma_api/controllers/chat_controller_test.exs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 8eed88752..4ce3e7419 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -18,8 +18,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do import Ecto.Query import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] - # TODO - # - Error handling + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug( OAuthScopesPlug, @@ -53,6 +52,8 @@ def delete_message(%{assigns: %{user: %{ap_id: actor} = user}} = conn, %{ conn |> put_view(ChatMessageView) |> render("show.json", for: user, object: message, chat: chat) + else + _e -> {:error, :could_not_delete} end end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 86ccbb117..75d4903ed 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -88,6 +88,8 @@ test "it deletes a message for the author of the message", %{conn: conn, user: u {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni") + object = Object.normalize(message, false) chat = Chat.get(user.id, recipient.ap_id) @@ -99,6 +101,16 @@ test "it deletes a message for the author of the message", %{conn: conn, user: u |> json_response_and_validate_schema(200) assert result["id"] == to_string(object.id) + + object = Object.normalize(other_message, false) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{object.id}") + |> json_response(400) + + assert result == %{"error" => "could_not_delete"} end end From a61120f497e5d17be1207eacd11525edb7b6db36 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 12 May 2020 13:25:25 +0200 Subject: [PATCH 102/375] Documention: Add chat message deletion docs --- docs/API/chats.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/API/chats.md b/docs/API/chats.md index ed160abd9..ad36961ae 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -192,6 +192,14 @@ Returned data: } ``` +### Deleting a chat message + +Deleting a chat message for given Chat id works like this: + +`DELETE /api/v1/pleroma/chats/{chat_id}/messages/{message_id}` + +Returned data is the deleted message. + ### Notifications There's a new `pleroma:chat_mention` notification, which has this form: From e44166b510f95bfb2e679b2d64bbf7e0facd0dd2 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 12 May 2020 14:44:11 +0200 Subject: [PATCH 103/375] Credo fixes. --- test/web/pleroma_api/controllers/chat_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 75d4903ed..861ef10b0 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -4,8 +4,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Object alias Pleroma.Chat + alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI From c0ea5c60e4e709d3d4415de42a65f878b55dc3bb Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 12 May 2020 16:43:04 +0200 Subject: [PATCH 104/375] ChatController: Don't return chats for user you've blocked. --- .../controllers/chat_controller.ex | 5 +++- .../controllers/chat_controller_test.exs | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 4ce3e7419..496cb8e87 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -102,10 +102,13 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = para end end - def index(%{assigns: %{user: %{id: user_id}}} = conn, params) do + def index(%{assigns: %{user: %{id: user_id} = user}} = conn, params) do + blocked_ap_ids = User.blocked_users_ap_ids(user) + chats = from(c in Chat, where: c.user_id == ^user_id, + where: c.recipient not in ^blocked_ap_ids, order_by: [desc: c.updated_at] ) |> Pagination.fetch_paginated(params |> stringify_keys) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 861ef10b0..037dd2297 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do alias Pleroma.Chat alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI @@ -209,6 +210,28 @@ test "it returns a chat", %{conn: conn, user: user} do describe "GET /api/v1/pleroma/chats" do setup do: oauth_access(["read:statuses"]) + test "it does not return chats with users you blocked", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 1 + + User.block(user, recipient) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 0 + end + test "it paginates", %{conn: conn, user: user} do Enum.each(1..30, fn _ -> recipient = insert(:user) From 06cad239e50cada3aec4fc3b4c494a70d328672c Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 13 May 2020 14:05:22 +0200 Subject: [PATCH 105/375] InstanceView: Add pleroma chat messages to nodeinfo --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 3 ++- test/web/node_info_test.exs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index a329ffc28..17cfc4fcf 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -68,7 +68,8 @@ def features do if Config.get([:instance, :safe_dm_mentions]) do "safe_dm_mentions" end, - "pleroma_emoji_reactions" + "pleroma_emoji_reactions", + "pleroma_chat_messages" ] |> Enum.filter(& &1) end diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index 9bcc07b37..00925caad 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -145,7 +145,8 @@ test "it shows default features flags", %{conn: conn} do "shareable_emoji_packs", "multifetch", "pleroma_emoji_reactions", - "pleroma:api/v1/notifications:include_types_filter" + "pleroma:api/v1/notifications:include_types_filter", + "pleroma_chat_messages" ] assert MapSet.subset?( From 0f0acc740d30c47d093f27875d4decf0693b2845 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 13 May 2020 15:31:28 +0200 Subject: [PATCH 106/375] Chat: Allow posting without content if an attachment is present. --- docs/API/chats.md | 5 ++- .../chat_message_validator.ex | 14 +++++++- .../web/api_spec/operations/chat_operation.ex | 12 ++++--- .../web/api_spec/schemas/chat_message.ex | 2 +- lib/pleroma/web/common_api/common_api.ex | 17 +++++++--- .../controllers/chat_controller.ex | 7 ++-- .../activity_pub/object_validator_test.exs | 32 +++++++++++++++++++ test/web/common_api/common_api_test.exs | 23 +++++++++++++ .../controllers/chat_controller_test.exs | 18 +++++++++-- 9 files changed, 111 insertions(+), 19 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index ad36961ae..1ea18ff5f 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -166,11 +166,10 @@ Posting a chat message for given Chat id works like this: `POST /api/v1/pleroma/chats/{id}/messages` Parameters: -- content: The text content of the message +- content: The text content of the message. Optional if media is attached. - media_id: The id of an upload that will be attached to the message. -Currently, no formatting beyond basic escaping and emoji is implemented, as well as no -attachments. This will most probably change. +Currently, no formatting beyond basic escaping and emoji is implemented. Returned data: diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index e40c80ab4..9c20c188a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -61,12 +61,24 @@ def changeset(struct, data) do def validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["ChatMessage"]) - |> validate_required([:id, :actor, :to, :type, :content, :published]) + |> validate_required([:id, :actor, :to, :type, :published]) + |> validate_content_or_attachment() |> validate_length(:to, is: 1) |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit])) |> validate_local_concern() end + def validate_content_or_attachment(cng) do + attachment = get_field(cng, :attachment) + + if attachment do + cng + else + cng + |> validate_required([:content]) + end + end + @doc """ Validates the following - If both users are in our system diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 8ba10c603..a1c5db5dc 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.Chat alias Pleroma.Web.ApiSpec.Schemas.ChatMessage @@ -149,14 +150,15 @@ def post_chat_message_operation do parameters: [ Operation.parameter(:id, :path, :string, "The ID of the Chat") ], - requestBody: request_body("Parameters", chat_message_create(), required: true), + requestBody: request_body("Parameters", chat_message_create()), responses: %{ 200 => Operation.response( "The newly created ChatMessage", "application/json", ChatMessage - ) + ), + 400 => Operation.response("Bad Request", "application/json", ApiError) }, security: [ %{ @@ -292,10 +294,12 @@ def chat_message_create do description: "POST body for creating an chat message", type: :object, properties: %{ - content: %Schema{type: :string, description: "The content of your message"}, + content: %Schema{ + type: :string, + description: "The content of your message. Optional if media_id is present" + }, media_id: %Schema{type: :string, description: "The id of an upload"} }, - required: [:content], example: %{ "content" => "Hey wanna buy feet pics?", "media_id" => "134234" diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex index 6e8f1a10a..3ee85aa76 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do id: %Schema{type: :string}, account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"}, chat_id: %Schema{type: :string}, - content: %Schema{type: :string}, + content: %Schema{type: :string, nullable: true}, created_at: %Schema{type: :string, format: :"date-time"}, emojis: %Schema{type: :array}, attachment: %Schema{type: :object, nullable: true} diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 664175a4f..7008cea44 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -26,14 +26,14 @@ defmodule Pleroma.Web.CommonAPI do require Logger def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do - with :ok <- validate_chat_content_length(content), - maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_content_length(content, !!maybe_attachment), {_, {:ok, chat_message_data, _meta}} <- {:build_object, Builder.chat_message( user, recipient.ap_id, - content |> Formatter.html_escape("text/plain"), + content |> format_chat_content, attachment: maybe_attachment )}, {_, {:ok, create_activity_data, _meta}} <- @@ -47,7 +47,16 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) end end - defp validate_chat_content_length(content) do + defp format_chat_content(nil), do: nil + + defp format_chat_content(content) do + content |> Formatter.html_escape("text/plain") + end + + defp validate_chat_content_length(_, true), do: :ok + defp validate_chat_content_length(nil, false), do: {:error, :no_content} + + defp validate_chat_content_length(content, _) do if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do :ok else diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 496cb8e87..210c8ec4a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -58,8 +58,7 @@ def delete_message(%{assigns: %{user: %{ap_id: actor} = user}} = conn, %{ end def post_chat_message( - %{body_params: %{content: content} = params, assigns: %{user: %{id: user_id} = user}} = - conn, + %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn, %{ id: id } @@ -67,7 +66,9 @@ def post_chat_message( with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), {:ok, activity} <- - CommonAPI.post_chat_message(user, recipient, content, media_id: params[:media_id]), + CommonAPI.post_chat_message(user, recipient, params[:content], + media_id: params[:media_id] + ), message <- Object.normalize(activity) do conn |> put_view(ChatMessageView) diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index d9f5a8fac..da33d3dbc 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -103,6 +103,38 @@ test "validates for a basic object with an attachment", %{ assert object["attachment"] end + test "validates for a basic object with an attachment but without content", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + |> Map.delete("content") + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "does not validate if the message has no content", %{ + valid_chat_message: valid_chat_message + } do + contentless = + valid_chat_message + |> Map.delete("content") + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) + end + test "does not validate if the message is longer than the remote_limit", %{ valid_chat_message: valid_chat_message } do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index fd2c486a1..46ffd2888 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -27,6 +27,29 @@ defmodule Pleroma.Web.CommonAPITest do describe "posting chat messages" do setup do: clear_config([:instance, :chat_limit]) + test "it posts a chat message without content but with an attachment" do + author = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + nil, + media_id: upload.id + ) + + assert activity + end + test "it posts a chat message" do author = insert(:user) recipient = insert(:user) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 037dd2297..d79aa3148 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -53,6 +53,20 @@ test "it posts a message to the chat", %{conn: conn, user: user} do assert result["chat_id"] == chat.id |> to_string() end + test "it fails if there is no content", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(400) + + assert result + end + test "it works with an attachment", %{conn: conn, user: user} do file = %Plug.Upload{ content_type: "image/jpg", @@ -70,13 +84,11 @@ test "it works with an attachment", %{conn: conn, user: user} do conn |> put_req_header("content-type", "application/json") |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ - "content" => "Hallo!!", "media_id" => to_string(upload.id) }) |> json_response_and_validate_schema(200) - assert result["content"] == "Hallo!!" - assert result["chat_id"] == chat.id |> to_string() + assert result["attachment"] end end From 3342846ac2bbd48e985cfeff26ba4593f4815879 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 14 May 2020 13:20:28 +0200 Subject: [PATCH 107/375] ChatView: Add update_at field. --- lib/pleroma/web/pleroma_api/views/chat_view.ex | 4 +++- test/web/pleroma_api/views/chat_view_test.exs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 21f0612ff..08d5110c3 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do alias Pleroma.Chat alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.PleromaAPI.ChatMessageView @@ -20,7 +21,8 @@ def render("show.json", %{chat: %Chat{} = chat} = opts) do account: AccountView.render("show.json", Map.put(opts, :user, recipient)), unread: chat.unread, last_message: - last_message && ChatMessageView.render("show.json", chat: chat, object: last_message) + last_message && ChatMessageView.render("show.json", chat: chat, object: last_message), + updated_at: Utils.to_masto_date(chat.updated_at) } end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index e24e29835..6062a0cfe 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do alias Pleroma.Chat alias Pleroma.Object alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.PleromaAPI.ChatMessageView alias Pleroma.Web.PleromaAPI.ChatView @@ -26,7 +27,8 @@ test "it represents a chat" do id: "#{chat.id}", account: AccountView.render("show.json", user: recipient), unread: 0, - last_message: nil + last_message: nil, + updated_at: Utils.to_masto_date(chat.updated_at) } {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") From 1d18721a3c60aa0acc7d1ba858a92277e544a54a Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 15 May 2020 13:18:41 +0200 Subject: [PATCH 108/375] Chats: Add updated_at to Schema and docs. --- docs/API/chats.md | 9 ++++++--- lib/pleroma/web/api_spec/schemas/chat.ex | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 1ea18ff5f..2e415e4da 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -68,7 +68,8 @@ Returned data: }, "id" : "1", "unread" : 2, - "last_message" : {...} // The last message in that chat + "last_message" : {...}, // The last message in that chat + "updated_at": "2020-04-21T15:11:46.000Z" } ``` @@ -88,7 +89,8 @@ Returned data: ... }, "id" : "1", - "unread" : 0 + "unread" : 0, + "updated_at": "2020-04-21T15:11:46.000Z" } ``` @@ -112,7 +114,8 @@ Returned data: }, "id" : "1", "unread" : 2, - "last_message" : {...} // The last message in that chat + "last_message" : {...}, // The last message in that chat + "updated_at": "2020-04-21T15:11:46.000Z" } ] ``` diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex index c6ec07c88..b4986b734 100644 --- a/lib/pleroma/web/api_spec/schemas/chat.ex +++ b/lib/pleroma/web/api_spec/schemas/chat.ex @@ -16,7 +16,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do id: %Schema{type: :string}, account: %Schema{type: :object}, unread: %Schema{type: :integer}, - last_message: ChatMessage + last_message: ChatMessage, + updated_at: %Schema{type: :string, format: :"date-time"} }, example: %{ "account" => %{ @@ -67,7 +68,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do }, "id" => "1", "unread" => 2, - "last_message" => ChatMessage.schema().example() + "last_message" => ChatMessage.schema().example(), + "updated_at" => "2020-04-21T15:06:45.000Z" } }) end From baf051a59e8bfcb2e55b5e28e46e80d6961b9bb4 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 17 May 2020 12:22:26 +0200 Subject: [PATCH 109/375] SideEffects: Don't update unread count for actor in chatmessages. --- lib/pleroma/web/activity_pub/side_effects.ex | 6 +++++- test/web/activity_pub/side_effects_test.exs | 21 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index c8b675d54..8e64b4615 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -117,7 +117,11 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do [[actor, recipient], [recipient, actor]] |> Enum.each(fn [user, other_user] -> if user.local do - Chat.bump_or_create(user.id, other_user.ap_id) + if user.ap_id == actor.ap_id do + Chat.get_or_create(user.id, other_user.ap_id) + else + Chat.bump_or_create(user.id, other_user.ap_id) + end end end) diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 148fa4442..37d7491ca 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -284,6 +284,27 @@ test "notifies the recipient" do assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) end + test "it creates a Chat for the local users and bumps the unread count, except for the author" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + chat = Chat.get(author.id, recipient.ap_id) + assert chat.unread == 0 + + chat = Chat.get(recipient.id, author.ap_id) + assert chat.unread == 1 + end + test "it creates a Chat for the local users and bumps the unread count" do author = insert(:user, local: false) recipient = insert(:user, local: true) From e7bc2f980cce170731960e024614c497b821fe90 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 7 May 2020 13:44:38 +0300 Subject: [PATCH 110/375] account visibility --- lib/pleroma/user.ex | 52 ++++++++++------ .../api_spec/operations/account_operation.ex | 8 ++- .../controllers/account_controller.ex | 21 +++++-- .../web/mastodon_api/views/account_view.ex | 2 +- test/user_test.exs | 10 ++-- .../controllers/account_controller_test.exs | 59 ++++++++++++++----- 6 files changed, 105 insertions(+), 47 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index cba391072..7a2558c29 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -262,37 +262,51 @@ def account_status(%User{deactivated: true}), do: :deactivated def account_status(%User{password_reset_pending: true}), do: :password_reset_pending def account_status(%User{confirmation_pending: true}) do - case Config.get([:instance, :account_activation_required]) do - true -> :confirmation_pending - _ -> :active + if Config.get([:instance, :account_activation_required]) do + :confirmation_pending + else + :active end end def account_status(%User{}), do: :active - @spec visible_for?(User.t(), User.t() | nil) :: boolean() - def visible_for?(user, for_user \\ nil) + @spec visible_for(User.t(), User.t() | nil) :: + boolean() + | :invisible + | :restricted_unauthenticated + | :deactivated + | :confirmation_pending + def visible_for(user, for_user \\ nil) - def visible_for?(%User{invisible: true}, _), do: false + def visible_for(%User{invisible: true}, _), do: :invisible - def visible_for?(%User{id: user_id}, %User{id: user_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 + def visible_for(%User{} = user, nil) do + if restrict_unauthenticated?(user) do + :restrict_unauthenticated + else + visible_account_status(user) + end end - def visible_for?(%User{} = user, for_user) do - account_status(user) == :active || superuser?(for_user) + def visible_for(%User{} = user, for_user) do + superuser?(for_user) || visible_account_status(user) end - def visible_for?(_, _), do: false + def visible_for(_, _), do: false + + defp restrict_unauthenticated?(%User{local: local}) do + config_key = if local, do: :local, else: :remote + + Config.get([:restrict_unauthenticated, :profiles, config_key], false) + end + + defp visible_account_status(user) do + status = account_status(user) + status in [:active, :password_reset_pending] || status + end @spec superuser?(User.t()) :: boolean() def superuser?(%User{local: true, is_admin: true}), do: true diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 934f6038e..43168acf7 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -102,7 +102,9 @@ def show_operation do parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], responses: %{ 200 => Operation.response("Account", "application/json", Account), - 404 => Operation.response("Error", "application/json", ApiError) + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError), + 410 => Operation.response("Error", "application/json", ApiError) } } end @@ -142,7 +144,9 @@ def statuses_operation do ] ++ pagination_params(), responses: %{ 200 => Operation.response("Statuses", "application/json", array_of_statuses()), - 404 => Operation.response("Error", "application/json", ApiError) + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError), + 410 => Operation.response("Error", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index ef41f9e96..ffa82731f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -221,17 +221,17 @@ def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) @doc "GET /api/v1/accounts/:id" def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user), - true <- User.visible_for?(user, for_user) do + true <- User.visible_for(user, for_user) do render(conn, "show.json", user: user, for: for_user) else - _e -> render_error(conn, :not_found, "Can't find user") + error -> user_visibility_error(conn, error) end end @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), - true <- User.visible_for?(user, reading_user) do + true <- User.visible_for(user, reading_user) do params = params |> Map.delete(:tagged) @@ -250,7 +250,20 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do as: :activity ) else - _e -> render_error(conn, :not_found, "Can't find user") + error -> user_visibility_error(conn, error) + end + end + + defp user_visibility_error(conn, error) do + case error do + :deactivated -> + render_error(conn, :gone, "") + + :restrict_unauthenticated -> + render_error(conn, :unauthorized, "This API requires an authenticated user") + + _ -> + render_error(conn, :not_found, "Can't find user") 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 45fffaad2..8e723d013 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -35,7 +35,7 @@ def render("index.json", %{users: users} = opts) do end def render("show.json", %{user: user} = opts) do - if User.visible_for?(user, opts[:for]) do + if User.visible_for(user, opts[:for]) == true do do_render("show.json", opts) else %{} diff --git a/test/user_test.exs b/test/user_test.exs index 6b9df60a4..3bfcfd10c 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1289,11 +1289,11 @@ test "returns false for a non-invisible user" do end end - describe "visible_for?/2" do + describe "visible_for/2" do test "returns true when the account is itself" do user = insert(:user, local: true) - assert User.visible_for?(user, user) + assert User.visible_for(user, user) end test "returns false when the account is unauthenticated and auth is required" do @@ -1302,14 +1302,14 @@ test "returns false when the account is unauthenticated and auth is required" do user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) - refute User.visible_for?(user, other_user) + refute User.visible_for(user, other_user) == true end test "returns true when the account is unauthenticated and auth is not required" do user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) - assert User.visible_for?(user, other_user) + assert User.visible_for(user, other_user) end test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do @@ -1318,7 +1318,7 @@ test "returns true when the account is unauthenticated and being viewed by a pri user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true, is_admin: true) - assert User.visible_for?(user, other_user) + assert User.visible_for(user, other_user) end end diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 280bd6aca..7dfea2f9e 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -127,6 +127,15 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do |> get("/api/v1/accounts/internal.fetch") |> json_response_and_validate_schema(404) end + + test "returns 401 for deactivated user", %{conn: conn} do + user = insert(:user, deactivated: true) + + assert %{} = + conn + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(:gone) + end end defp local_and_remote_users do @@ -143,15 +152,15 @@ defp local_and_remote_users do setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "Can't find user"} == + assert %{"error" => "This API requires an authenticated user"} == conn |> get("/api/v1/accounts/#{local.id}") - |> json_response_and_validate_schema(:not_found) + |> json_response_and_validate_schema(:unauthorized) - assert %{"error" => "Can't find user"} == + assert %{"error" => "This API requires an authenticated user"} == conn |> get("/api/v1/accounts/#{remote.id}") - |> json_response_and_validate_schema(:not_found) + |> json_response_and_validate_schema(:unauthorized) end test "if user is authenticated", %{local: local, remote: remote} do @@ -173,8 +182,8 @@ test "if user is authenticated", %{local: local, remote: remote} do test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Can't find user" + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "This API requires an authenticated user" } res_conn = get(conn, "/api/v1/accounts/#{remote.id}") @@ -203,8 +212,8 @@ test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} d res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Can't find user" + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "This API requires an authenticated user" } end @@ -249,6 +258,24 @@ test "works with announces that are just addressed to public", %{conn: conn} do assert id == announce.id end + test "deactivated user", %{conn: conn} do + user = insert(:user, deactivated: true) + + assert %{} == + conn + |> get("/api/v1/accounts/#{user.id}/statuses") + |> json_response_and_validate_schema(:gone) + end + + test "returns 404 when user is invisible", %{conn: conn} do + user = insert(:user, %{invisible: true}) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(404) + end + test "respects blocks", %{user: user_one, conn: conn} do user_two = insert(:user) user_three = insert(:user) @@ -422,15 +449,15 @@ defp local_and_remote_activities(%{local: local, remote: remote}) do setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "Can't find user"} == + assert %{"error" => "This API requires an authenticated user"} == conn |> get("/api/v1/accounts/#{local.id}/statuses") - |> json_response_and_validate_schema(:not_found) + |> json_response_and_validate_schema(:unauthorized) - assert %{"error" => "Can't find user"} == + assert %{"error" => "This API requires an authenticated user"} == conn |> get("/api/v1/accounts/#{remote.id}/statuses") - |> json_response_and_validate_schema(:not_found) + |> json_response_and_validate_schema(:unauthorized) end test "if user is authenticated", %{local: local, remote: remote} do @@ -451,10 +478,10 @@ test "if user is authenticated", %{local: local, remote: remote} do setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "Can't find user"} == + assert %{"error" => "This API requires an authenticated user"} == conn |> get("/api/v1/accounts/#{local.id}/statuses") - |> json_response_and_validate_schema(:not_found) + |> json_response_and_validate_schema(:unauthorized) res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") assert length(json_response_and_validate_schema(res_conn, 200)) == 1 @@ -481,10 +508,10 @@ test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} d res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - assert %{"error" => "Can't find user"} == + assert %{"error" => "This API requires an authenticated user"} == conn |> get("/api/v1/accounts/#{remote.id}/statuses") - |> json_response_and_validate_schema(:not_found) + |> json_response_and_validate_schema(:unauthorized) end test "if user is authenticated", %{local: local, remote: remote} do From b1aa402229b6422a5ab1aa7102c7a104e218d0e3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 13 May 2020 11:11:10 +0300 Subject: [PATCH 111/375] removing 410 status --- lib/pleroma/web/api_spec/operations/account_operation.ex | 6 ++---- .../web/mastodon_api/controllers/account_controller.ex | 3 --- .../mastodon_api/controllers/account_controller_test.exs | 8 ++++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 43168acf7..74b395dfe 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -103,8 +103,7 @@ def show_operation do responses: %{ 200 => Operation.response("Account", "application/json", Account), 401 => Operation.response("Error", "application/json", ApiError), - 404 => Operation.response("Error", "application/json", ApiError), - 410 => Operation.response("Error", "application/json", ApiError) + 404 => Operation.response("Error", "application/json", ApiError) } } end @@ -145,8 +144,7 @@ def statuses_operation do responses: %{ 200 => Operation.response("Statuses", "application/json", array_of_statuses()), 401 => Operation.response("Error", "application/json", ApiError), - 404 => Operation.response("Error", "application/json", ApiError), - 410 => Operation.response("Error", "application/json", ApiError) + 404 => Operation.response("Error", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index ffa82731f..1edc0d96a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -256,9 +256,6 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do defp user_visibility_error(conn, error) do case error do - :deactivated -> - render_error(conn, :gone, "") - :restrict_unauthenticated -> render_error(conn, :unauthorized, "This API requires an authenticated user") diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 7dfea2f9e..8700ab2f5 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -131,10 +131,10 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do test "returns 401 for deactivated user", %{conn: conn} do user = insert(:user, deactivated: true) - assert %{} = + assert %{"error" => "Can't find user"} = conn |> get("/api/v1/accounts/#{user.id}") - |> json_response_and_validate_schema(:gone) + |> json_response_and_validate_schema(:not_found) end end @@ -261,10 +261,10 @@ test "works with announces that are just addressed to public", %{conn: conn} do test "deactivated user", %{conn: conn} do user = insert(:user, deactivated: true) - assert %{} == + assert %{"error" => "Can't find user"} == conn |> get("/api/v1/accounts/#{user.id}/statuses") - |> json_response_and_validate_schema(:gone) + |> json_response_and_validate_schema(:not_found) end test "returns 404 when user is invisible", %{conn: conn} do From 1671864d886bf63d11bbf3d7303719e8744bfc32 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 15 May 2020 20:29:09 +0300 Subject: [PATCH 112/375] return :visible instead of boolean --- lib/pleroma/user.ex | 19 ++++++++++++++----- .../controllers/account_controller.ex | 4 ++-- .../web/mastodon_api/views/account_view.ex | 2 +- test/user_test.exs | 8 ++++---- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7a2558c29..5052f7b97 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -272,7 +272,7 @@ def account_status(%User{confirmation_pending: true}) do def account_status(%User{}), do: :active @spec visible_for(User.t(), User.t() | nil) :: - boolean() + :visible | :invisible | :restricted_unauthenticated | :deactivated @@ -281,7 +281,7 @@ def visible_for(user, for_user \\ nil) def visible_for(%User{invisible: true}, _), do: :invisible - def visible_for(%User{id: user_id}, %User{id: user_id}), do: true + def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible def visible_for(%User{} = user, nil) do if restrict_unauthenticated?(user) do @@ -292,10 +292,14 @@ def visible_for(%User{} = user, nil) do end def visible_for(%User{} = user, for_user) do - superuser?(for_user) || visible_account_status(user) + if superuser?(for_user) do + :visible + else + visible_account_status(user) + end end - def visible_for(_, _), do: false + def visible_for(_, _), do: :invisible defp restrict_unauthenticated?(%User{local: local}) do config_key = if local, do: :local, else: :remote @@ -305,7 +309,12 @@ defp restrict_unauthenticated?(%User{local: local}) do defp visible_account_status(user) do status = account_status(user) - status in [:active, :password_reset_pending] || status + + if status in [:active, :password_reset_pending] do + :visible + else + status + end end @spec superuser?(User.t()) :: boolean() diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 1edc0d96a..8727faab7 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -221,7 +221,7 @@ def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) @doc "GET /api/v1/accounts/:id" def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user), - true <- User.visible_for(user, for_user) do + :visible <- User.visible_for(user, for_user) do render(conn, "show.json", user: user, for: for_user) else error -> user_visibility_error(conn, error) @@ -231,7 +231,7 @@ 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), - true <- User.visible_for(user, reading_user) do + :visible <- User.visible_for(user, reading_user) do params = params |> Map.delete(:tagged) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 8e723d013..4a1508b22 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -35,7 +35,7 @@ def render("index.json", %{users: users} = opts) do end def render("show.json", %{user: user} = opts) do - if User.visible_for(user, opts[:for]) == true do + if User.visible_for(user, opts[:for]) == :visible do do_render("show.json", opts) else %{} diff --git a/test/user_test.exs b/test/user_test.exs index 3bfcfd10c..6865bd9be 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1293,7 +1293,7 @@ test "returns false for a non-invisible user" do test "returns true when the account is itself" do user = insert(:user, local: true) - assert User.visible_for(user, user) + assert User.visible_for(user, user) == :visible end test "returns false when the account is unauthenticated and auth is required" do @@ -1302,14 +1302,14 @@ test "returns false when the account is unauthenticated and auth is required" do user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) - refute User.visible_for(user, other_user) == true + refute User.visible_for(user, other_user) == :visible end test "returns true when the account is unauthenticated and auth is not required" do user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) - assert User.visible_for(user, other_user) + assert User.visible_for(user, other_user) == :visible end test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do @@ -1318,7 +1318,7 @@ test "returns true when the account is unauthenticated and being viewed by a pri user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true, is_admin: true) - assert User.visible_for(user, other_user) + assert User.visible_for(user, other_user) == :visible end end From 0321a3e07814c3f225f19e0372b69a7813cef15e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 18 May 2020 10:34:34 +0300 Subject: [PATCH 113/375] test naming fix --- test/web/mastodon_api/controllers/account_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 8700ab2f5..3008970af 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -128,7 +128,7 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do |> json_response_and_validate_schema(404) end - test "returns 401 for deactivated user", %{conn: conn} do + test "returns 404 for deactivated user", %{conn: conn} do user = insert(:user, deactivated: true) assert %{"error" => "Can't find user"} = From 1be6b3056e97654612f377eaf3c8d80de6d8d77f Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Mon, 18 May 2020 12:38:16 +0300 Subject: [PATCH 114/375] Use indexed split_part/3 to get a hostname rather than ts_ functions --- .../20200508092434_update_counter_cache_table.exs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/priv/repo/migrations/20200508092434_update_counter_cache_table.exs b/priv/repo/migrations/20200508092434_update_counter_cache_table.exs index 3d9bfc877..738344868 100644 --- a/priv/repo/migrations/20200508092434_update_counter_cache_table.exs +++ b/priv/repo/migrations/20200508092434_update_counter_cache_table.exs @@ -25,22 +25,17 @@ def up do RETURNS TRIGGER AS $$ DECLARE - token_id smallint; hostname character varying(255); visibility_new character varying(64); visibility_old character varying(64); actor character varying(255); BEGIN - SELECT "tokid" INTO "token_id" FROM ts_token_type('default') WHERE "alias" = 'host'; IF TG_OP = 'DELETE' THEN actor := OLD.actor; ELSE actor := NEW.actor; END IF; - SELECT "token" INTO "hostname" FROM ts_parse('default', actor) WHERE "tokid" = token_id; - IF hostname IS NULL THEN - hostname := split_part(actor, '/', 3); - END IF; + hostname := split_part(actor, '/', 3); IF TG_OP = 'INSERT' THEN visibility_new := activity_visibility(NEW.actor, NEW.recipients, NEW.data); IF NEW.data->>'type' = 'Create' From be4db41d713f981cc464e5fa7bc7191d3ff776d6 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 18 May 2020 18:45:33 +0200 Subject: [PATCH 115/375] ChatMessageValidator: Allow one message in an array, too. --- .../chat_message_validator.ex | 9 +++++ .../activity_pub/object_validator_test.exs | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 9c20c188a..138736f23 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -47,9 +47,18 @@ def cast_data(data) do def fix(data) do data |> fix_emoji() + |> fix_attachment() |> Map.put_new("actor", data["attributedTo"]) end + # Throws everything but the first one away + def fix_attachment(%{"attachment" => [attachment | _]} = data) do + data + |> Map.put("attachment", attachment) + end + + def fix_attachment(data), do: data + def changeset(struct, data) do data = fix(data) diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index da33d3dbc..a79e50a29 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -13,6 +13,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do import Pleroma.Factory describe "attachments" do + test "works with honkerific attachments" do + attachment = %{ + "mediaType" => "image/jpeg", + "name" => "298p3RG7j27tfsZ9RQ.jpg", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + end + test "it turns mastodon attachments into our attachments" do attachment = %{ "url" => @@ -103,6 +117,27 @@ test "validates for a basic object with an attachment", %{ assert object["attachment"] end + test "validates for a basic object with an attachment in an array", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", [attachment.data]) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + test "validates for a basic object with an attachment but without content", %{ valid_chat_message: valid_chat_message, user: user From d19c7167704308df093f060082639c0a15996af7 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 18 May 2020 20:17:28 +0200 Subject: [PATCH 116/375] AttachmentValidator: Handle empty mediatypes --- .../object_validators/attachment_validator.ex | 14 +++++-- .../activity_pub/object_validator_test.exs | 4 +- .../transmogrifier/chat_message_test.exs | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index 16ed49051..c4b502cb9 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do @primary_key false embedded_schema do field(:type, :string) - field(:mediaType, :string) + field(:mediaType, :string, default: "application/octet-stream") field(:name, :string) embeds_many(:url, UrlObjectValidator) @@ -41,8 +41,16 @@ def changeset(struct, data) do end def fix_media_type(data) do - data - |> Map.put_new("mediaType", data["mimeType"]) + data = + data + |> Map.put_new("mediaType", data["mimeType"]) + + if data["mediaType"] == "" do + data + |> Map.put("mediaType", "application/octet-stream") + else + data + end end def fix_url(data) do diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index a79e50a29..ed6b84e8e 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -15,8 +15,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do describe "attachments" do test "works with honkerific attachments" do attachment = %{ - "mediaType" => "image/jpeg", - "name" => "298p3RG7j27tfsZ9RQ.jpg", + "mediaType" => "", + "name" => "", "summary" => "298p3RG7j27tfsZ9RQ.jpg", "type" => "Document", "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index 85644d787..820090de3 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -13,6 +13,43 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do alias Pleroma.Web.ActivityPub.Transmogrifier describe "handle_incoming" do + test "handles this" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honk.tedunangst.com/u/tedu", + "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T", + "object" => %{ + "attachment" => [ + %{ + "mediaType" => "image/jpeg", + "name" => "298p3RG7j27tfsZ9RQ.jpg", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + ], + "attributedTo" => "https://honk.tedunangst.com/u/tedu", + "content" => "", + "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b", + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "ChatMessage" + }, + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "Create" + } + + _user = insert(:user, ap_id: data["actor"]) + _user = insert(:user, ap_id: hd(data["to"])) + + assert {:ok, _activity} = Transmogrifier.handle_incoming(data) + end + test "it rejects messages that don't contain content" do data = File.read!("test/fixtures/create-chat-message.json") From cc0d462e91dd29c834c56b82e02022e1babda369 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 21 May 2020 15:08:56 +0200 Subject: [PATCH 117/375] Attachments: Have the mediaType on the root, too. --- lib/pleroma/upload.ex | 1 + .../activity_pub/object_validator_test.exs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 1be1a3a5b..797555bff 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -67,6 +67,7 @@ def store(upload, opts \\ []) do {:ok, %{ "type" => opts.activity_type, + "mediaType" => upload.content_type, "url" => [ %{ "type" => "Link", diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index ed6b84e8e..f9990bd2c 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -25,6 +25,8 @@ test "works with honkerific attachments" do assert {:ok, attachment} = AttachmentValidator.cast_and_validate(attachment) |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "application/octet-stream" end test "it turns mastodon attachments into our attachments" do @@ -48,6 +50,27 @@ test "it turns mastodon attachments into our attachments" do mediaType: "image/jpeg" } ] = attachment.url + + assert attachment.mediaType == "image/jpeg" + end + + test "it handles our own uploads" do + user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, attachment} = + attachment.data + |> AttachmentValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/jpeg" end end From c4a5cead51770f0d54cb77805b7e2bd705f251d9 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 21 May 2020 15:17:39 +0200 Subject: [PATCH 118/375] UploadTest: Fix test. --- test/upload_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/upload_test.exs b/test/upload_test.exs index 060a940bb..2abf0edec 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -54,6 +54,7 @@ test "it returns file" do %{ "name" => "image.jpg", "type" => "Document", + "mediaType" => "image/jpeg", "url" => [ %{ "href" => "http://localhost:4001/media/post-process-file.jpg", From cbcd592300673582e38d0bf539dcdb9a2c1985a1 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 22 May 2020 17:52:26 +0400 Subject: [PATCH 119/375] Add OpenAPI spec for AdminAPI.RelayController --- .../controllers/admin_api_controller.ex | 48 +--------- .../admin_api/controllers/relay_controller.ex | 67 ++++++++++++++ .../operations/admin/relay_operation.ex | 83 +++++++++++++++++ lib/pleroma/web/router.ex | 6 +- .../controllers/admin_api_controller_test.exs | 51 ---------- .../controllers/relay_controller_test.exs | 92 +++++++++++++++++++ 6 files changed, 246 insertions(+), 101 deletions(-) create mode 100644 lib/pleroma/web/admin_api/controllers/relay_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/relay_operation.ex create mode 100644 test/web/admin_api/controllers/relay_controller_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 6b1d64a2e..b73701f5e 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -20,7 +20,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline - alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView @@ -80,7 +79,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["write:follows"], admin: true} - when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] + when action in [:user_follow, :user_unfollow] ) plug( @@ -108,7 +107,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do :config_show, :list_log, :stats, - :relay_list, :config_descriptions, :need_reboot ] @@ -531,50 +529,6 @@ def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" render_error(conn, :forbidden, "You can't revoke your own admin status.") end - def relay_list(conn, _params) do - with {:ok, list} <- Relay.list() do - json(conn, %{relays: list}) - else - _ -> - conn - |> put_status(500) - end - end - - def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do - with {:ok, _message} <- Relay.follow(target) do - ModerationLog.insert_log(%{ - action: "relay_follow", - actor: admin, - target: target - }) - - json(conn, target) - else - _ -> - conn - |> put_status(500) - |> json(target) - end - end - - def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do - with {:ok, _message} <- Relay.unfollow(target) do - ModerationLog.insert_log(%{ - action: "relay_unfollow", - actor: admin, - target: target - }) - - json(conn, target) - else - _ -> - conn - |> put_status(500) - |> json(target) - end - end - @doc "Sends registration invite via email" def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex new file mode 100644 index 000000000..cf9f3a14b --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayController do + use Pleroma.Web, :controller + + alias Pleroma.ModerationLog + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ActivityPub.Relay + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["write:follows"], admin: true} + when action in [:follow, :unfollow] + ) + + plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :index) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RelayOperation + + def index(conn, _params) do + with {:ok, list} <- Relay.list() do + json(conn, %{relays: list}) + end + end + + def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do + with {:ok, _message} <- Relay.follow(target) do + ModerationLog.insert_log(%{ + action: "relay_follow", + actor: admin, + target: target + }) + + json(conn, target) + else + _ -> + conn + |> put_status(500) + |> json(target) + end + end + + def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do + with {:ok, _message} <- Relay.unfollow(target) do + ModerationLog.insert_log(%{ + action: "relay_unfollow", + actor: admin, + target: target + }) + + json(conn, target) + else + _ -> + conn + |> put_status(500) + |> json(target) + end + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex new file mode 100644 index 000000000..7672cb467 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "List Relays", + operationId: "AdminAPI.RelayController.index", + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{ + relays: %Schema{ + type: :array, + items: %Schema{type: :string}, + example: ["lain.com", "mstdn.io"] + } + } + }) + } + } + end + + def follow_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "Follow a Relay", + operationId: "AdminAPI.RelayController.follow", + security: [%{"oAuth" => ["write:follows"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri} + } + }), + responses: %{ + 200 => + Operation.response("Status", "application/json", %Schema{ + type: :string, + example: "http://mastodon.example.org/users/admin" + }) + } + } + end + + def unfollow_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "Unfollow a Relay", + operationId: "AdminAPI.RelayController.unfollow", + security: [%{"oAuth" => ["write:follows"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri} + } + }), + responses: %{ + 200 => + Operation.response("Status", "application/json", %Schema{ + type: :string, + example: "http://mastodon.example.org/users/admin" + }) + } + } + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e493a4153..269bbabde 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -160,9 +160,9 @@ defmodule Pleroma.Web.Router do :right_delete_multiple ) - get("/relay", AdminAPIController, :relay_list) - post("/relay", AdminAPIController, :relay_follow) - delete("/relay", AdminAPIController, :relay_unfollow) + get("/relay", RelayController, :index) + post("/relay", RelayController, :follow) + delete("/relay", RelayController, :unfollow) post("/users/invite_token", AdminAPIController, :create_invite_token) get("/users/invites", AdminAPIController, :invites) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index 321840a8c..82825473c 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -3254,57 +3254,6 @@ test "sets password_reset_pending to true", %{conn: conn} do end end - describe "relays" do - test "POST /relay", %{conn: conn, admin: admin} do - conn = - post(conn, "/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response(conn, 200) == "http://mastodon.example.org/users/admin" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - end - - test "GET /relay", %{conn: conn} do - relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() - - ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] - |> Enum.each(fn ap_id -> - {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) - User.follow(relay_user, user) - end) - - conn = get(conn, "/api/pleroma/admin/relay") - - assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == [] - end - - test "DELETE /relay", %{conn: conn, admin: admin} do - post(conn, "/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - conn = - delete(conn, "/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response(conn, 200) == "http://mastodon.example.org/users/admin" - - [log_entry_one, log_entry_two] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry_one) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - - assert ModerationLog.get_log_entry_message(log_entry_two) == - "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" - end - end - describe "instances" do test "GET /instances/:instance/statuses", %{conn: conn} do user = insert(:user, local: false, nickname: "archaeme@archae.me") diff --git a/test/web/admin_api/controllers/relay_controller_test.exs b/test/web/admin_api/controllers/relay_controller_test.exs new file mode 100644 index 000000000..64086adc5 --- /dev/null +++ b/test/web/admin_api/controllers/relay_controller_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "relays" do + test "POST /relay", %{conn: conn, admin: admin} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == + "http://mastodon.example.org/users/admin" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + end + + test "GET /relay", %{conn: conn} do + relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + conn = get(conn, "/api/pleroma/admin/relay") + + assert json_response_and_validate_schema(conn, 200)["relays"] -- + ["mastodon.example.org", "mstdn.io"] == [] + end + + test "DELETE /relay", %{conn: conn, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == + "http://mastodon.example.org/users/admin" + + [log_entry_one, log_entry_two] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry_one) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + + assert ModerationLog.get_log_entry_message(log_entry_two) == + "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" + end + end +end From dbd07d29a358a446d87078d60b993a59b757ad1d Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 25 May 2020 17:27:45 +0200 Subject: [PATCH 120/375] Streamer: Don't crash on streaming chat notifications --- lib/pleroma/web/common_api/common_api.ex | 9 +++++---- test/web/streamer/streamer_test.exs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index c08edbc5f..764fa4f4f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -467,12 +467,13 @@ def remove_mute(user, activity) do {:ok, activity} end - def thread_muted?(%{id: nil} = _user, _activity), do: false - - def thread_muted?(user, activity) do - ThreadMute.exists?(user.id, activity.data["context"]) + def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}}) + when is_binary("context") do + ThreadMute.exists?(user_id, context) end + def thread_muted?(_, _), do: false + def report(user, data) do with {:ok, account} <- get_reported_account(data.account_id), {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index cb4595bb6..115ba4703 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -126,6 +126,21 @@ test "it sends notify to in the 'user:notification' stream", %{user: user, notif refute Streamer.filtered_by_user?(user, notify) end + test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + notify = + Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) + |> Repo.preload(:activity) + + Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.stream("user:notification", notify) + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ user: user } do From 0ba1f2631a09cc0a40f8a0bc2f81ff2c83beedfb Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 25 May 2020 22:02:22 +0400 Subject: [PATCH 121/375] Add OpenAPI spec for AdminAPI.OAuthAppContoller --- .../controllers/admin_api_controller.ex | 83 ------- .../controllers/oauth_app_controller.ex | 87 +++++++ .../operations/admin/oauth_app_operation.ex | 215 +++++++++++++++++ lib/pleroma/web/oauth/app.ex | 29 +-- lib/pleroma/web/router.ex | 8 +- .../controllers/admin_api_controller_test.exs | 185 --------------- .../controllers/oauth_app_controller_test.exs | 220 ++++++++++++++++++ 7 files changed, 541 insertions(+), 286 deletions(-) create mode 100644 lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex create mode 100644 test/web/admin_api/controllers/oauth_app_controller_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 6b1d64a2e..4f10bd947 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -32,8 +32,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Web.MastodonAPI - alias Pleroma.Web.MastodonAPI.AppView - alias Pleroma.Web.OAuth.App alias Pleroma.Web.Router require Logger @@ -122,10 +120,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do :config_update, :resend_confirmation_email, :confirm_email, - :oauth_app_create, - :oauth_app_list, - :oauth_app_update, - :oauth_app_delete, :reload_emoji ] ) @@ -995,83 +989,6 @@ def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" = conn |> json("") end - def oauth_app_create(conn, params) do - params = - if params["name"] do - Map.put(params, "client_name", params["name"]) - else - params - end - - result = - case App.create(params) do - {:ok, app} -> - AppView.render("show.json", %{app: app, admin: true}) - - {:error, changeset} -> - App.errors(changeset) - end - - json(conn, result) - end - - def oauth_app_update(conn, params) do - params = - if params["name"] do - Map.put(params, "client_name", params["name"]) - else - params - end - - with {:ok, app} <- App.update(params) do - json(conn, AppView.render("show.json", %{app: app, admin: true})) - else - {:error, changeset} -> - json(conn, App.errors(changeset)) - - nil -> - json_response(conn, :bad_request, "") - end - end - - def oauth_app_list(conn, params) do - {page, page_size} = page_params(params) - - search_params = %{ - client_name: params["name"], - client_id: params["client_id"], - page: page, - page_size: page_size - } - - search_params = - if Map.has_key?(params, "trusted") do - Map.put(search_params, :trusted, params["trusted"]) - else - search_params - end - - with {:ok, apps, count} <- App.search(search_params) do - json( - conn, - AppView.render("index.json", - apps: apps, - count: count, - page_size: page_size, - admin: true - ) - ) - end - end - - def oauth_app_delete(conn, params) do - with {:ok, _app} <- App.destroy(params["id"]) do - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end - def stats(conn, _) do count = Stats.get_status_visibility_count() diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex new file mode 100644 index 000000000..04e629fc1 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex @@ -0,0 +1,87 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.OAuth.App + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.AppView) + + plug( + OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [:create, :index, :update, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation + + def index(conn, params) do + search_params = + params + |> Map.take([:client_id, :page, :page_size, :trusted]) + |> Map.put(:client_name, params[:name]) + + with {:ok, apps, count} <- App.search(search_params) do + render(conn, "index.json", + apps: apps, + count: count, + page_size: params.page_size, + admin: true + ) + end + end + + def create(%{body_params: params} = conn, _) do + params = + if params[:name] do + Map.put(params, :client_name, params[:name]) + else + params + end + + case App.create(params) do + {:ok, app} -> + render(conn, "show.json", app: app, admin: true) + + {:error, changeset} -> + json(conn, App.errors(changeset)) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + params = + if params[:name] do + Map.put(params, :client_name, params.name) + else + params + end + + with {:ok, app} <- App.update(id, params) do + render(conn, "show.json", app: app, admin: true) + else + {:error, changeset} -> + json(conn, App.errors(changeset)) + + nil -> + json_response(conn, :bad_request, "") + end + end + + def delete(conn, params) do + with {:ok, _app} <- App.destroy(params.id) do + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex new file mode 100644 index 000000000..fbc9f80d7 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex @@ -0,0 +1,215 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + summary: "List OAuth apps", + tags: ["Admin", "oAuth Apps"], + operationId: "AdminAPI.OAuthAppController.index", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), + Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), + Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), + Operation.parameter( + :trusted, + :query, + %Schema{type: :boolean, default: false}, + "Trusted apps" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of apps to return" + ) + ], + responses: %{ + 200 => + Operation.response("List of apps", "application/json", %Schema{ + type: :object, + properties: %{ + apps: %Schema{type: :array, items: oauth_app()}, + count: %Schema{type: :integer}, + page_size: %Schema{type: :integer} + }, + example: %{ + "apps" => [ + %{ + "id" => 1, + "name" => "App name", + "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", + "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", + "redirect_uri" => "https://example.com/oauth-callback", + "website" => "https://example.com", + "trusted" => true + } + ], + "count" => 1, + "page_size" => 50 + } + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Create OAuth App", + operationId: "AdminAPI.OAuthAppController.create", + requestBody: request_body("Parameters", create_request()), + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Update OAuth App", + operationId: "AdminAPI.OAuthAppController.update", + parameters: [id_param()], + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + oneOf: [ApiError, %Schema{type: :string}] + }) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Delete OAuth App", + operationId: "AdminAPI.OAuthAppController.delete", + parameters: [id_param()], + security: [%{"oAuth" => ["write"]}], + responses: %{ + 204 => no_content_response(), + 400 => no_content_response() + } + } + end + + defp create_request do + %Schema{ + title: "oAuthAppCreateRequest", + type: :object, + required: [:name, :redirect_uris], + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp update_request do + %Schema{ + title: "oAuthAppUpdateRequest", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp oauth_app do + %Schema{ + title: "oAuthApp", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + name: %Schema{type: :string}, + client_id: %Schema{type: :string}, + client_secret: %Schema{type: :string}, + redirect_uri: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true}, + trusted: %Schema{type: :boolean} + }, + example: %{ + "id" => 123, + "name" => "My App", + "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", + "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", + "redirect_uri" => "https://myapp.com/oauth-callback", + "website" => "https://myapp.com/", + "trusted" => false + } + } + end + + def id_param do + Operation.parameter(:id, :path, :integer, "App ID", + example: 1337, + required: true + ) + end +end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 6a6d5f2e2..df99472e1 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -25,12 +25,12 @@ defmodule Pleroma.Web.OAuth.App do timestamps() end - @spec changeset(App.t(), map()) :: Ecto.Changeset.t() + @spec changeset(t(), map()) :: Ecto.Changeset.t() def changeset(struct, params) do cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) end - @spec register_changeset(App.t(), map()) :: Ecto.Changeset.t() + @spec register_changeset(t(), map()) :: Ecto.Changeset.t() def register_changeset(struct, params \\ %{}) do changeset = struct @@ -52,18 +52,19 @@ def register_changeset(struct, params \\ %{}) do end end - @spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def create(params) do - with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do - Repo.insert(changeset) - end + %__MODULE__{} + |> register_changeset(params) + |> Repo.insert() end - @spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} - def update(params) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]), - changeset <- changeset(app, params) do - Repo.update(changeset) + @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def update(id, params) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + app + |> changeset(params) + |> Repo.update() end end @@ -71,7 +72,7 @@ def update(params) do Gets app by attrs or create new with attrs. And updates the scopes if need. """ - @spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def get_or_make(attrs, scopes) do with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do update_scopes(app, scopes) @@ -92,7 +93,7 @@ defp update_scopes(%__MODULE__{} = app, scopes) do |> Repo.update() end - @spec search(map()) :: {:ok, [App.t()], non_neg_integer()} + @spec search(map()) :: {:ok, [t()], non_neg_integer()} def search(params) do query = from(a in __MODULE__) @@ -128,7 +129,7 @@ def search(params) do {:ok, Repo.all(query), count} end - @spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def destroy(id) do with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do Repo.delete(app) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e493a4153..46f03cdfd 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -205,10 +205,10 @@ defmodule Pleroma.Web.Router do post("/reload_emoji", AdminAPIController, :reload_emoji) get("/stats", AdminAPIController, :stats) - get("/oauth_app", AdminAPIController, :oauth_app_list) - post("/oauth_app", AdminAPIController, :oauth_app_create) - patch("/oauth_app/:id", AdminAPIController, :oauth_app_update) - delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete) + get("/oauth_app", OAuthAppController, :index) + post("/oauth_app", OAuthAppController, :create) + patch("/oauth_app/:id", OAuthAppController, :update) + delete("/oauth_app/:id", OAuthAppController, :delete) end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index 321840a8c..f704cdd3a 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -3522,191 +3522,6 @@ test "status visibility count", %{conn: conn} do response["status_visibility"] end end - - describe "POST /api/pleroma/admin/oauth_app" do - test "errors", %{conn: conn} do - response = conn |> post("/api/pleroma/admin/oauth_app", %{}) |> json_response(200) - - assert response == %{"name" => "can't be blank", "redirect_uris" => "can't be blank"} - end - - test "success", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url - }) - |> json_response(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => false - } = response - end - - test "with trusted", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url, - trusted: true - }) - |> json_response(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => true - } = response - end - end - - describe "GET /api/pleroma/admin/oauth_app" do - setup do - app = insert(:oauth_app) - {:ok, app: app} - end - - test "list", %{conn: conn} do - response = - conn - |> get("/api/pleroma/admin/oauth_app") - |> json_response(200) - - assert %{"apps" => apps, "count" => count, "page_size" => _} = response - - assert length(apps) == count - end - - test "with page size", %{conn: conn} do - insert(:oauth_app) - page_size = 1 - - response = - conn - |> get("/api/pleroma/admin/oauth_app", %{page_size: to_string(page_size)}) - |> json_response(200) - - assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response - - assert length(apps) == page_size - end - - test "search by client name", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app", %{name: app.client_name}) - |> json_response(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "search by client id", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app", %{client_id: app.client_id}) - |> json_response(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "only trusted", %{conn: conn} do - app = insert(:oauth_app, trusted: true) - - response = - conn - |> get("/api/pleroma/admin/oauth_app", %{trusted: true}) - |> json_response(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - end - - describe "DELETE /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - response = - conn - |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) - |> json_response(:no_content) - - assert response == "" - end - - test "with non existance id", %{conn: conn} do - response = - conn - |> delete("/api/pleroma/admin/oauth_app/0") - |> json_response(:bad_request) - - assert response == "" - end - end - - describe "PATCH /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - name = "another name" - url = "https://example.com" - scopes = ["admin"] - id = app.id - website = "http://website.com" - - response = - conn - |> patch("/api/pleroma/admin/oauth_app/" <> to_string(app.id), %{ - name: name, - trusted: true, - redirect_uris: url, - scopes: scopes, - website: website - }) - |> json_response(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "id" => ^id, - "name" => ^name, - "redirect_uri" => ^url, - "trusted" => true, - "website" => ^website - } = response - end - - test "without id", %{conn: conn} do - response = - conn - |> patch("/api/pleroma/admin/oauth_app/0") - |> json_response(:bad_request) - - assert response == "" - end - end end # Needed for testing diff --git a/test/web/admin_api/controllers/oauth_app_controller_test.exs b/test/web/admin_api/controllers/oauth_app_controller_test.exs new file mode 100644 index 000000000..ed7c4172c --- /dev/null +++ b/test/web/admin_api/controllers/oauth_app_controller_test.exs @@ -0,0 +1,220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do + use Pleroma.Web.ConnCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Web + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "POST /api/pleroma/admin/oauth_app" do + test "errors", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{}) + |> json_response_and_validate_schema(400) + + assert %{ + "error" => "Missing field: name. Missing field: redirect_uris." + } = response + end + + test "success", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => false + } = response + end + + test "with trusted", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url, + trusted: true + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => true + } = response + end + end + + describe "GET /api/pleroma/admin/oauth_app" do + setup do + app = insert(:oauth_app) + {:ok, app: app} + end + + test "list", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/oauth_app") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => count, "page_size" => _} = response + + assert length(apps) == count + end + + test "with page size", %{conn: conn} do + insert(:oauth_app) + page_size = 1 + + response = + conn + |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response + + assert length(apps) == page_size + end + + test "search by client name", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "search by client id", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "only trusted", %{conn: conn} do + app = insert(:oauth_app, trusted: true) + + response = + conn + |> get("/api/pleroma/admin/oauth_app?trusted=true") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + end + + describe "DELETE /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + response = + conn + |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) + |> json_response_and_validate_schema(:no_content) + + assert response == "" + end + + test "with non existance id", %{conn: conn} do + response = + conn + |> delete("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end + + describe "PATCH /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + name = "another name" + url = "https://example.com" + scopes = ["admin"] + id = app.id + website = "http://website.com" + + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ + name: name, + trusted: true, + redirect_uris: url, + scopes: scopes, + website: website + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "id" => ^id, + "name" => ^name, + "redirect_uri" => ^url, + "trusted" => true, + "website" => ^website + } = response + end + + test "without id", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end +end From 8f08384d8058f61753c28d37c90b47a2886f348c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 18 May 2020 10:09:21 +0300 Subject: [PATCH 122/375] another view for account in admin-fe status_show --- .../web/admin_api/controllers/status_controller.ex | 2 +- test/web/admin_api/controllers/status_controller_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index 08cb9c10b..c91fbc771 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -42,7 +42,7 @@ def index(%{assigns: %{user: _admin}} = conn, params) do def show(conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id) do conn - |> put_view(MastodonAPI.StatusView) + |> put_view(Pleroma.Web.AdminAPI.StatusView) |> render("show.json", %{activity: activity}) else nil -> {:error, :not_found} diff --git a/test/web/admin_api/controllers/status_controller_test.exs b/test/web/admin_api/controllers/status_controller_test.exs index 124d8dc2e..eff78fb0a 100644 --- a/test/web/admin_api/controllers/status_controller_test.exs +++ b/test/web/admin_api/controllers/status_controller_test.exs @@ -42,6 +42,14 @@ test "shows activity", %{conn: conn} do |> json_response_and_validate_schema(200) assert response["id"] == activity.id + + account = response["account"] + actor = User.get_by_ap_id(activity.actor) + + assert account["id"] == actor.id + assert account["nickname"] == actor.nickname + assert account["deactivated"] == actor.deactivated + assert account["confirmation_pending"] == actor.confirmation_pending end end From 95ebfb9190e6e7d446213ca57e8c99aa3116ed0a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 26 May 2020 13:13:39 +0400 Subject: [PATCH 123/375] Move invite actions to AdminAPI.InviteTokenController --- .../controllers/admin_api_controller.ex | 72 ----- .../controllers/invite_token_controller.ex | 88 +++++++ .../admin/invite_token_operation.ex | 165 ++++++++++++ lib/pleroma/web/router.ex | 8 +- .../controllers/admin_api_controller_test.exs | 223 ---------------- .../invite_token_controller_test.exs | 247 ++++++++++++++++++ 6 files changed, 504 insertions(+), 299 deletions(-) create mode 100644 lib/pleroma/web/admin_api/controllers/invite_token_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex create mode 100644 test/web/admin_api/controllers/invite_token_controller_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 6b1d64a2e..95582b008 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -16,7 +16,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.ReportNote alias Pleroma.Stats alias Pleroma.User - alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline @@ -69,14 +68,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do ] ) - plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites) - - plug( - OAuthScopesPlug, - %{scopes: ["write:invites"], admin: true} - when action in [:create_invite_token, :revoke_invite, :email_invite] - ) - plug( OAuthScopesPlug, %{scopes: ["write:follows"], admin: true} @@ -575,69 +566,6 @@ def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) end end - @doc "Sends registration invite via email" - def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do - with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, - {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, - {:ok, invite_token} <- UserInviteToken.create_invite(), - email <- - Pleroma.Emails.UserEmail.user_invitation_email( - user, - invite_token, - email, - params["name"] - ), - {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do - json_response(conn, :no_content, "") - else - {:registrations_open, _} -> - {:error, "To send invites you need to set the `registrations_open` option to false."} - - {:invites_enabled, _} -> - {:error, "To send invites you need to set the `invites_enabled` option to true."} - end - end - - @doc "Create an account registration invite token" - def create_invite_token(conn, params) do - opts = %{} - - opts = - if params["max_use"], - do: Map.put(opts, :max_use, params["max_use"]), - else: opts - - opts = - if params["expires_at"], - do: Map.put(opts, :expires_at, params["expires_at"]), - else: opts - - {:ok, invite} = UserInviteToken.create_invite(opts) - - json(conn, AccountView.render("invite.json", %{invite: invite})) - end - - @doc "Get list of created invites" - def invites(conn, _params) do - invites = UserInviteToken.list_invites() - - conn - |> put_view(AccountView) - |> render("invites.json", %{invites: invites}) - end - - @doc "Revokes invite by token" - def revoke_invite(conn, %{"token" => token}) do - with {:ok, invite} <- UserInviteToken.find_by_token(token), - {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do - conn - |> put_view(AccountView) - |> render("invite.json", %{invite: updated_invite}) - else - nil -> {:error, :not_found} - end - end - @doc "Get a password reset token (base64 string) for given nickname" def get_password_reset(conn, %{"nickname" => nickname}) do (%User{local: true} = user) = User.get_cached_by_nickname(nickname) diff --git a/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex new file mode 100644 index 000000000..a0291e9c3 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex @@ -0,0 +1,88 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteTokenController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Config + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.UserInviteToken + alias Pleroma.Web.AdminAPI.AccountView + + require Logger + + plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index) + + plug( + OAuthScopesPlug, + %{scopes: ["write:invites"], admin: true} when action in [:create, :revoke, :email] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + @doc "Get list of created invites" + def index(conn, _params) do + invites = UserInviteToken.list_invites() + + conn + |> put_view(AccountView) + |> render("invites.json", %{invites: invites}) + end + + @doc "Create an account registration invite token" + def create(conn, params) do + opts = %{} + + opts = + if params["max_use"], + do: Map.put(opts, :max_use, params["max_use"]), + else: opts + + opts = + if params["expires_at"], + do: Map.put(opts, :expires_at, params["expires_at"]), + else: opts + + {:ok, invite} = UserInviteToken.create_invite(opts) + + json(conn, AccountView.render("invite.json", %{invite: invite})) + end + + @doc "Revokes invite by token" + def revoke(conn, %{"token" => token}) do + with {:ok, invite} <- UserInviteToken.find_by_token(token), + {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do + conn + |> put_view(AccountView) + |> render("invite.json", %{invite: updated_invite}) + else + nil -> {:error, :not_found} + end + end + + @doc "Sends registration invite via email" + def email(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do + with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, + {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, + {:ok, invite_token} <- UserInviteToken.create_invite(), + email <- + Pleroma.Emails.UserEmail.user_invitation_email( + user, + invite_token, + email, + params["name"] + ), + {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do + json_response(conn, :no_content, "") + else + {:registrations_open, _} -> + {:error, "To send invites you need to set the `registrations_open` option to false."} + + {:invites_enabled, _} -> + {:error, "To send invites you need to set the `invites_enabled` option to true."} + end + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex new file mode 100644 index 000000000..09a7735d1 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex @@ -0,0 +1,165 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.InviteTokenOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Status + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + import Pleroma.Web.ApiSpec.Helpers + import Pleroma.Web.ApiSpec.StatusOperation, only: [id_param: 0] + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Statuses"], + operationId: "AdminAPI.StatusController.index", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [ + Operation.parameter( + :godmode, + :query, + %Schema{type: :boolean, default: false}, + "Allows to see private statuses" + ), + Operation.parameter( + :local_only, + :query, + %Schema{type: :boolean, default: false}, + "Excludes remote statuses" + ), + Operation.parameter( + :with_reblogs, + :query, + %Schema{type: :boolean, default: false}, + "Allows to see reblogs" + ), + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of statuses to return" + ) + ], + responses: %{ + 200 => + Operation.response("Array of statuses", "application/json", %Schema{ + type: :array, + items: status() + }) + } + } + end + + def show_operation do + %Operation{ + tags: ["Admin", "Statuses"], + summary: "Show Status", + operationId: "AdminAPI.StatusController.show", + parameters: [id_param()], + security: [%{"oAuth" => ["read:statuses"]}], + responses: %{ + 200 => Operation.response("Status", "application/json", Status), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "Statuses"], + summary: "Change the scope of an individual reported status", + operationId: "AdminAPI.StatusController.update", + parameters: [id_param()], + security: [%{"oAuth" => ["write:statuses"]}], + requestBody: request_body("Parameters", update_request(), required: true), + responses: %{ + 200 => Operation.response("Status", "application/json", Status), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "Statuses"], + summary: "Delete an individual reported status", + operationId: "AdminAPI.StatusController.delete", + parameters: [id_param()], + security: [%{"oAuth" => ["write:statuses"]}], + responses: %{ + 200 => empty_object_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp status do + %Schema{ + anyOf: [ + Status, + %Schema{ + type: :object, + properties: %{ + account: %Schema{allOf: [Account, admin_account()]} + } + } + ] + } + end + + defp admin_account do + %Schema{ + type: :object, + properties: %{ + id: FlakeID, + avatar: %Schema{type: :string}, + nickname: %Schema{type: :string}, + display_name: %Schema{type: :string}, + deactivated: %Schema{type: :boolean}, + local: %Schema{type: :boolean}, + roles: %Schema{ + type: :object, + properties: %{ + admin: %Schema{type: :boolean}, + moderator: %Schema{type: :boolean} + } + }, + tags: %Schema{type: :string}, + confirmation_pending: %Schema{type: :string} + } + } + end + + defp update_request do + %Schema{ + type: :object, + properties: %{ + sensitive: %Schema{ + type: :boolean, + description: "Mark status and attached media as sensitive?" + }, + visibility: VisibilityScope + }, + example: %{ + "visibility" => "private", + "sensitive" => "false" + } + } + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e493a4153..fe36f0189 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do post("/relay", AdminAPIController, :relay_follow) delete("/relay", AdminAPIController, :relay_unfollow) - post("/users/invite_token", AdminAPIController, :create_invite_token) - get("/users/invites", AdminAPIController, :invites) - post("/users/revoke_invite", AdminAPIController, :revoke_invite) - post("/users/email_invite", AdminAPIController, :email_invite) + post("/users/invite_token", InviteTokenController, :create) + get("/users/invites", InviteTokenController, :index) + post("/users/revoke_invite", InviteTokenController, :revoke) + post("/users/email_invite", InviteTokenController, :email) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index 321840a8c..f7e163f57 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -20,7 +20,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User - alias Pleroma.UserInviteToken alias Pleroma.Web alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI @@ -588,122 +587,6 @@ test "/:right DELETE, can remove from a permission group (multiple)", %{ end end - describe "POST /api/pleroma/admin/email_invite, with valid config" do - 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" - recipient_name = "J. D." - - conn = - post( - conn, - "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" - ) - - assert json_response(conn, :no_content) - - token_record = List.last(Repo.all(Pleroma.UserInviteToken)) - assert token_record - refute token_record.used - - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - email = - Pleroma.Emails.UserEmail.user_invitation_email( - admin, - token_record, - recipient_email, - recipient_name - ) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {recipient_name, recipient_email}, - html_body: email.html_body - ) - end - - test "it returns 403 if requested by a non-admin" do - non_admin_user = insert(:user) - token = insert(:oauth_token, user: non_admin_user) - - conn = - build_conn() - |> assign(:user, non_admin_user) - |> assign(:token, token) - |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") - - assert json_response(conn, :forbidden) - end - - test "email with +", %{conn: conn, admin: admin} do - recipient_email = "foo+bar@baz.com" - - conn - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) - |> json_response(:no_content) - - token_record = - Pleroma.UserInviteToken - |> Repo.all() - |> List.last() - - assert token_record - refute token_record.used - - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - email = - Pleroma.Emails.UserEmail.user_invitation_email( - admin, - token_record, - recipient_email - ) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: recipient_email, - html_body: email.html_body - ) - end - end - - describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do - 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) - Config.put([:instance, :invites_enabled], false) - - conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") - - assert json_response(conn, :bad_request) == - %{ - "error" => - "To send invites you need to set the `invites_enabled` option to true." - } - end - - test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do - Config.put([:instance, :registrations_open], true) - Config.put([:instance, :invites_enabled], true) - - conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") - - assert json_response(conn, :bad_request) == - %{ - "error" => - "To send invites you need to set the `registrations_open` option to false." - } - end - end - test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do user = insert(:user) @@ -1318,112 +1201,6 @@ test "returns 404 if user not found", %{conn: conn} do end end - describe "POST /api/pleroma/admin/users/invite_token" do - test "without options", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/users/invite_token") - - invite_json = json_response(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - refute invite.expires_at - refute invite.max_use - assert invite.invite_type == "one_time" - end - - test "with expires_at", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/users/invite_token", %{ - "expires_at" => Date.to_string(Date.utc_today()) - }) - - invite_json = json_response(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - - refute invite.used - assert invite.expires_at == Date.utc_today() - refute invite.max_use - assert invite.invite_type == "date_limited" - end - - test "with max_use", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) - - invite_json = json_response(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - refute invite.expires_at - assert invite.max_use == 150 - assert invite.invite_type == "reusable" - end - - test "with max use and expires_at", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/users/invite_token", %{ - "max_use" => 150, - "expires_at" => Date.to_string(Date.utc_today()) - }) - - invite_json = json_response(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - assert invite.expires_at == Date.utc_today() - assert invite.max_use == 150 - assert invite.invite_type == "reusable_date_limited" - end - end - - describe "GET /api/pleroma/admin/users/invites" do - test "no invites", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/users/invites") - - assert json_response(conn, 200) == %{"invites" => []} - end - - test "with invite", %{conn: conn} do - {:ok, invite} = UserInviteToken.create_invite() - - conn = get(conn, "/api/pleroma/admin/users/invites") - - assert json_response(conn, 200) == %{ - "invites" => [ - %{ - "expires_at" => nil, - "id" => invite.id, - "invite_type" => "one_time", - "max_use" => nil, - "token" => invite.token, - "used" => false, - "uses" => 0 - } - ] - } - end - end - - describe "POST /api/pleroma/admin/users/revoke_invite" do - test "with token", %{conn: conn} do - {:ok, invite} = UserInviteToken.create_invite() - - conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) - - assert json_response(conn, 200) == %{ - "expires_at" => nil, - "id" => invite.id, - "invite_type" => "one_time", - "max_use" => nil, - "token" => invite.token, - "used" => true, - "uses" => 0 - } - end - - test "with invalid token", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) - - assert json_response(conn, :not_found) == %{"error" => "Not found"} - end - end - describe "GET /api/pleroma/admin/reports/:id" do test "returns report by its id", %{conn: conn} do [reporter, target_user] = insert_pair(:user) diff --git a/test/web/admin_api/controllers/invite_token_controller_test.exs b/test/web/admin_api/controllers/invite_token_controller_test.exs new file mode 100644 index 000000000..eb57b4d44 --- /dev/null +++ b/test/web/admin_api/controllers/invite_token_controller_test.exs @@ -0,0 +1,247 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteTokenControllerTest do + use Pleroma.Web.ConnCase, async: true + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.UserInviteToken + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "POST /api/pleroma/admin/users/email_invite, with valid config" do + 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" + recipient_name = "J. D." + + conn = + post( + conn, + "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" + ) + + assert json_response(conn, :no_content) + + token_record = List.last(Repo.all(Pleroma.UserInviteToken)) + assert token_record + refute token_record.used + + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + admin, + token_record, + recipient_email, + recipient_name + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {recipient_name, recipient_email}, + html_body: email.html_body + ) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + + assert json_response(conn, :forbidden) + end + + test "email with +", %{conn: conn, admin: admin} do + recipient_email = "foo+bar@baz.com" + + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) + |> json_response(:no_content) + + token_record = + Pleroma.UserInviteToken + |> Repo.all() + |> List.last() + + assert token_record + refute token_record.used + + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + admin, + token_record, + recipient_email + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: recipient_email, + html_body: email.html_body + ) + end + end + + describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do + 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) + Config.put([:instance, :invites_enabled], false) + + conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + + assert json_response(conn, :bad_request) == + %{ + "error" => + "To send invites you need to set the `invites_enabled` option to true." + } + end + + test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :invites_enabled], true) + + conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + + assert json_response(conn, :bad_request) == + %{ + "error" => + "To send invites you need to set the `registrations_open` option to false." + } + end + end + + describe "POST /api/pleroma/admin/users/invite_token" do + test "without options", %{conn: conn} do + conn = post(conn, "/api/pleroma/admin/users/invite_token") + + invite_json = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + refute invite.expires_at + refute invite.max_use + assert invite.invite_type == "one_time" + end + + test "with expires_at", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/users/invite_token", %{ + "expires_at" => Date.to_string(Date.utc_today()) + }) + + invite_json = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + + refute invite.used + assert invite.expires_at == Date.utc_today() + refute invite.max_use + assert invite.invite_type == "date_limited" + end + + test "with max_use", %{conn: conn} do + conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) + + invite_json = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + refute invite.expires_at + assert invite.max_use == 150 + assert invite.invite_type == "reusable" + end + + test "with max use and expires_at", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/users/invite_token", %{ + "max_use" => 150, + "expires_at" => Date.to_string(Date.utc_today()) + }) + + invite_json = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + assert invite.expires_at == Date.utc_today() + assert invite.max_use == 150 + assert invite.invite_type == "reusable_date_limited" + end + end + + describe "GET /api/pleroma/admin/users/invites" do + test "no invites", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response(conn, 200) == %{"invites" => []} + end + + test "with invite", %{conn: conn} do + {:ok, invite} = UserInviteToken.create_invite() + + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response(conn, 200) == %{ + "invites" => [ + %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => false, + "uses" => 0 + } + ] + } + end + end + + describe "POST /api/pleroma/admin/users/revoke_invite" do + test "with token", %{conn: conn} do + {:ok, invite} = UserInviteToken.create_invite() + + conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) + + assert json_response(conn, 200) == %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => true, + "uses" => 0 + } + end + + test "with invalid token", %{conn: conn} do + conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + + assert json_response(conn, :not_found) == %{"error" => "Not found"} + end + end +end From 2a4f965191af6ec6ab953569898acff55bd1502b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 26 May 2020 15:02:51 +0400 Subject: [PATCH 124/375] Add OpenAPI spec for AdminAPI.InviteTokenController --- .../controllers/invite_token_controller.ex | 25 +- .../admin/invite_token_operation.ex | 241 ++++++++---------- .../invite_token_controller_test.exs | 84 ++++-- 3 files changed, 179 insertions(+), 171 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex index a0291e9c3..a09966e5c 100644 --- a/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.AdminAPI.InviteTokenController do require Logger + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index) plug( @@ -23,6 +24,8 @@ defmodule Pleroma.Web.AdminAPI.InviteTokenController do action_fallback(Pleroma.Web.AdminAPI.FallbackController) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InviteTokenOperation + @doc "Get list of created invites" def index(conn, _params) do invites = UserInviteToken.list_invites() @@ -33,26 +36,14 @@ def index(conn, _params) do end @doc "Create an account registration invite token" - def create(conn, params) do - opts = %{} - - opts = - if params["max_use"], - do: Map.put(opts, :max_use, params["max_use"]), - else: opts - - opts = - if params["expires_at"], - do: Map.put(opts, :expires_at, params["expires_at"]), - else: opts - - {:ok, invite} = UserInviteToken.create_invite(opts) + def create(%{body_params: params} = conn, _) do + {:ok, invite} = UserInviteToken.create_invite(params) json(conn, AccountView.render("invite.json", %{invite: invite})) end @doc "Revokes invite by token" - def revoke(conn, %{"token" => token}) do + def revoke(%{body_params: %{token: token}} = conn, _) do with {:ok, invite} <- UserInviteToken.find_by_token(token), {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do conn @@ -64,7 +55,7 @@ def revoke(conn, %{"token" => token}) do end @doc "Sends registration invite via email" - def email(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do + def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, {:ok, invite_token} <- UserInviteToken.create_invite(), @@ -73,7 +64,7 @@ def email(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do user, invite_token, email, - params["name"] + params[:name] ), {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do json_response(conn, :no_content, "") diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex index 09a7735d1..0f7403f26 100644 --- a/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex @@ -5,14 +5,9 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteTokenOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema - alias Pleroma.Web.ApiSpec.Schemas.Account alias Pleroma.Web.ApiSpec.Schemas.ApiError - alias Pleroma.Web.ApiSpec.Schemas.FlakeID - alias Pleroma.Web.ApiSpec.Schemas.Status - alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope import Pleroma.Web.ApiSpec.Helpers - import Pleroma.Web.ApiSpec.StatusOperation, only: [id_param: 0] def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -21,144 +16,132 @@ def open_api_operation(action) do def index_operation do %Operation{ - tags: ["Admin", "Statuses"], - operationId: "AdminAPI.StatusController.index", - security: [%{"oAuth" => ["read:statuses"]}], - parameters: [ - Operation.parameter( - :godmode, - :query, - %Schema{type: :boolean, default: false}, - "Allows to see private statuses" - ), - Operation.parameter( - :local_only, - :query, - %Schema{type: :boolean, default: false}, - "Excludes remote statuses" - ), - Operation.parameter( - :with_reblogs, - :query, - %Schema{type: :boolean, default: false}, - "Allows to see reblogs" - ), - Operation.parameter( - :page, - :query, - %Schema{type: :integer, default: 1}, - "Page" - ), - Operation.parameter( - :page_size, - :query, - %Schema{type: :integer, default: 50}, - "Number of statuses to return" - ) - ], + tags: ["Admin", "Invites"], + summary: "Get a list of generated invites", + operationId: "AdminAPI.InviteTokenController.index", + security: [%{"oAuth" => ["read:invites"]}], responses: %{ 200 => - Operation.response("Array of statuses", "application/json", %Schema{ - type: :array, - items: status() + Operation.response("Intites", "application/json", %Schema{ + type: :object, + properties: %{ + invites: %Schema{type: :array, items: invite()} + }, + example: %{ + "invites" => [ + %{ + "id" => 123, + "token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=", + "used" => true, + "expires_at" => nil, + "uses" => 0, + "max_use" => nil, + "invite_type" => "one_time" + } + ] + } }) } } end - def show_operation do + def create_operation do %Operation{ - tags: ["Admin", "Statuses"], - summary: "Show Status", - operationId: "AdminAPI.StatusController.show", - parameters: [id_param()], - security: [%{"oAuth" => ["read:statuses"]}], - responses: %{ - 200 => Operation.response("Status", "application/json", Status), - 404 => Operation.response("Not Found", "application/json", ApiError) - } - } - end - - def update_operation do - %Operation{ - tags: ["Admin", "Statuses"], - summary: "Change the scope of an individual reported status", - operationId: "AdminAPI.StatusController.update", - parameters: [id_param()], - security: [%{"oAuth" => ["write:statuses"]}], - requestBody: request_body("Parameters", update_request(), required: true), - responses: %{ - 200 => Operation.response("Status", "application/json", Status), - 400 => Operation.response("Error", "application/json", ApiError) - } - } - end - - def delete_operation do - %Operation{ - tags: ["Admin", "Statuses"], - summary: "Delete an individual reported status", - operationId: "AdminAPI.StatusController.delete", - parameters: [id_param()], - security: [%{"oAuth" => ["write:statuses"]}], - responses: %{ - 200 => empty_object_response(), - 404 => Operation.response("Not Found", "application/json", ApiError) - } - } - end - - defp status do - %Schema{ - anyOf: [ - Status, - %Schema{ + tags: ["Admin", "Invites"], + summary: "Create an account registration invite token", + operationId: "AdminAPI.InviteTokenController.create", + security: [%{"oAuth" => ["write:invites"]}], + requestBody: + request_body("Parameters", %Schema{ type: :object, properties: %{ - account: %Schema{allOf: [Account, admin_account()]} + max_use: %Schema{type: :integer}, + expires_at: %Schema{type: :string, format: :date, example: "2020-04-20"} } + }), + responses: %{ + 200 => Operation.response("Invite", "application/json", invite()) + } + } + end + + def revoke_operation do + %Operation{ + tags: ["Admin", "Invites"], + summary: "Revoke invite by token", + operationId: "AdminAPI.InviteTokenController.revoke", + security: [%{"oAuth" => ["write:invites"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + required: [:token], + properties: %{ + token: %Schema{type: :string} + } + }, + required: true + ), + responses: %{ + 200 => Operation.response("Invite", "application/json", invite()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def email_operation do + %Operation{ + tags: ["Admin", "Invites"], + summary: "Sends registration invite via email", + operationId: "AdminAPI.InviteTokenController.email", + security: [%{"oAuth" => ["write:invites"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + required: [:email], + properties: %{ + email: %Schema{type: :string, format: :email}, + name: %Schema{type: :string} + } + }, + required: true + ), + responses: %{ + 204 => no_content_response(), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + defp invite do + %Schema{ + title: "Invite", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + token: %Schema{type: :string}, + used: %Schema{type: :boolean}, + expires_at: %Schema{type: :string, format: :date, nullable: true}, + uses: %Schema{type: :integer}, + max_use: %Schema{type: :integer, nullable: true}, + invite_type: %Schema{ + type: :string, + enum: ["one_time", "reusable", "date_limited", "reusable_date_limited"] } - ] - } - end - - defp admin_account do - %Schema{ - type: :object, - properties: %{ - id: FlakeID, - avatar: %Schema{type: :string}, - nickname: %Schema{type: :string}, - display_name: %Schema{type: :string}, - deactivated: %Schema{type: :boolean}, - local: %Schema{type: :boolean}, - roles: %Schema{ - type: :object, - properties: %{ - admin: %Schema{type: :boolean}, - moderator: %Schema{type: :boolean} - } - }, - tags: %Schema{type: :string}, - confirmation_pending: %Schema{type: :string} - } - } - end - - defp update_request do - %Schema{ - type: :object, - properties: %{ - sensitive: %Schema{ - type: :boolean, - description: "Mark status and attached media as sensitive?" - }, - visibility: VisibilityScope }, example: %{ - "visibility" => "private", - "sensitive" => "false" + "id" => 123, + "token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=", + "used" => true, + "expires_at" => nil, + "uses" => 0, + "max_use" => nil, + "invite_type" => "one_time" } } end diff --git a/test/web/admin_api/controllers/invite_token_controller_test.exs b/test/web/admin_api/controllers/invite_token_controller_test.exs index eb57b4d44..cb486f4d1 100644 --- a/test/web/admin_api/controllers/invite_token_controller_test.exs +++ b/test/web/admin_api/controllers/invite_token_controller_test.exs @@ -32,12 +32,14 @@ test "sends invitation and returns 204", %{admin: admin, conn: conn} do recipient_name = "J. D." conn = - post( - conn, - "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" - ) + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: recipient_email, + name: recipient_name + }) - assert json_response(conn, :no_content) + assert json_response_and_validate_schema(conn, :no_content) token_record = List.last(Repo.all(Pleroma.UserInviteToken)) assert token_record @@ -69,7 +71,11 @@ test "it returns 403 if requested by a non-admin" do build_conn() |> assign(:user, non_admin_user) |> assign(:token, token) - |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) assert json_response(conn, :forbidden) end @@ -80,7 +86,7 @@ test "email with +", %{conn: conn, admin: admin} do conn |> put_req_header("content-type", "application/json;charset=utf-8") |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) - |> json_response(:no_content) + |> json_response_and_validate_schema(:no_content) token_record = Pleroma.UserInviteToken @@ -116,9 +122,15 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do Config.put([:instance, :registrations_open], false) Config.put([:instance, :invites_enabled], false) - conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) - assert json_response(conn, :bad_request) == + assert json_response_and_validate_schema(conn, :bad_request) == %{ "error" => "To send invites you need to set the `invites_enabled` option to true." @@ -129,9 +141,15 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do Config.put([:instance, :registrations_open], true) Config.put([:instance, :invites_enabled], true) - conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) - assert json_response(conn, :bad_request) == + assert json_response_and_validate_schema(conn, :bad_request) == %{ "error" => "To send invites you need to set the `registrations_open` option to false." @@ -141,9 +159,12 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do describe "POST /api/pleroma/admin/users/invite_token" do test "without options", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/users/invite_token") + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token") - invite_json = json_response(conn, 200) + invite_json = json_response_and_validate_schema(conn, 200) invite = UserInviteToken.find_by_token!(invite_json["token"]) refute invite.used refute invite.expires_at @@ -153,11 +174,13 @@ test "without options", %{conn: conn} do test "with expires_at", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/users/invite_token", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{ "expires_at" => Date.to_string(Date.utc_today()) }) - invite_json = json_response(conn, 200) + invite_json = json_response_and_validate_schema(conn, 200) invite = UserInviteToken.find_by_token!(invite_json["token"]) refute invite.used @@ -167,9 +190,12 @@ test "with expires_at", %{conn: conn} do end test "with max_use", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) - invite_json = json_response(conn, 200) + invite_json = json_response_and_validate_schema(conn, 200) invite = UserInviteToken.find_by_token!(invite_json["token"]) refute invite.used refute invite.expires_at @@ -179,12 +205,14 @@ test "with max_use", %{conn: conn} do test "with max use and expires_at", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/users/invite_token", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{ "max_use" => 150, "expires_at" => Date.to_string(Date.utc_today()) }) - invite_json = json_response(conn, 200) + invite_json = json_response_and_validate_schema(conn, 200) invite = UserInviteToken.find_by_token!(invite_json["token"]) refute invite.used assert invite.expires_at == Date.utc_today() @@ -197,7 +225,7 @@ test "with max use and expires_at", %{conn: conn} do test "no invites", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users/invites") - assert json_response(conn, 200) == %{"invites" => []} + assert json_response_and_validate_schema(conn, 200) == %{"invites" => []} end test "with invite", %{conn: conn} do @@ -205,7 +233,7 @@ test "with invite", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users/invites") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "invites" => [ %{ "expires_at" => nil, @@ -225,9 +253,12 @@ test "with invite", %{conn: conn} do test "with token", %{conn: conn} do {:ok, invite} = UserInviteToken.create_invite() - conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "expires_at" => nil, "id" => invite.id, "invite_type" => "one_time", @@ -239,9 +270,12 @@ test "with token", %{conn: conn} do end test "with invalid token", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) - assert json_response(conn, :not_found) == %{"error" => "Not found"} + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} end end end From fca48154a23c0b38d514b2bc4d49a74274e02a8f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 26 May 2020 15:21:33 +0400 Subject: [PATCH 125/375] Add AdminAPI.InviteView --- ...ken_controller.ex => invite_controller.ex} | 29 +++++++++---------- .../web/admin_api/views/account_view.ex | 18 ------------ .../web/admin_api/views/invite_view.ex | 25 ++++++++++++++++ ...token_operation.ex => invite_operation.ex} | 10 +++---- lib/pleroma/web/router.ex | 8 ++--- ...er_test.exs => invite_controller_test.exs} | 2 +- 6 files changed, 49 insertions(+), 43 deletions(-) rename lib/pleroma/web/admin_api/controllers/{invite_token_controller.ex => invite_controller.ex} (79%) create mode 100644 lib/pleroma/web/admin_api/views/invite_view.ex rename lib/pleroma/web/api_spec/operations/admin/{invite_token_operation.ex => invite_operation.ex} (93%) rename test/web/admin_api/controllers/{invite_token_controller_test.exs => invite_controller_test.exs} (99%) diff --git a/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex similarity index 79% rename from lib/pleroma/web/admin_api/controllers/invite_token_controller.ex rename to lib/pleroma/web/admin_api/controllers/invite_controller.ex index a09966e5c..7d169b8d2 100644 --- a/lib/pleroma/web/admin_api/controllers/invite_token_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.AdminAPI.InviteTokenController do +defmodule Pleroma.Web.AdminAPI.InviteController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, only: [json_response: 3] @@ -10,7 +10,6 @@ defmodule Pleroma.Web.AdminAPI.InviteTokenController do alias Pleroma.Config alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.UserInviteToken - alias Pleroma.Web.AdminAPI.AccountView require Logger @@ -24,33 +23,30 @@ defmodule Pleroma.Web.AdminAPI.InviteTokenController do action_fallback(Pleroma.Web.AdminAPI.FallbackController) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InviteTokenOperation + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InviteOperation @doc "Get list of created invites" def index(conn, _params) do invites = UserInviteToken.list_invites() - conn - |> put_view(AccountView) - |> render("invites.json", %{invites: invites}) + render(conn, "index.json", invites: invites) end @doc "Create an account registration invite token" def create(%{body_params: params} = conn, _) do {:ok, invite} = UserInviteToken.create_invite(params) - json(conn, AccountView.render("invite.json", %{invite: invite})) + render(conn, "show.json", invite: invite) end @doc "Revokes invite by token" def revoke(%{body_params: %{token: token}} = conn, _) do with {:ok, invite} <- UserInviteToken.find_by_token(token), {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do - conn - |> put_view(AccountView) - |> render("invite.json", %{invite: updated_invite}) + render(conn, "show.json", invite: updated_invite) else nil -> {:error, :not_found} + error -> error end end @@ -59,14 +55,14 @@ def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = con with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, {:ok, invite_token} <- UserInviteToken.create_invite(), - email <- - Pleroma.Emails.UserEmail.user_invitation_email( - user, + {:ok, _} <- + user + |> Pleroma.Emails.UserEmail.user_invitation_email( invite_token, email, params[:name] - ), - {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do + ) + |> Pleroma.Emails.Mailer.deliver() do json_response(conn, :no_content, "") else {:registrations_open, _} -> @@ -74,6 +70,9 @@ def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = con {:invites_enabled, _} -> {:error, "To send invites you need to set the `invites_enabled` option to true."} + + {:error, error} -> + {:error, error} end end end diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 46dadb5ee..120159527 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -80,24 +80,6 @@ def render("show.json", %{user: user}) do } end - def render("invite.json", %{invite: invite}) do - %{ - "id" => invite.id, - "token" => invite.token, - "used" => invite.used, - "expires_at" => invite.expires_at, - "uses" => invite.uses, - "max_use" => invite.max_use, - "invite_type" => invite.invite_type - } - end - - def render("invites.json", %{invites: invites}) do - %{ - invites: render_many(invites, AccountView, "invite.json", as: :invite) - } - end - def render("created.json", %{user: user}) do %{ type: "success", diff --git a/lib/pleroma/web/admin_api/views/invite_view.ex b/lib/pleroma/web/admin_api/views/invite_view.ex new file mode 100644 index 000000000..f93cb6916 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/invite_view.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteView do + use Pleroma.Web, :view + + def render("index.json", %{invites: invites}) do + %{ + invites: render_many(invites, __MODULE__, "show.json", as: :invite) + } + end + + def render("show.json", %{invite: invite}) do + %{ + "id" => invite.id, + "token" => invite.token, + "used" => invite.used, + "expires_at" => invite.expires_at, + "uses" => invite.uses, + "max_use" => invite.max_use, + "invite_type" => invite.invite_type + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex similarity index 93% rename from lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex rename to lib/pleroma/web/api_spec/operations/admin/invite_operation.ex index 0f7403f26..4ae44fff6 100644 --- a/lib/pleroma/web/api_spec/operations/admin/invite_token_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.Admin.InviteTokenOperation do +defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.ApiError @@ -18,7 +18,7 @@ def index_operation do %Operation{ tags: ["Admin", "Invites"], summary: "Get a list of generated invites", - operationId: "AdminAPI.InviteTokenController.index", + operationId: "AdminAPI.InviteController.index", security: [%{"oAuth" => ["read:invites"]}], responses: %{ 200 => @@ -49,7 +49,7 @@ def create_operation do %Operation{ tags: ["Admin", "Invites"], summary: "Create an account registration invite token", - operationId: "AdminAPI.InviteTokenController.create", + operationId: "AdminAPI.InviteController.create", security: [%{"oAuth" => ["write:invites"]}], requestBody: request_body("Parameters", %Schema{ @@ -69,7 +69,7 @@ def revoke_operation do %Operation{ tags: ["Admin", "Invites"], summary: "Revoke invite by token", - operationId: "AdminAPI.InviteTokenController.revoke", + operationId: "AdminAPI.InviteController.revoke", security: [%{"oAuth" => ["write:invites"]}], requestBody: request_body( @@ -95,7 +95,7 @@ def email_operation do %Operation{ tags: ["Admin", "Invites"], summary: "Sends registration invite via email", - operationId: "AdminAPI.InviteTokenController.email", + operationId: "AdminAPI.InviteController.email", security: [%{"oAuth" => ["write:invites"]}], requestBody: request_body( diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index fe36f0189..9b7c7ee3d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do post("/relay", AdminAPIController, :relay_follow) delete("/relay", AdminAPIController, :relay_unfollow) - post("/users/invite_token", InviteTokenController, :create) - get("/users/invites", InviteTokenController, :index) - post("/users/revoke_invite", InviteTokenController, :revoke) - post("/users/email_invite", InviteTokenController, :email) + post("/users/invite_token", InviteController, :create) + get("/users/invites", InviteController, :index) + post("/users/revoke_invite", InviteController, :revoke) + post("/users/email_invite", InviteController, :email) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset) diff --git a/test/web/admin_api/controllers/invite_token_controller_test.exs b/test/web/admin_api/controllers/invite_controller_test.exs similarity index 99% rename from test/web/admin_api/controllers/invite_token_controller_test.exs rename to test/web/admin_api/controllers/invite_controller_test.exs index cb486f4d1..ab186c5e7 100644 --- a/test/web/admin_api/controllers/invite_token_controller_test.exs +++ b/test/web/admin_api/controllers/invite_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.AdminAPI.InviteTokenControllerTest do +defmodule Pleroma.Web.AdminAPI.InviteControllerTest do use Pleroma.Web.ConnCase, async: true import Pleroma.Factory From 3249141588c8f73f1958f782041798fbde05e69f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 27 May 2020 09:42:28 +0300 Subject: [PATCH 126/375] validate actor type --- docs/API/admin_api.md | 18 +++++++++- lib/pleroma/user.ex | 5 +-- .../controllers/admin_api_controller.ex | 13 ++++--- .../controllers/admin_api_controller_test.exs | 35 ++++++++++++++++--- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index c455047cc..639c3224d 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -511,7 +511,23 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `discoverable` - `actor_type` -- Response: none (code `200`) +- Response: + +```json +{"status": "success"} +``` + +```json +{"errors": + {"actor_type": "is invalid"}, + {"email": "has invalid format"}, + ... + } +``` + +```json +{"error": "Unable to update user."} +``` ## `GET /api/pleroma/admin/reports` diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 842b28c06..2684e1139 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -538,9 +538,10 @@ def update_as_admin_changeset(struct, params) do |> delete_change(:also_known_as) |> unique_constraint(:email) |> validate_format(:email, @email_regex) + |> validate_inclusion(:actor_type, ["Person", "Service"]) end - @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} + @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()} def update_as_admin(user, params) do params = Map.put(params, "password_confirmation", params["password"]) changeset = update_as_admin_changeset(user, params) @@ -561,7 +562,7 @@ def password_update_changeset(struct, params) do |> put_change(:password_reset_pending, false) end - @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} + @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()} def reset_password(%User{} = user, params) do reset_password(user, user, params) end diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 6b1d64a2e..6aedccec6 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -693,7 +693,7 @@ def update_user_credentials( %{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params ) do - with {_, user} <- {:user, User.get_cached_by_nickname(nickname)}, + with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)}, {:ok, _user} <- User.update_as_admin(user, params) do ModerationLog.insert_log(%{ @@ -715,11 +715,16 @@ def update_user_credentials( json(conn, %{status: "success"}) else {:error, changeset} -> - {_, {error, _}} = Enum.at(changeset.errors, 0) - json(conn, %{error: "New password #{error}."}) + errors = + Enum.reduce(changeset.errors, %{}, fn + {key, {error, _}}, acc -> + Map.put(acc, key, error) + end) + + json(conn, %{errors: errors}) _ -> - json(conn, %{error: "Unable to change password."}) + json(conn, %{error: "Unable to update user."}) end end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index 321840a8c..ead840186 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -3191,8 +3191,12 @@ test "returns 403 if requested by a non-admin" do end describe "PATCH /users/:nickname/credentials" do - test "changes password and email", %{conn: conn, admin: admin} do + setup do user = insert(:user) + [user: user] + end + + test "changes password and email", %{conn: conn, admin: admin, user: user} do assert user.password_reset_pending == false conn = @@ -3222,9 +3226,7 @@ test "changes password and email", %{conn: conn, admin: admin} do "@#{admin.nickname} forced password reset for users: @#{user.nickname}" end - test "returns 403 if requested by a non-admin" do - user = insert(:user) - + test "returns 403 if requested by a non-admin", %{user: user} do conn = build_conn() |> assign(:user, user) @@ -3236,6 +3238,31 @@ test "returns 403 if requested by a non-admin" do assert json_response(conn, :forbidden) end + + test "changes actor type from permitted list", %{conn: conn, user: user} do + assert user.actor_type == "Person" + + assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "actor_type" => "Service" + }) + |> json_response(200) == %{"status" => "success"} + + updated_user = User.get_by_id(user.id) + + assert updated_user.actor_type == "Service" + + assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "actor_type" => "Application" + }) + |> json_response(200) == %{"errors" => %{"actor_type" => "is invalid"}} + end + + test "update non existing user", %{conn: conn} do + assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{ + "password" => "new_password" + }) + |> json_response(200) == %{"error" => "Unable to update user."} + end end describe "PATCH /users/:nickname/force_password_reset" do From c6290be682bd12b1772153d421f36e5ddb9d664b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 27 May 2020 14:42:21 +0400 Subject: [PATCH 127/375] Fix typo --- lib/pleroma/web/api_spec/operations/admin/invite_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex index 4ae44fff6..d3af9db49 100644 --- a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex @@ -22,7 +22,7 @@ def index_operation do security: [%{"oAuth" => ["read:invites"]}], responses: %{ 200 => - Operation.response("Intites", "application/json", %Schema{ + Operation.response("Invites", "application/json", %Schema{ type: :object, properties: %{ invites: %Schema{type: :array, items: invite()} From 047a11c48f2bc88b6b278b6a5acd94807c7e5138 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 27 May 2020 10:55:42 +0000 Subject: [PATCH 128/375] Apply suggestion to lib/pleroma/web/admin_api/controllers/admin_api_controller.ex --- .../web/admin_api/controllers/admin_api_controller.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 6aedccec6..783203c07 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -715,11 +715,7 @@ def update_user_credentials( json(conn, %{status: "success"}) else {:error, changeset} -> - errors = - Enum.reduce(changeset.errors, %{}, fn - {key, {error, _}}, acc -> - Map.put(acc, key, error) - end) + errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end) json(conn, %{errors: errors}) From 48fd9be65ae2c25e170e494720a07c126e80e2f6 Mon Sep 17 00:00:00 2001 From: kPherox Date: Tue, 26 May 2020 09:47:03 +0000 Subject: [PATCH 129/375] Exclude post actor from to of relay announce --- lib/pleroma/web/activity_pub/builder.ex | 16 +++++++++++----- test/web/activity_pub/relay_test.exs | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 7ece764f5..51b74414a 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -85,15 +86,20 @@ def like(actor, object) do end end + @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} def announce(actor, object, options \\ []) do public? = Keyword.get(options, :public, false) - to = [actor.follower_address, object.data["actor"]] to = - if public? do - [Pleroma.Constants.as_public() | to] - else - to + cond do + actor.ap_id == Relay.relay_ap_id() -> + [actor.follower_address] + + public? -> + [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] + + true -> + [actor.follower_address, object.data["actor"]] end {:ok, diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index dbee8a0f4..b3b573c9b 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -108,6 +108,7 @@ test "returns error when object is unknown" do assert {:ok, %Activity{} = activity} = Relay.publish(note) assert activity.data["type"] == "Announce" assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [service_actor.follower_address] assert called(Pleroma.Web.Federator.publish(activity)) end From 78c46fb7ba2aa9e9842d3c7d8331488fd10a3b9d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 27 May 2020 19:34:56 +0300 Subject: [PATCH 130/375] MediaProxy test: use config macros instead of directly putting values They were not properly cleaned later and caused trouble for another tests --- test/web/media_proxy/media_proxy_test.exs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index 69c2d5dae..69d2a71a6 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -124,15 +124,7 @@ test "encoded url are tried to match for proxy as `conn.request_path` encodes th end test "uses the configured base_url" do - base_url = Pleroma.Config.get([:media_proxy, :base_url]) - - if base_url do - on_exit(fn -> - Pleroma.Config.put([:media_proxy, :base_url], base_url) - end) - end - - Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") + clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") url = "https://pleroma.soykaf.com/static/logo.png" encoded = url(url) @@ -213,8 +205,8 @@ test "mediaproxy whitelist" do end test "does not change whitelisted urls" do - Pleroma.Config.put([:media_proxy, :whitelist], ["mycdn.akamai.com"]) - Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") + clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"]) + clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") media_url = "https://mycdn.akamai.com" From 8f6d428880721d4b0151991e7943706b70ab8005 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 27 May 2020 19:35:35 +0300 Subject: [PATCH 131/375] AccountView: Use mediaproxy URLs for emojis Also use atom keys in emoji maps instead of binaries Closes #1810 --- .../web/mastodon_api/views/account_view.ex | 12 ++++--- .../mastodon_api/views/account_view_test.exs | 35 ++++++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 45fffaad2..04c419d2f 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -182,12 +182,14 @@ defp do_render("show.json", %{user: user} = opts) do bot = user.actor_type in ["Application", "Service"] emojis = - Enum.map(user.emoji, fn {shortcode, url} -> + Enum.map(user.emoji, fn {shortcode, raw_url} -> + url = MediaProxy.url(raw_url) + %{ - "shortcode" => shortcode, - "url" => url, - "static_url" => url, - "visible_in_picker" => false + shortcode: shortcode, + url: url, + static_url: url, + visible_in_picker: false } end) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 487ec26c2..f91333e5c 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -54,10 +54,10 @@ test "Represent a user account" do header_static: "http://localhost:4001/images/banner.png", emojis: [ %{ - "static_url" => "/file.png", - "url" => "/file.png", - "shortcode" => "karjalanpiirakka", - "visible_in_picker" => false + static_url: "/file.png", + url: "/file.png", + shortcode: "karjalanpiirakka", + visible_in_picker: false } ], fields: [], @@ -491,4 +491,31 @@ test "shows non-zero when historical unapproved requests are present" do AccountView.render("show.json", %{user: user, for: user}) end end + + test "uses mediaproxy urls when it's enabled" do + clear_config([:media_proxy, :enabled], true) + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, + banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]}, + emoji: %{"joker_smile" => "https://evil.website/society.png"} + ) + + AccountView.render("show.json", %{user: user}) + |> Enum.all?(fn + {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> + String.starts_with?(url, Pleroma.Web.base_url()) + + {:emojis, emojis} -> + Enum.all?(emojis, fn %{url: url, static_url: static_url} -> + String.starts_with?(url, Pleroma.Web.base_url()) && + String.starts_with?(static_url, Pleroma.Web.base_url()) + end) + + _ -> + true + end) + |> assert() + end end From 455a402c8a967b3a234c836b0574c4f011860d43 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 27 May 2020 20:27:30 +0300 Subject: [PATCH 132/375] HTTP Security plug: rewrite &csp_string/0 - Directives are now separated with ";" instead of " ;", according to https://www.w3.org/TR/CSP2/#policy-parsing the space is optional - Use an IO list, which at the end gets converted to a binary as opposed to ++ing a bunch of arrays with binaries together and joining them to a string. I doubt it gives any significant real world advantage, but the code is cleaner and now I can sleep at night. - The static part of csp is pre-joined to a single binary at compile time. Same reasoning as the last point. --- lib/pleroma/plugs/http_security_plug.ex | 52 ++++++++++++++----------- test/plugs/http_security_plug_test.exs | 2 +- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 6462797b6..f9aff2fab 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -31,7 +31,7 @@ defp headers do {"x-content-type-options", "nosniff"}, {"referrer-policy", referrer_policy}, {"x-download-options", "noopen"}, - {"content-security-policy", csp_string() <> ";"} + {"content-security-policy", csp_string()} ] if report_uri do @@ -43,23 +43,35 @@ defp headers do ] } - headers ++ [{"reply-to", Jason.encode!(report_group)}] + [{"reply-to", Jason.encode!(report_group)} | headers] else headers end end + @csp_start [ + "default-src 'none'", + "base-uri 'self'", + "frame-ancestors 'none'", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "manifest-src 'self'" + ] + |> Enum.join(";") + |> Kernel.<>(";") + |> List.wrap() + defp csp_string do scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] static_url = Pleroma.Web.Endpoint.static_url() websocket_url = Pleroma.Web.Endpoint.websocket_url() report_uri = Config.get([:http_security, :report_uri]) - connect_src = "connect-src 'self' #{static_url} #{websocket_url}" + connect_src = ["connect-src 'self' ", static_url, ?\s, websocket_url] connect_src = if Pleroma.Config.get(:env) == :dev do - connect_src <> " http://localhost:3035/" + [connect_src," http://localhost:3035/"] else connect_src end @@ -71,27 +83,23 @@ defp csp_string do "script-src 'self'" end - main_part = [ - "default-src 'none'", - "base-uri 'self'", - "frame-ancestors 'none'", - "img-src 'self' data: blob: https:", - "media-src 'self' https:", - "style-src 'self' 'unsafe-inline'", - "font-src 'self'", - "manifest-src 'self'", - connect_src, - script_src - ] + report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] + insecure = if scheme == "https", do: "upgrade-insecure-requests" - report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: [] - - insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: [] - - (main_part ++ report ++ insecure) - |> Enum.join("; ") + @csp_start + |> add_csp_param("img-src 'self' data: blob: https:") + |> add_csp_param("media-src 'self' https:") + |> add_csp_param(connect_src) + |> add_csp_param(script_src) + |> add_csp_param(insecure) + |> add_csp_param(report) + |> :erlang.iolist_to_binary() end + defp add_csp_param(csp_iodata, nil), do: csp_iodata + + defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] + def warn_if_disabled do unless Config.get([:http_security, :enabled]) do Logger.warn(" diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 84e4c274f..63b4d3f31 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -67,7 +67,7 @@ test "it sends `report-to` & `report-uri` CSP response headers" do [csp] = Conn.get_resp_header(conn, "content-security-policy") - assert csp =~ ~r|report-uri https://endpoint.com; report-to csp-endpoint;| + assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;| [reply_to] = Conn.get_resp_header(conn, "reply-to") From 29ff6d414ba096e74e04264af895abcabcf580b4 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 27 May 2020 21:01:36 +0300 Subject: [PATCH 133/375] HTTP security plug: Harden img-src and media-src when MediaProxy is enabled --- lib/pleroma/plugs/http_security_plug.ex | 41 +++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index f9aff2fab..df38d5022 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -67,11 +67,23 @@ defp csp_string do websocket_url = Pleroma.Web.Endpoint.websocket_url() report_uri = Config.get([:http_security, :report_uri]) + img_src = "img-src 'self' data: blob:" + media_src = "media-src 'self'" + + {img_src, media_src} = + if Config.get([:media_proxy, :enabled]) && + !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do + sources = get_proxy_and_attachment_sources() + {[img_src, sources], [media_src, sources]} + else + {img_src, media_src} + end + connect_src = ["connect-src 'self' ", static_url, ?\s, websocket_url] connect_src = if Pleroma.Config.get(:env) == :dev do - [connect_src," http://localhost:3035/"] + [connect_src, " http://localhost:3035/"] else connect_src end @@ -87,8 +99,8 @@ defp csp_string do insecure = if scheme == "https", do: "upgrade-insecure-requests" @csp_start - |> add_csp_param("img-src 'self' data: blob: https:") - |> add_csp_param("media-src 'self' https:") + |> add_csp_param(img_src) + |> add_csp_param(media_src) |> add_csp_param(connect_src) |> add_csp_param(script_src) |> add_csp_param(insecure) @@ -96,6 +108,29 @@ defp csp_string do |> :erlang.iolist_to_binary() end + defp get_proxy_and_attachment_sources do + media_proxy_whitelist = + Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> + add_source(acc, host) + end) + + upload_base_url = + if Config.get([Pleroma.Upload, :base_url]), + do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host + + s3_endpoint = + if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3, + do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host + + [] + |> add_source(upload_base_url) + |> add_source(s3_endpoint) + |> add_source(media_proxy_whitelist) + end + + defp add_source(iodata, nil), do: iodata + defp add_source(iodata, source), do: [[?\s, source] | iodata] + defp add_csp_param(csp_iodata, nil), do: csp_iodata defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] From a2f57bd82b1b495a754516231b56e53ae41c6b69 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 27 May 2020 16:27:07 -0500 Subject: [PATCH 134/375] Permit easy access to vaccum full and analyze via a mix task --- lib/mix/tasks/pleroma/database.ex | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 778de162f..c4f343f04 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -135,4 +135,30 @@ def run(["fix_likes_collections"]) do end) |> Stream.run() end + + def run(["vacuum", args]) do + start_pleroma() + + case args do + "analyze" -> + Logger.info("Runnning VACUUM ANALYZE.") + Repo.query!( + "vacuum analyze;", + [], + timeout: :infinity + ) + + "full" -> + Logger.info("Runnning VACUUM FULL. This could take a while.") + + Repo.query!( + "vacuum full;", + [], + timeout: :infinity + ) + + _ -> + Logger.error("Error: invalid vacuum argument.") + end + end end From 73ca57e4f1620ddaf167c368f48a0096b2096a96 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 27 May 2020 16:27:29 -0500 Subject: [PATCH 135/375] Make it obvious a full vacuum can take a while --- lib/mix/tasks/pleroma/database.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index c4f343f04..1fdafcc88 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -34,7 +34,7 @@ def run(["remove_embedded_objects" | args]) do ) if Keyword.get(options, :vacuum) do - Logger.info("Runnning VACUUM FULL") + Logger.info("Runnning VACUUM FULL. This could take a while.") Repo.query!( "vacuum full;", @@ -94,7 +94,7 @@ def run(["prune_objects" | args]) do |> Repo.delete_all(timeout: :infinity) if Keyword.get(options, :vacuum) do - Logger.info("Runnning VACUUM FULL") + Logger.info("Runnning VACUUM FULL. This could take a while.") Repo.query!( "vacuum full;", From 0d57e066260234fb582a63870cbae7517e7b6246 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 27 May 2020 16:31:37 -0500 Subject: [PATCH 136/375] Make clearer that this is time and resource consuming --- lib/mix/tasks/pleroma/database.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 1fdafcc88..2f1f33469 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -34,7 +34,11 @@ def run(["remove_embedded_objects" | args]) do ) if Keyword.get(options, :vacuum) do - Logger.info("Runnning VACUUM FULL. This could take a while.") + Logger.info("Runnning VACUUM FULL.") + + Logger.warn( + "Re-packing your entire database may take a while and will consume extra disk space during the process." + ) Repo.query!( "vacuum full;", @@ -94,7 +98,11 @@ def run(["prune_objects" | args]) do |> Repo.delete_all(timeout: :infinity) if Keyword.get(options, :vacuum) do - Logger.info("Runnning VACUUM FULL. This could take a while.") + Logger.info("Runnning VACUUM FULL.") + + Logger.warn( + "Re-packing your entire database may take a while and will consume extra disk space during the process." + ) Repo.query!( "vacuum full;", @@ -142,6 +150,7 @@ def run(["vacuum", args]) do case args do "analyze" -> Logger.info("Runnning VACUUM ANALYZE.") + Repo.query!( "vacuum analyze;", [], @@ -149,7 +158,11 @@ def run(["vacuum", args]) do ) "full" -> - Logger.info("Runnning VACUUM FULL. This could take a while.") + Logger.info("Runnning VACUUM FULL.") + + Logger.warn( + "Re-packing your entire database may take a while and will consume extra disk space during the process." + ) Repo.query!( "vacuum full;", From 30f96b19c1850d0dd534edbe66ce19a1c8198729 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 27 May 2020 16:40:51 -0500 Subject: [PATCH 137/375] Abstract out the database maintenance. I'd like to use this from AdminFE too. --- lib/mix/tasks/pleroma/database.ex | 52 +++---------------------------- lib/pleroma/maintenance.ex | 37 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 48 deletions(-) create mode 100644 lib/pleroma/maintenance.ex diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 2f1f33469..7049293d9 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -5,6 +5,7 @@ defmodule Mix.Tasks.Pleroma.Database do alias Pleroma.Conversation alias Pleroma.Object + alias Pleroma.Maintenance alias Pleroma.Repo alias Pleroma.User require Logger @@ -34,17 +35,7 @@ def run(["remove_embedded_objects" | args]) do ) if Keyword.get(options, :vacuum) do - Logger.info("Runnning VACUUM FULL.") - - Logger.warn( - "Re-packing your entire database may take a while and will consume extra disk space during the process." - ) - - Repo.query!( - "vacuum full;", - [], - timeout: :infinity - ) + Maintenance.vacuum("full") end end @@ -98,17 +89,7 @@ def run(["prune_objects" | args]) do |> Repo.delete_all(timeout: :infinity) if Keyword.get(options, :vacuum) do - Logger.info("Runnning VACUUM FULL.") - - Logger.warn( - "Re-packing your entire database may take a while and will consume extra disk space during the process." - ) - - Repo.query!( - "vacuum full;", - [], - timeout: :infinity - ) + Maintenance.vacuum("full") end end @@ -147,31 +128,6 @@ def run(["fix_likes_collections"]) do def run(["vacuum", args]) do start_pleroma() - case args do - "analyze" -> - Logger.info("Runnning VACUUM ANALYZE.") - - Repo.query!( - "vacuum analyze;", - [], - timeout: :infinity - ) - - "full" -> - Logger.info("Runnning VACUUM FULL.") - - Logger.warn( - "Re-packing your entire database may take a while and will consume extra disk space during the process." - ) - - Repo.query!( - "vacuum full;", - [], - timeout: :infinity - ) - - _ -> - Logger.error("Error: invalid vacuum argument.") - end + Maintenance.vacuum(args) end end diff --git a/lib/pleroma/maintenance.ex b/lib/pleroma/maintenance.ex new file mode 100644 index 000000000..326c17825 --- /dev/null +++ b/lib/pleroma/maintenance.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Maintenance do + alias Pleroma.Repo + require Logger + + def vacuum(args) do + case args do + "analyze" -> + Logger.info("Runnning VACUUM ANALYZE.") + + Repo.query!( + "vacuum analyze;", + [], + timeout: :infinity + ) + + "full" -> + Logger.info("Runnning VACUUM FULL.") + + Logger.warn( + "Re-packing your entire database may take a while and will consume extra disk space during the process." + ) + + Repo.query!( + "vacuum full;", + [], + timeout: :infinity + ) + + _ -> + Logger.error("Error: invalid vacuum argument.") + end + end +end From 92fba24c743a5d2d9ed78df7499fd3123a6ad6ac Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 27 May 2020 17:17:06 -0500 Subject: [PATCH 138/375] Alpha sort --- lib/mix/tasks/pleroma/database.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 7049293d9..82e2abdcb 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -4,8 +4,8 @@ defmodule Mix.Tasks.Pleroma.Database do alias Pleroma.Conversation - alias Pleroma.Object alias Pleroma.Maintenance + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User require Logger From 800e62405855af673328278ce08e9b1c5cb0602f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 28 May 2020 19:32:56 +0400 Subject: [PATCH 139/375] Update installation guides --- docs/installation/debian_based_en.md | 4 ++-- docs/installation/debian_based_jp.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 62d8733f7..2c20d521a 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -38,8 +38,8 @@ sudo apt install git build-essential postgresql postgresql-contrib * Download and add the Erlang repository: ```shell -wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb -sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb +wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb +sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb ``` * Install Elixir and Erlang: diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index a3c4621d8..1e5a9be91 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -40,8 +40,8 @@ sudo apt install git build-essential postgresql postgresql-contrib * Erlangのリポジトリをダウンロードおよびインストールします。 ``` -wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb -sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb +wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb +sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb ``` * ElixirとErlangをインストールします、 From ae05792d2a825dbb7d53a7f5a079548ae8310c63 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 28 May 2020 19:41:34 +0300 Subject: [PATCH 140/375] get-packs for local generated pack --- lib/mix/tasks/pleroma/emoji.ex | 38 ++++++++++--------- test/instance_static/local_pack/files.json | 3 ++ test/instance_static/local_pack/manifest.json | 10 +++++ test/tasks/emoji_test.exs | 13 +++++++ 4 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 test/instance_static/local_pack/files.json create mode 100644 test/instance_static/local_pack/manifest.json diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index cdffa88b2..29a5fa99c 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -15,7 +15,7 @@ def run(["ls-packs" | args]) do {options, [], []} = parse_global_opts(args) url_or_path = options[:manifest] || default_manifest() - manifest = fetch_manifest(url_or_path) + manifest = fetch_and_decode(url_or_path) Enum.each(manifest, fn {name, info} -> to_print = [ @@ -42,12 +42,12 @@ def run(["get-packs" | args]) do url_or_path = options[:manifest] || default_manifest() - manifest = fetch_manifest(url_or_path) + manifest = fetch_and_decode(url_or_path) for pack_name <- pack_names do if Map.has_key?(manifest, pack_name) do pack = manifest[pack_name] - src_url = pack["src"] + src = pack["src"] IO.puts( IO.ANSI.format([ @@ -57,11 +57,11 @@ def run(["get-packs" | args]) do :normal, " from ", :underline, - src_url + src ]) ) - binary_archive = Tesla.get!(client(), src_url).body + {:ok, binary_archive} = fetch(src) archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] @@ -74,8 +74,8 @@ def run(["get-packs" | args]) do raise "Bad SHA256 for #{pack_name}" end - # The url specified in files should be in the same directory - files_url = + # The location specified in files should be in the same directory + files_loc = url_or_path |> Path.dirname() |> Path.join(pack["files"]) @@ -88,11 +88,11 @@ def run(["get-packs" | args]) do :normal, " from ", :underline, - files_url + files_loc ]) ) - files = Tesla.get!(client(), files_url).body |> Jason.decode!() + files = fetch_and_decode(files_loc) IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -237,16 +237,20 @@ def run(["gen-pack" | args]) do end end - defp fetch_manifest(from) do - Jason.decode!( - if String.starts_with?(from, "http") do - Tesla.get!(client(), from).body - else - File.read!(from) - end - ) + defp fetch_and_decode(from) do + with {:ok, json} <- fetch(from) do + Jason.decode!(json) + end end + defp fetch("http" <> _ = from) do + with {:ok, %{body: body}} <- Tesla.get(client(), from) do + {:ok, body} + end + end + + defp fetch(path), do: File.read(path) + defp parse_global_opts(args) do OptionParser.parse( args, diff --git a/test/instance_static/local_pack/files.json b/test/instance_static/local_pack/files.json new file mode 100644 index 000000000..279770998 --- /dev/null +++ b/test/instance_static/local_pack/files.json @@ -0,0 +1,3 @@ +{ + "blank": "blank.png" +} \ No newline at end of file diff --git a/test/instance_static/local_pack/manifest.json b/test/instance_static/local_pack/manifest.json new file mode 100644 index 000000000..01067042f --- /dev/null +++ b/test/instance_static/local_pack/manifest.json @@ -0,0 +1,10 @@ +{ + "local": { + "src_sha256": "384025A1AC6314473863A11AC7AB38A12C01B851A3F82359B89B4D4211D3291D", + "src": "test/fixtures/emoji/packs/blank.png.zip", + "license": "Apache 2.0", + "homepage": "https://example.com", + "files": "files.json", + "description": "Some local pack" + } +} \ No newline at end of file diff --git a/test/tasks/emoji_test.exs b/test/tasks/emoji_test.exs index f5de3ef0e..499f098c2 100644 --- a/test/tasks/emoji_test.exs +++ b/test/tasks/emoji_test.exs @@ -73,6 +73,19 @@ test "download pack from default manifest" do on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end) end + test "install local emoji pack" do + assert capture_io(fn -> + Emoji.run([ + "get-packs", + "local", + "--manifest", + "test/instance_static/local_pack/manifest.json" + ]) + end) =~ "Writing pack.json for" + + on_exit(fn -> File.rm_rf!("test/instance_static/emoji/local") end) + end + test "pack not found" do mock(fn %{ From d4a18d44feb4ae67f6476b30fac96c0e6aa511dd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 28 May 2020 00:49:49 -0500 Subject: [PATCH 141/375] Update default instance description --- config/config.exs | 2 +- lib/pleroma/web/api_spec/operations/instance_operation.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index d15998715..3729526ea 100644 --- a/config/config.exs +++ b/config/config.exs @@ -183,7 +183,7 @@ name: "Pleroma", email: "example@example.com", notify_email: "noreply@example.com", - description: "A Pleroma instance, an alternative fediverse server", + description: "Pleroma: An efficient and flexible fediverse server", background_image: "/images/city.jpg", limit: 5_000, chat_limit: 5_000, diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index d5c335d0c..bf39ae643 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -137,7 +137,7 @@ defp instance do "background_upload_limit" => 4_000_000, "background_image" => "/static/image.png", "banner_upload_limit" => 4_000_000, - "description" => "A Pleroma instance, an alternative fediverse server", + "description" => "Pleroma: An efficient and flexible fediverse server", "email" => "lain@lain.com", "languages" => ["en"], "max_toot_chars" => 5000, From d1ee3527ef8062c34e222a1c7084c207b80fe4db Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 28 May 2020 22:23:15 +0400 Subject: [PATCH 142/375] Move config actions to AdminAPI.ConfigController --- .../controllers/admin_api_controller.ex | 127 -- .../controllers/config_controller.ex | 150 ++ lib/pleroma/web/router.ex | 6 +- .../controllers/admin_api_controller_test.exs | 1220 ---------------- .../controllers/config_controller_test.exs | 1244 +++++++++++++++++ 5 files changed, 1397 insertions(+), 1350 deletions(-) create mode 100644 lib/pleroma/web/admin_api/controllers/config_controller.ex create mode 100644 test/web/admin_api/controllers/config_controller_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 783203c07..52900026f 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Activity alias Pleroma.Config - alias Pleroma.ConfigDB alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug @@ -24,7 +23,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.ReportView @@ -38,7 +36,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do require Logger - @descriptions Pleroma.Docs.JSON.compile() @users_page_size 50 plug( @@ -105,11 +102,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do OAuthScopesPlug, %{scopes: ["read"], admin: true} when action in [ - :config_show, :list_log, :stats, :relay_list, - :config_descriptions, :need_reboot ] ) @@ -119,7 +114,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do %{scopes: ["write"], admin: true} when action in [ :restart, - :config_update, :resend_confirmation_email, :confirm_email, :oauth_app_create, @@ -821,105 +815,6 @@ def list_log(conn, params) do |> render("index.json", %{log: log}) end - def config_descriptions(conn, _params) do - descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) - - json(conn, descriptions) - end - - def config_show(conn, %{"only_db" => true}) do - with :ok <- configurable_from_database() do - configs = Pleroma.Repo.all(ConfigDB) - - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: configs}) - end - end - - def config_show(conn, _params) do - with :ok <- configurable_from_database() do - configs = ConfigDB.get_all_as_keyword() - - merged = - Config.Holder.default_config() - |> ConfigDB.merge(configs) - |> Enum.map(fn {group, values} -> - Enum.map(values, fn {key, value} -> - db = - if configs[group][key] do - ConfigDB.get_db_keys(configs[group][key], key) - end - - db_value = configs[group][key] - - merged_value = - if !is_nil(db_value) and Keyword.keyword?(db_value) and - ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do - ConfigDB.merge_group(group, key, value, db_value) - else - value - end - - setting = %{ - group: ConfigDB.convert(group), - key: ConfigDB.convert(key), - value: ConfigDB.convert(merged_value) - } - - if db, do: Map.put(setting, :db, db), else: setting - end) - end) - |> List.flatten() - - json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) - end - end - - def config_update(conn, %{"configs" => configs}) do - with :ok <- configurable_from_database() do - {_errors, results} = - configs - |> Enum.filter(&whitelisted_config?/1) - |> Enum.map(fn - %{"group" => group, "key" => key, "delete" => true} = params -> - ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) - - %{"group" => group, "key" => key, "value" => value} -> - ConfigDB.update_or_create(%{group: group, key: key, value: value}) - end) - |> Enum.split_with(fn result -> elem(result, 0) == :error end) - - {deleted, updated} = - results - |> Enum.map(fn {:ok, config} -> - Map.put(config, :db, ConfigDB.get_db_keys(config)) - end) - |> Enum.split_with(fn config -> - Ecto.get_meta(config, :state) == :deleted - end) - - Config.TransferTask.load_and_update_env(deleted, false) - - if !Restarter.Pleroma.need_reboot?() do - changed_reboot_settings? = - (updated ++ deleted) - |> Enum.any?(fn config -> - group = ConfigDB.from_string(config.group) - key = ConfigDB.from_string(config.key) - value = ConfigDB.from_binary(config.value) - Config.TransferTask.pleroma_need_restart?(group, key, value) - end) - - if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() - end - - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()}) - end - end - def restart(conn, _params) do with :ok <- configurable_from_database() do Restarter.Pleroma.restart(Config.get(:env), 50) @@ -940,28 +835,6 @@ defp configurable_from_database do end end - defp whitelisted_config?(group, key) do - if whitelisted_configs = Config.get(:database_config_whitelist) do - Enum.any?(whitelisted_configs, fn - {whitelisted_group} -> - group == inspect(whitelisted_group) - - {whitelisted_group, whitelisted_key} -> - group == inspect(whitelisted_group) && key == inspect(whitelisted_key) - end) - else - true - end - end - - defp whitelisted_config?(%{"group" => group, "key" => key}) do - whitelisted_config?(group, key) - end - - defp whitelisted_config?(%{:group => group} = config) do - whitelisted_config?(group, config[:key]) - end - def reload_emoji(conn, _params) do Pleroma.Emoji.reload() diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex new file mode 100644 index 000000000..742980976 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -0,0 +1,150 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigController do + use Pleroma.Web, :controller + + alias Pleroma.Config + alias Pleroma.ConfigDB + alias Pleroma.Plugs.OAuthScopesPlug + + @descriptions Pleroma.Docs.JSON.compile() + + plug( + OAuthScopesPlug, + %{scopes: ["read"], admin: true} + when action in [:show, :descriptions] + ) + + plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + def descriptions(conn, _params) do + descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) + + json(conn, descriptions) + end + + def show(conn, %{"only_db" => true}) do + with :ok <- configurable_from_database() do + configs = Pleroma.Repo.all(ConfigDB) + render(conn, "index.json", %{configs: configs}) + end + end + + def show(conn, _params) do + with :ok <- configurable_from_database() do + configs = ConfigDB.get_all_as_keyword() + + merged = + Config.Holder.default_config() + |> ConfigDB.merge(configs) + |> Enum.map(fn {group, values} -> + Enum.map(values, fn {key, value} -> + db = + if configs[group][key] do + ConfigDB.get_db_keys(configs[group][key], key) + end + + db_value = configs[group][key] + + merged_value = + if not is_nil(db_value) and Keyword.keyword?(db_value) and + ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do + ConfigDB.merge_group(group, key, value, db_value) + else + value + end + + setting = %{ + group: ConfigDB.convert(group), + key: ConfigDB.convert(key), + value: ConfigDB.convert(merged_value) + } + + if db, do: Map.put(setting, :db, db), else: setting + end) + end) + |> List.flatten() + + json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) + end + end + + def update(conn, %{"configs" => configs}) do + with :ok <- configurable_from_database() do + results = + configs + |> Enum.filter(&whitelisted_config?/1) + |> Enum.map(fn + %{"group" => group, "key" => key, "delete" => true} = params -> + ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) + + %{"group" => group, "key" => key, "value" => value} -> + ConfigDB.update_or_create(%{group: group, key: key, value: value}) + end) + |> Enum.reject(fn {result, _} -> result == :error end) + + {deleted, updated} = + results + |> Enum.map(fn {:ok, config} -> + Map.put(config, :db, ConfigDB.get_db_keys(config)) + end) + |> Enum.split_with(fn config -> + Ecto.get_meta(config, :state) == :deleted + end) + + Config.TransferTask.load_and_update_env(deleted, false) + + if not Restarter.Pleroma.need_reboot?() do + changed_reboot_settings? = + (updated ++ deleted) + |> Enum.any?(fn config -> + group = ConfigDB.from_string(config.group) + key = ConfigDB.from_string(config.key) + value = ConfigDB.from_binary(config.value) + Config.TransferTask.pleroma_need_restart?(group, key, value) + end) + + if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() + end + + render(conn, "index.json", %{ + configs: updated, + need_reboot: Restarter.Pleroma.need_reboot?() + }) + end + end + + defp configurable_from_database do + if Config.get(:configurable_from_database) do + :ok + else + {:error, "To use this endpoint you need to enable configuration from database."} + end + end + + defp whitelisted_config?(group, key) do + if whitelisted_configs = Config.get(:database_config_whitelist) do + Enum.any?(whitelisted_configs, fn + {whitelisted_group} -> + group == inspect(whitelisted_group) + + {whitelisted_group, whitelisted_key} -> + group == inspect(whitelisted_group) && key == inspect(whitelisted_key) + end) + else + true + end + end + + defp whitelisted_config?(%{"group" => group, "key" => key}) do + whitelisted_config?(group, key) + end + + defp whitelisted_config?(%{:group => group} = config) do + whitelisted_config?(group, config[:key]) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e493a4153..b683a4ff3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -194,9 +194,9 @@ defmodule Pleroma.Web.Router do delete("/statuses/:id", StatusController, :delete) get("/statuses", StatusController, :index) - get("/config", AdminAPIController, :config_show) - post("/config", AdminAPIController, :config_update) - get("/config/descriptions", AdminAPIController, :config_descriptions) + get("/config", ConfigController, :show) + post("/config", ConfigController, :update) + get("/config/descriptions", ConfigController, :descriptions) get("/need_reboot", AdminAPIController, :need_reboot) get("/restart", AdminAPIController, :restart) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index ead840186..bd44ffed3 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -12,7 +12,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.Activity alias Pleroma.Config - alias Pleroma.ConfigDB alias Pleroma.HTML alias Pleroma.MFA alias Pleroma.ModerationLog @@ -1704,1175 +1703,6 @@ test "returns 403 when requested by anonymous" do end end - describe "GET /api/pleroma/admin/config" do - setup do: clear_config(:configurable_from_database, true) - - test "when configuration from database is off", %{conn: conn} do - Config.put(:configurable_from_database, false) - conn = get(conn, "/api/pleroma/admin/config") - - assert json_response(conn, 400) == - %{ - "error" => "To use this endpoint you need to enable configuration from database." - } - end - - test "with settings only in db", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true}) - - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => key1, - "value" => _ - }, - %{ - "group" => ":pleroma", - "key" => key2, - "value" => _ - } - ] - } = json_response(conn, 200) - - assert key1 == config1.key - assert key2 == config2.key - end - - test "db is added to settings that are in db", %{conn: conn} do - _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name")) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - [instance_config] = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key == ":instance" - end) - - assert instance_config["db"] == [":name"] - end - - test "merged default setting with db settings", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - config3 = - insert(:config, - value: ConfigDB.to_binary(k1: :v1, k2: :v2) - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert length(configs) > 3 - - received_configs = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [config1.key, config2.key, config3.key] - end) - - assert length(received_configs) == 3 - - db_keys = - config3.value - |> ConfigDB.from_binary() - |> Keyword.keys() - |> ConfigDB.convert() - - Enum.each(received_configs, fn %{"value" => value, "db" => db} -> - assert db in [[config1.key], [config2.key], db_keys] - - assert value in [ - ConfigDB.from_binary_with_convert(config1.value), - ConfigDB.from_binary_with_convert(config2.value), - ConfigDB.from_binary_with_convert(config3.value) - ] - end) - end - - test "subkeys with full update right merge", %{conn: conn} do - config1 = - insert(:config, - key: ":emoji", - value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]) - ) - - config2 = - insert(:config, - key: ":assets", - value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1]) - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - vals = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [config1.key, config2.key] - end) - - emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) - assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) - - emoji_val = ConfigDB.transform_with_out_binary(emoji["value"]) - assets_val = ConfigDB.transform_with_out_binary(assets["value"]) - - assert emoji_val[:groups] == [a: 1, b: 2] - assert assets_val[:mascots] == [a: 1, b: 2] - end - end - - test "POST /api/pleroma/admin/config error", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []}) - - assert json_response(conn, 400) == - %{"error" => "To use this endpoint you need to enable configuration from database."} - end - - describe "POST /api/pleroma/admin/config" do - setup do - http = Application.get_env(:pleroma, :http) - - on_exit(fn -> - Application.delete_env(:pleroma, :key1) - Application.delete_env(:pleroma, :key2) - Application.delete_env(:pleroma, :key3) - Application.delete_env(:pleroma, :key4) - Application.delete_env(:pleroma, :keyaa1) - Application.delete_env(:pleroma, :keyaa2) - Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) - Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) - Application.put_env(:pleroma, :http, http) - Application.put_env(:tesla, :adapter, Tesla.Mock) - Restarter.Pleroma.refresh() - end) - end - - setup do: clear_config(:configurable_from_database, true) - - @tag capture_log: true - test "create new config setting in db", %{conn: conn} do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{ - group: ":ueberauth", - key: "Ueberauth", - value: [%{"tuple" => [":consumer_secret", "aaaa"]}] - }, - %{ - group: ":pleroma", - key: ":key2", - value: %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - } - }, - %{ - group: ":pleroma", - key: ":key3", - value: [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - }, - %{ - group: ":pleroma", - key: ":key4", - value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} - }, - %{ - group: ":idna", - key: ":key5", - value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => "value1", - "db" => [":key1"] - }, - %{ - "group" => ":ueberauth", - "key" => "Ueberauth", - "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], - "db" => [":consumer_secret"] - }, - %{ - "group" => ":pleroma", - "key" => ":key2", - "value" => %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - }, - "db" => [":key2"] - }, - %{ - "group" => ":pleroma", - "key" => ":key3", - "value" => [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ], - "db" => [":key3"] - }, - %{ - "group" => ":pleroma", - "key" => ":key4", - "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, - "db" => [":key4"] - }, - %{ - "group" => ":idna", - "key" => ":key5", - "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, - "db" => [":key5"] - } - ] - } - - assert Application.get_env(:pleroma, :key1) == "value1" - - assert Application.get_env(:pleroma, :key2) == %{ - nested_1: "nested_value1", - nested_2: [ - %{nested_22: "nested_value222"}, - %{nested_33: %{nested_44: "nested_444"}} - ] - } - - assert Application.get_env(:pleroma, :key3) == [ - %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - - assert Application.get_env(:pleroma, :key4) == %{ - "endpoint" => "https://example.com", - nested_5: :upload - } - - assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} - end - - test "save configs setting without explicit key", %{conn: conn} do - level = Application.get_env(:quack, :level) - meta = Application.get_env(:quack, :meta) - webhook_url = Application.get_env(:quack, :webhook_url) - - on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) - Application.put_env(:quack, :webhook_url, webhook_url) - end) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":quack", - key: ":level", - value: ":info" - }, - %{ - group: ":quack", - key: ":meta", - value: [":none"] - }, - %{ - group: ":quack", - key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY" - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":quack", - "key" => ":level", - "value" => ":info", - "db" => [":level"] - }, - %{ - "group" => ":quack", - "key" => ":meta", - "value" => [":none"], - "db" => [":meta"] - }, - %{ - "group" => ":quack", - "key" => ":webhook_url", - "value" => "https://hooks.slack.com/services/KEY", - "db" => [":webhook_url"] - } - ] - } - - assert Application.get_env(:quack, :level) == :info - assert Application.get_env(:quack, :meta) == [:none] - assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" - end - - test "saving config with partial update", %{conn: conn} do - config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]} - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key2", 2]}, - %{"tuple" => [":key3", 3]} - ], - "db" => [":key1", ":key2", ":key3"] - } - ] - } - end - - test "saving config which need pleroma reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert post( - conn, - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert configs["need_reboot"] - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert configs["need_reboot"] == false - end - - test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert post( - conn, - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - assert post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} - ] - }) - |> json_response(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key3", 3]} - ], - "db" => [":key3"] - } - ], - "need_reboot" => true - } - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert configs["need_reboot"] == false - end - - test "saving config with nested merge", %{conn: conn} do - config = - insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: config.group, - key: config.key, - value: [ - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k1", 1]}, - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ], - "db" => [":key1", ":key3", ":key2"] - } - ] - } - end - - test "saving special atoms", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ], - "db" => [":ssl_options"] - } - ] - } - - assert Application.get_env(:pleroma, :key1) == [ - ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] - ] - end - - test "saving full setting if value is in full_key_update list", %{conn: conn} do - backends = Application.get_env(:logger, :backends) - on_exit(fn -> Application.put_env(:logger, :backends, backends) end) - - config = - insert(:config, - group: ":logger", - key: ":backends", - value: :erlang.term_to_binary([]) - ) - - Pleroma.Config.TransferTask.load_and_update_env([], false) - - assert Application.get_env(:logger, :backends) == [] - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: config.group, - key: config.key, - value: [":console"] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":logger", - "key" => ":backends", - "value" => [ - ":console" - ], - "db" => [":backends"] - } - ] - } - - assert Application.get_env(:logger, :backends) == [ - :console - ] - end - - test "saving full setting if value is not keyword", %{conn: conn} do - config = - insert(:config, - group: ":tesla", - key: ":adapter", - value: :erlang.term_to_binary(Tesla.Adapter.Hackey) - ) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"} - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":tesla", - "key" => ":adapter", - "value" => "Tesla.Adapter.Httpc", - "db" => [":adapter"] - } - ] - } - end - - test "update config setting & delete with fallback to default value", %{ - conn: conn, - admin: admin, - token: token - } do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - config1 = insert(:config, key: ":keyaa1") - config2 = insert(:config, key: ":keyaa2") - - config3 = - insert(:config, - group: ":ueberauth", - key: "Ueberauth" - ) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: config1.group, key: config1.key, value: "another_value"}, - %{group: config2.group, key: config2.key, value: "another_value"} - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => config1.key, - "value" => "another_value", - "db" => [":keyaa1"] - }, - %{ - "group" => ":pleroma", - "key" => config2.key, - "value" => "another_value", - "db" => [":keyaa2"] - } - ] - } - - assert Application.get_env(:pleroma, :keyaa1) == "another_value" - assert Application.get_env(:pleroma, :keyaa2) == "another_value" - assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: config2.group, key: config2.key, delete: true}, - %{ - group: ":ueberauth", - key: "Ueberauth", - delete: true - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [] - } - - assert Application.get_env(:ueberauth, Ueberauth) == ueberauth - refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) - end - - test "common config example", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ] - } - ] - }) - - assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ], - "db" => [ - ":enabled", - ":method", - ":seconds_valid", - ":path", - ":key1", - ":partial_chain", - ":regex1", - ":regex2", - ":regex3", - ":regex4", - ":name" - ] - } - ] - } - end - - test "tuples with more than two values", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ], - "db" => [":http"] - } - ] - } - end - - test "settings with nesting map", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000, - "nested" => %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000 - } - } - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0, - "nested" => %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0 - } - } - ] - } - ], - "db" => [":key2", ":key3"] - } - ] - } - end - - test "value as map", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"} - } - ] - }) - - assert json_response(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"}, - "db" => [":key1"] - } - ] - } - end - - test "queues key as atom", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ], - "db" => [ - ":federator_incoming", - ":federator_outgoing", - ":web_push", - ":mailer", - ":transmogrifier", - ":scheduled_activities", - ":background" - ] - } - ] - } - end - - test "delete part of settings by atom subkeys", %{conn: conn} do - config = - insert(:config, - key: ":keyaa1", - value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") - ) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: config.group, - key: config.key, - subkeys: [":subkey1", ":subkey3"], - delete: true - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":keyaa1", - "value" => [%{"tuple" => [":subkey2", "val2"]}], - "db" => [":subkey2"] - } - ] - } - end - - test "proxy tuple localhost", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple domain", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple ip", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value - assert ":proxy_url" in db - end - - @tag capture_log: true - test "doesn't set keys not in the whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :key1}, - {:pleroma, :key2}, - {:pleroma, Pleroma.Captcha.NotReal}, - {:not_real} - ]) - - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{group: ":pleroma", key: ":key2", value: "value2"}, - %{group: ":pleroma", key: ":key3", value: "value3"}, - %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, - %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, - %{group: ":not_real", key: ":anything", value: "value6"} - ] - }) - - assert Application.get_env(:pleroma, :key1) == "value1" - assert Application.get_env(:pleroma, :key2) == "value2" - assert Application.get_env(:pleroma, :key3) == nil - assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil - assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" - assert Application.get_env(:not_real, :anything) == "value6" - end - end - describe "GET /api/pleroma/admin/restart" do setup do: clear_config(:configurable_from_database, true) @@ -3481,56 +2311,6 @@ test "it deletes the note", %{conn: conn, report_id: report_id} do end end - describe "GET /api/pleroma/admin/config/descriptions" do - test "structure", %{conn: conn} do - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - assert [child | _others] = json_response(conn, 200) - - assert child["children"] - assert child["key"] - assert String.starts_with?(child["group"], ":") - assert child["description"] - end - - test "filters by database configuration whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :instance}, - {:pleroma, :activitypub}, - {:pleroma, Pleroma.Upload}, - {:esshd} - ]) - - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - children = json_response(conn, 200) - - assert length(children) == 4 - - assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 - - instance = Enum.find(children, fn c -> c["key"] == ":instance" end) - assert instance["children"] - - activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) - assert activitypub["children"] - - web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) - assert web_endpoint["children"] - - esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) - assert esshd["children"] - end - end - describe "/api/pleroma/admin/stats" do test "status visibility count", %{conn: conn} do admin = insert(:user, is_admin: true) diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs new file mode 100644 index 000000000..9bc6fd91c --- /dev/null +++ b/test/web/admin_api/controllers/config_controller_test.exs @@ -0,0 +1,1244 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do + use Pleroma.Web.ConnCase, async: true + + import ExUnit.CaptureLog + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ConfigDB + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/config" do + setup do: clear_config(:configurable_from_database, true) + + test "when configuration from database is off", %{conn: conn} do + Config.put(:configurable_from_database, false) + conn = get(conn, "/api/pleroma/admin/config") + + assert json_response(conn, 400) == + %{ + "error" => "To use this endpoint you need to enable configuration from database." + } + end + + test "with settings only in db", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true}) + + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => key1, + "value" => _ + }, + %{ + "group" => ":pleroma", + "key" => key2, + "value" => _ + } + ] + } = json_response(conn, 200) + + assert key1 == config1.key + assert key2 == config2.key + end + + test "db is added to settings that are in db", %{conn: conn} do + _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name")) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response(200) + + [instance_config] = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key == ":instance" + end) + + assert instance_config["db"] == [":name"] + end + + test "merged default setting with db settings", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + config3 = + insert(:config, + value: ConfigDB.to_binary(k1: :v1, k2: :v2) + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response(200) + + assert length(configs) > 3 + + received_configs = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in [config1.key, config2.key, config3.key] + end) + + assert length(received_configs) == 3 + + db_keys = + config3.value + |> ConfigDB.from_binary() + |> Keyword.keys() + |> ConfigDB.convert() + + Enum.each(received_configs, fn %{"value" => value, "db" => db} -> + assert db in [[config1.key], [config2.key], db_keys] + + assert value in [ + ConfigDB.from_binary_with_convert(config1.value), + ConfigDB.from_binary_with_convert(config2.value), + ConfigDB.from_binary_with_convert(config3.value) + ] + end) + end + + test "subkeys with full update right merge", %{conn: conn} do + config1 = + insert(:config, + key: ":emoji", + value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]) + ) + + config2 = + insert(:config, + key: ":assets", + value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1]) + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response(200) + + vals = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in [config1.key, config2.key] + end) + + emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) + assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) + + emoji_val = ConfigDB.transform_with_out_binary(emoji["value"]) + assets_val = ConfigDB.transform_with_out_binary(assets["value"]) + + assert emoji_val[:groups] == [a: 1, b: 2] + assert assets_val[:mascots] == [a: 1, b: 2] + end + end + + test "POST /api/pleroma/admin/config error", %{conn: conn} do + conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []}) + + assert json_response(conn, 400) == + %{"error" => "To use this endpoint you need to enable configuration from database."} + end + + describe "POST /api/pleroma/admin/config" do + setup do + http = Application.get_env(:pleroma, :http) + + on_exit(fn -> + Application.delete_env(:pleroma, :key1) + Application.delete_env(:pleroma, :key2) + Application.delete_env(:pleroma, :key3) + Application.delete_env(:pleroma, :key4) + Application.delete_env(:pleroma, :keyaa1) + Application.delete_env(:pleroma, :keyaa2) + Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) + Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) + Application.put_env(:pleroma, :http, http) + Application.put_env(:tesla, :adapter, Tesla.Mock) + Restarter.Pleroma.refresh() + end) + end + + setup do: clear_config(:configurable_from_database, true) + + @tag capture_log: true + test "create new config setting in db", %{conn: conn} do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{ + group: ":ueberauth", + key: "Ueberauth", + value: [%{"tuple" => [":consumer_secret", "aaaa"]}] + }, + %{ + group: ":pleroma", + key: ":key2", + value: %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + } + }, + %{ + group: ":pleroma", + key: ":key3", + value: [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + }, + %{ + group: ":pleroma", + key: ":key4", + value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} + }, + %{ + group: ":idna", + key: ":key5", + value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => "value1", + "db" => [":key1"] + }, + %{ + "group" => ":ueberauth", + "key" => "Ueberauth", + "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], + "db" => [":consumer_secret"] + }, + %{ + "group" => ":pleroma", + "key" => ":key2", + "value" => %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + }, + "db" => [":key2"] + }, + %{ + "group" => ":pleroma", + "key" => ":key3", + "value" => [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ], + "db" => [":key3"] + }, + %{ + "group" => ":pleroma", + "key" => ":key4", + "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, + "db" => [":key4"] + }, + %{ + "group" => ":idna", + "key" => ":key5", + "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, + "db" => [":key5"] + } + ] + } + + assert Application.get_env(:pleroma, :key1) == "value1" + + assert Application.get_env(:pleroma, :key2) == %{ + nested_1: "nested_value1", + nested_2: [ + %{nested_22: "nested_value222"}, + %{nested_33: %{nested_44: "nested_444"}} + ] + } + + assert Application.get_env(:pleroma, :key3) == [ + %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + + assert Application.get_env(:pleroma, :key4) == %{ + "endpoint" => "https://example.com", + nested_5: :upload + } + + assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} + end + + test "save configs setting without explicit key", %{conn: conn} do + level = Application.get_env(:quack, :level) + meta = Application.get_env(:quack, :meta) + webhook_url = Application.get_env(:quack, :webhook_url) + + on_exit(fn -> + Application.put_env(:quack, :level, level) + Application.put_env(:quack, :meta, meta) + Application.put_env(:quack, :webhook_url, webhook_url) + end) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":quack", + key: ":level", + value: ":info" + }, + %{ + group: ":quack", + key: ":meta", + value: [":none"] + }, + %{ + group: ":quack", + key: ":webhook_url", + value: "https://hooks.slack.com/services/KEY" + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":quack", + "key" => ":level", + "value" => ":info", + "db" => [":level"] + }, + %{ + "group" => ":quack", + "key" => ":meta", + "value" => [":none"], + "db" => [":meta"] + }, + %{ + "group" => ":quack", + "key" => ":webhook_url", + "value" => "https://hooks.slack.com/services/KEY", + "db" => [":webhook_url"] + } + ] + } + + assert Application.get_env(:quack, :level) == :info + assert Application.get_env(:quack, :meta) == [:none] + assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" + end + + test "saving config with partial update", %{conn: conn} do + config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]} + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key2", 2]}, + %{"tuple" => [":key3", 3]} + ], + "db" => [":key1", ":key2", ":key3"] + } + ] + } + end + + test "saving config which need pleroma reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert post( + conn, + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response(200) + + assert configs["need_reboot"] + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response(200) + + assert configs["need_reboot"] == false + end + + test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert post( + conn, + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + assert post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} + ] + }) + |> json_response(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key3", 3]} + ], + "db" => [":key3"] + } + ], + "need_reboot" => true + } + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response(200) + + assert configs["need_reboot"] == false + end + + test "saving config with nested merge", %{conn: conn} do + config = + insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + value: [ + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ] + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k1", 1]}, + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ], + "db" => [":key1", ":key3", ":key2"] + } + ] + } + end + + test "saving special atoms", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ] + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ], + "db" => [":ssl_options"] + } + ] + } + + assert Application.get_env(:pleroma, :key1) == [ + ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] + ] + end + + test "saving full setting if value is in full_key_update list", %{conn: conn} do + backends = Application.get_env(:logger, :backends) + on_exit(fn -> Application.put_env(:logger, :backends, backends) end) + + config = + insert(:config, + group: ":logger", + key: ":backends", + value: :erlang.term_to_binary([]) + ) + + Pleroma.Config.TransferTask.load_and_update_env([], false) + + assert Application.get_env(:logger, :backends) == [] + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + value: [":console"] + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":logger", + "key" => ":backends", + "value" => [ + ":console" + ], + "db" => [":backends"] + } + ] + } + + assert Application.get_env(:logger, :backends) == [ + :console + ] + end + + test "saving full setting if value is not keyword", %{conn: conn} do + config = + insert(:config, + group: ":tesla", + key: ":adapter", + value: :erlang.term_to_binary(Tesla.Adapter.Hackey) + ) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"} + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":tesla", + "key" => ":adapter", + "value" => "Tesla.Adapter.Httpc", + "db" => [":adapter"] + } + ] + } + end + + test "update config setting & delete with fallback to default value", %{ + conn: conn, + admin: admin, + token: token + } do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + config1 = insert(:config, key: ":keyaa1") + config2 = insert(:config, key: ":keyaa2") + + config3 = + insert(:config, + group: ":ueberauth", + key: "Ueberauth" + ) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{group: config1.group, key: config1.key, value: "another_value"}, + %{group: config2.group, key: config2.key, value: "another_value"} + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => config1.key, + "value" => "another_value", + "db" => [":keyaa1"] + }, + %{ + "group" => ":pleroma", + "key" => config2.key, + "value" => "another_value", + "db" => [":keyaa2"] + } + ] + } + + assert Application.get_env(:pleroma, :keyaa1) == "another_value" + assert Application.get_env(:pleroma, :keyaa2) == "another_value" + assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: config2.group, key: config2.key, delete: true}, + %{ + group: ":ueberauth", + key: "Ueberauth", + delete: true + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [] + } + + assert Application.get_env(:ueberauth, Ueberauth) == ueberauth + refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) + end + + test "common config example", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ] + } + ] + }) + + assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ], + "db" => [ + ":enabled", + ":method", + ":seconds_valid", + ":path", + ":key1", + ":partial_chain", + ":regex1", + ":regex2", + ":regex3", + ":regex4", + ":name" + ] + } + ] + } + end + + test "tuples with more than two values", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ] + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ], + "db" => [":http"] + } + ] + } + end + + test "settings with nesting map", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000, + "nested" => %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000 + } + } + ] + } + ] + } + ] + }) + + assert json_response(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0, + "nested" => %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0 + } + } + ] + } + ], + "db" => [":key2", ":key3"] + } + ] + } + end + + test "value as map", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"} + } + ] + }) + + assert json_response(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"}, + "db" => [":key1"] + } + ] + } + end + + test "queues key as atom", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ] + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ], + "db" => [ + ":federator_incoming", + ":federator_outgoing", + ":web_push", + ":mailer", + ":transmogrifier", + ":scheduled_activities", + ":background" + ] + } + ] + } + end + + test "delete part of settings by atom subkeys", %{conn: conn} do + config = + insert(:config, + key: ":keyaa1", + value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") + ) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + subkeys: [":subkey1", ":subkey3"], + delete: true + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":keyaa1", + "value" => [%{"tuple" => [":subkey2", "val2"]}], + "db" => [":subkey2"] + } + ] + } + end + + test "proxy tuple localhost", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple domain", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple ip", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value + assert ":proxy_url" in db + end + + @tag capture_log: true + test "doesn't set keys not in the whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :key1}, + {:pleroma, :key2}, + {:pleroma, Pleroma.Captcha.NotReal}, + {:not_real} + ]) + + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{group: ":pleroma", key: ":key2", value: "value2"}, + %{group: ":pleroma", key: ":key3", value: "value3"}, + %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, + %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, + %{group: ":not_real", key: ":anything", value: "value6"} + ] + }) + + assert Application.get_env(:pleroma, :key1) == "value1" + assert Application.get_env(:pleroma, :key2) == "value2" + assert Application.get_env(:pleroma, :key3) == nil + assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil + assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" + assert Application.get_env(:not_real, :anything) == "value6" + end + end + + describe "GET /api/pleroma/admin/config/descriptions" do + test "structure", %{conn: conn} do + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + assert [child | _others] = json_response(conn, 200) + + assert child["children"] + assert child["key"] + assert String.starts_with?(child["group"], ":") + assert child["description"] + end + + test "filters by database configuration whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :instance}, + {:pleroma, :activitypub}, + {:pleroma, Pleroma.Upload}, + {:esshd} + ]) + + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + children = json_response(conn, 200) + + assert length(children) == 4 + + assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 + + instance = Enum.find(children, fn c -> c["key"] == ":instance" end) + assert instance["children"] + + activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) + assert activitypub["children"] + + web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) + assert web_endpoint["children"] + + esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) + assert esshd["children"] + end + end +end From 06f20e918129b1f434783b64d59b5ae6b4b4ed51 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 28 May 2020 23:11:12 +0400 Subject: [PATCH 143/375] Add OpenApi spec to AdminAPI.ConfigController --- .../controllers/config_controller.ex | 21 ++- .../operations/admin/config_operation.ex | 142 +++++++++++++++ .../controllers/config_controller_test.exs | 164 +++++++++++------- 3 files changed, 259 insertions(+), 68 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/admin/config_operation.ex diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index 742980976..e221d9418 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -11,23 +11,26 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do @descriptions Pleroma.Docs.JSON.compile() + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) + plug( OAuthScopesPlug, %{scopes: ["read"], admin: true} when action in [:show, :descriptions] ) - plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) - action_fallback(Pleroma.Web.AdminAPI.FallbackController) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation + def descriptions(conn, _params) do descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) json(conn, descriptions) end - def show(conn, %{"only_db" => true}) do + def show(conn, %{only_db: true}) do with :ok <- configurable_from_database() do configs = Pleroma.Repo.all(ConfigDB) render(conn, "index.json", %{configs: configs}) @@ -73,16 +76,16 @@ def show(conn, _params) do end end - def update(conn, %{"configs" => configs}) do + def update(%{body_params: %{configs: configs}} = conn, _) do with :ok <- configurable_from_database() do results = configs |> Enum.filter(&whitelisted_config?/1) |> Enum.map(fn - %{"group" => group, "key" => key, "delete" => true} = params -> - ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) + %{group: group, key: key, delete: true} = params -> + ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]}) - %{"group" => group, "key" => key, "value" => value} -> + %{group: group, key: key, value: value} -> ConfigDB.update_or_create(%{group: group, key: key, value: value}) end) |> Enum.reject(fn {result, _} -> result == :error end) @@ -140,11 +143,11 @@ defp whitelisted_config?(group, key) do end end - defp whitelisted_config?(%{"group" => group, "key" => key}) do + defp whitelisted_config?(%{group: group, key: key}) do whitelisted_config?(group, key) end - defp whitelisted_config?(%{:group => group} = config) do + defp whitelisted_config?(%{group: group} = config) do whitelisted_config?(group, config[:key]) end end diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex new file mode 100644 index 000000000..7b38a2ef4 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex @@ -0,0 +1,142 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Get list of merged default settings with saved in database", + operationId: "AdminAPI.ConfigController.show", + parameters: [ + Operation.parameter( + :only_db, + :query, + %Schema{type: :boolean, default: false}, + "Get only saved in database settings" + ) + ], + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => Operation.response("Config", "application/json", config_response()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Update config settings", + operationId: "AdminAPI.ConfigController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + configs: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + value: any(), + delete: %Schema{type: :boolean}, + subkeys: %Schema{type: :array, items: %Schema{type: :string}} + } + } + } + } + }), + responses: %{ + 200 => Operation.response("Config", "application/json", config_response()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def descriptions_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Get JSON with config descriptions.", + operationId: "AdminAPI.ConfigController.descriptions", + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Config Descriptions", "application/json", %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, + description: %Schema{type: :string}, + children: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + key: %Schema{type: :string}, + type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, + description: %Schema{type: :string}, + suggestions: %Schema{type: :array} + } + } + } + } + } + }), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + defp any do + %Schema{ + oneOf: [ + %Schema{type: :array}, + %Schema{type: :object}, + %Schema{type: :string}, + %Schema{type: :integer}, + %Schema{type: :boolean} + ] + } + end + + defp config_response do + %Schema{ + type: :object, + properties: %{ + configs: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + value: any() + } + } + }, + need_reboot: %Schema{ + type: :boolean, + description: + "If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect" + } + } + } + end +end diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs index 9bc6fd91c..780de8d18 100644 --- a/test/web/admin_api/controllers/config_controller_test.exs +++ b/test/web/admin_api/controllers/config_controller_test.exs @@ -30,7 +30,7 @@ test "when configuration from database is off", %{conn: conn} do Config.put(:configurable_from_database, false) conn = get(conn, "/api/pleroma/admin/config") - assert json_response(conn, 400) == + assert json_response_and_validate_schema(conn, 400) == %{ "error" => "To use this endpoint you need to enable configuration from database." } @@ -40,7 +40,7 @@ test "with settings only in db", %{conn: conn} do config1 = insert(:config) config2 = insert(:config) - conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true}) + conn = get(conn, "/api/pleroma/admin/config?only_db=true") %{ "configs" => [ @@ -55,7 +55,7 @@ test "with settings only in db", %{conn: conn} do "value" => _ } ] - } = json_response(conn, 200) + } = json_response_and_validate_schema(conn, 200) assert key1 == config1.key assert key2 == config2.key @@ -67,7 +67,7 @@ test "db is added to settings that are in db", %{conn: conn} do %{"configs" => configs} = conn |> get("/api/pleroma/admin/config") - |> json_response(200) + |> json_response_and_validate_schema(200) [instance_config] = Enum.filter(configs, fn %{"group" => group, "key" => key} -> @@ -89,7 +89,7 @@ test "merged default setting with db settings", %{conn: conn} do %{"configs" => configs} = conn |> get("/api/pleroma/admin/config") - |> json_response(200) + |> json_response_and_validate_schema(200) assert length(configs) > 3 @@ -133,7 +133,7 @@ test "subkeys with full update right merge", %{conn: conn} do %{"configs" => configs} = conn |> get("/api/pleroma/admin/config") - |> json_response(200) + |> json_response_and_validate_schema(200) vals = Enum.filter(configs, fn %{"group" => group, "key" => key} -> @@ -152,9 +152,12 @@ test "subkeys with full update right merge", %{conn: conn} do end test "POST /api/pleroma/admin/config error", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => []}) - assert json_response(conn, 400) == + assert json_response_and_validate_schema(conn, 400) == %{"error" => "To use this endpoint you need to enable configuration from database."} end @@ -185,7 +188,9 @@ test "create new config setting in db", %{conn: conn} do on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{group: ":pleroma", key: ":key1", value: "value1"}, %{ @@ -225,7 +230,7 @@ test "create new config setting in db", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -310,7 +315,9 @@ test "save configs setting without explicit key", %{conn: conn} do end) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: ":quack", @@ -330,7 +337,7 @@ test "save configs setting without explicit key", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":quack", @@ -362,13 +369,15 @@ test "saving config with partial update", %{conn: conn} do config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]} ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -388,8 +397,9 @@ test "saving config which need pleroma reboot", %{conn: conn} do chat = Config.get(:chat) on_exit(fn -> Config.put(:chat, chat) end) - assert post( - conn, + assert conn + |> put_req_header("content-type", "application/json") + |> post( "/api/pleroma/admin/config", %{ configs: [ @@ -397,7 +407,7 @@ test "saving config which need pleroma reboot", %{conn: conn} do ] } ) - |> json_response(200) == %{ + |> json_response_and_validate_schema(200) == %{ "configs" => [ %{ "db" => [":enabled"], @@ -412,18 +422,19 @@ test "saving config which need pleroma reboot", %{conn: conn} do configs = conn |> get("/api/pleroma/admin/config") - |> json_response(200) + |> json_response_and_validate_schema(200) assert configs["need_reboot"] capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} end) =~ "pleroma restarted" configs = conn |> get("/api/pleroma/admin/config") - |> json_response(200) + |> json_response_and_validate_schema(200) assert configs["need_reboot"] == false end @@ -432,8 +443,9 @@ test "update setting which need reboot, don't change reboot flag until reboot", chat = Config.get(:chat) on_exit(fn -> Config.put(:chat, chat) end) - assert post( - conn, + assert conn + |> put_req_header("content-type", "application/json") + |> post( "/api/pleroma/admin/config", %{ configs: [ @@ -441,7 +453,7 @@ test "update setting which need reboot, don't change reboot flag until reboot", ] } ) - |> json_response(200) == %{ + |> json_response_and_validate_schema(200) == %{ "configs" => [ %{ "db" => [":enabled"], @@ -453,12 +465,14 @@ test "update setting which need reboot, don't change reboot flag until reboot", "need_reboot" => true } - assert post(conn, "/api/pleroma/admin/config", %{ + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} ] }) - |> json_response(200) == %{ + |> json_response_and_validate_schema(200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -473,13 +487,14 @@ test "update setting which need reboot, don't change reboot flag until reboot", } capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} end) =~ "pleroma restarted" configs = conn |> get("/api/pleroma/admin/config") - |> json_response(200) + |> json_response_and_validate_schema(200) assert configs["need_reboot"] == false end @@ -489,7 +504,9 @@ test "saving config with nested merge", %{conn: conn} do insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: config.group, @@ -510,7 +527,7 @@ test "saving config with nested merge", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -537,7 +554,9 @@ test "saving config with nested merge", %{conn: conn} do test "saving special atoms", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ "configs" => [ %{ "group" => ":pleroma", @@ -554,7 +573,7 @@ test "saving special atoms", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -593,7 +612,9 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do assert Application.get_env(:logger, :backends) == [] conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: config.group, @@ -603,7 +624,7 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":logger", @@ -630,13 +651,15 @@ test "saving full setting if value is not keyword", %{conn: conn} do ) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"} ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":tesla", @@ -664,14 +687,16 @@ test "update config setting & delete with fallback to default value", %{ ) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{group: config1.group, key: config1.key, value: "another_value"}, %{group: config2.group, key: config2.key, value: "another_value"} ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -696,6 +721,7 @@ test "update config setting & delete with fallback to default value", %{ build_conn() |> assign(:user, admin) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/config", %{ configs: [ %{group: config2.group, key: config2.key, delete: true}, @@ -707,7 +733,7 @@ test "update config setting & delete with fallback to default value", %{ ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [] } @@ -717,7 +743,9 @@ test "update config setting & delete with fallback to default value", %{ test "common config example", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ "group" => ":pleroma", @@ -741,7 +769,7 @@ test "common config example", %{conn: conn} do assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -779,7 +807,9 @@ test "common config example", %{conn: conn} do test "tuples with more than two values", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ "group" => ":pleroma", @@ -843,7 +873,7 @@ test "tuples with more than two values", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -911,7 +941,9 @@ test "tuples with more than two values", %{conn: conn} do test "settings with nesting map", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ "group" => ":pleroma", @@ -940,7 +972,7 @@ test "settings with nesting map", %{conn: conn} do ] }) - assert json_response(conn, 200) == + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ @@ -974,7 +1006,9 @@ test "settings with nesting map", %{conn: conn} do test "value as map", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ "group" => ":pleroma", @@ -984,7 +1018,7 @@ test "value as map", %{conn: conn} do ] }) - assert json_response(conn, 200) == + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ @@ -999,7 +1033,9 @@ test "value as map", %{conn: conn} do test "queues key as atom", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ "group" => ":oban", @@ -1017,7 +1053,7 @@ test "queues key as atom", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":oban", @@ -1053,7 +1089,9 @@ test "delete part of settings by atom subkeys", %{conn: conn} do ) conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: config.group, @@ -1064,7 +1102,7 @@ test "delete part of settings by atom subkeys", %{conn: conn} do ] }) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ "group" => ":pleroma", @@ -1078,7 +1116,9 @@ test "delete part of settings by atom subkeys", %{conn: conn} do test "proxy tuple localhost", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: ":pleroma", @@ -1099,7 +1139,7 @@ test "proxy tuple localhost", %{conn: conn} do "db" => db } ] - } = json_response(conn, 200) + } = json_response_and_validate_schema(conn, 200) assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value assert ":proxy_url" in db @@ -1107,7 +1147,9 @@ test "proxy tuple localhost", %{conn: conn} do test "proxy tuple domain", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: ":pleroma", @@ -1128,7 +1170,7 @@ test "proxy tuple domain", %{conn: conn} do "db" => db } ] - } = json_response(conn, 200) + } = json_response_and_validate_schema(conn, 200) assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value assert ":proxy_url" in db @@ -1136,7 +1178,9 @@ test "proxy tuple domain", %{conn: conn} do test "proxy tuple ip", %{conn: conn} do conn = - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{ group: ":pleroma", @@ -1157,7 +1201,7 @@ test "proxy tuple ip", %{conn: conn} do "db" => db } ] - } = json_response(conn, 200) + } = json_response_and_validate_schema(conn, 200) assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value assert ":proxy_url" in db @@ -1172,7 +1216,9 @@ test "doesn't set keys not in the whitelist", %{conn: conn} do {:not_real} ]) - post(conn, "/api/pleroma/admin/config", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ configs: [ %{group: ":pleroma", key: ":key1", value: "value1"}, %{group: ":pleroma", key: ":key2", value: "value2"}, @@ -1200,7 +1246,7 @@ test "structure", %{conn: conn} do assign(conn, :user, admin) |> get("/api/pleroma/admin/config/descriptions") - assert [child | _others] = json_response(conn, 200) + assert [child | _others] = json_response_and_validate_schema(conn, 200) assert child["children"] assert child["key"] @@ -1222,7 +1268,7 @@ test "filters by database configuration whitelist", %{conn: conn} do assign(conn, :user, admin) |> get("/api/pleroma/admin/config/descriptions") - children = json_response(conn, 200) + children = json_response_and_validate_schema(conn, 200) assert length(children) == 4 From 394258d548d20d1bea50166bc31f8e48462080dd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 28 May 2020 16:10:06 -0500 Subject: [PATCH 144/375] Docs: Attachement limitations in MastoAPI differences --- docs/API/differences_in_mastoapi_responses.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index e65fd5da4..434ade9a4 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -6,10 +6,6 @@ A Pleroma instance can be identified by " (compatible; Pleroma Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings -## Attachment cap - -Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting. - ## Timelines Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. @@ -32,12 +28,20 @@ Has these additional fields under the `pleroma` object: - `thread_muted`: true if the thread the post belongs to is muted - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. -## Attachments +## Media Attachments Has these additional fields under the `pleroma` object: - `mime_type`: mime type of the attachment. +### Attachment cap + +Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting. + +### Limitations + +Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it. + ## Accounts The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc. From 27180611dfffd064e65793f90c67dc16fff8ecc2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 29 May 2020 12:32:48 +0300 Subject: [PATCH 145/375] HTTP Security plug: make starting csp string generation more readable --- lib/pleroma/plugs/http_security_plug.ex | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index df38d5022..2208d1d6c 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -49,17 +49,16 @@ defp headers do end end - @csp_start [ - "default-src 'none'", - "base-uri 'self'", - "frame-ancestors 'none'", - "style-src 'self' 'unsafe-inline'", - "font-src 'self'", - "manifest-src 'self'" - ] - |> Enum.join(";") - |> Kernel.<>(";") - |> List.wrap() + static_csp_rules = [ + "default-src 'none'", + "base-uri 'self'", + "frame-ancestors 'none'", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "manifest-src 'self'" + ] + + @csp_start [Enum.join(static_csp_rules, ";") <> ";"] defp csp_string do scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] From 9df5b1e6ae8357942ef85563eebaf583f1dbc19a Mon Sep 17 00:00:00 2001 From: kPherox Date: Tue, 26 May 2020 11:32:05 +0000 Subject: [PATCH 146/375] Don't make relay announce notification --- lib/pleroma/web/activity_pub/side_effects.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 7eae0c52c..60ab8733d 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@ -36,8 +37,10 @@ def handle(%{data: %{"type" => "Announce"}} = object, meta) do Utils.add_announce_to_object(object, announced_object) - Notification.create_notifications(object) - ActivityPub.stream_out(object) + if object.data["actor"] != Relay.relay_ap_id() do + Notification.create_notifications(object) + ActivityPub.stream_out(object) + end {:ok, object, meta} end From 228ff3760efb62d4452b3025fa9e78fed164655e Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 27 May 2020 05:24:36 +0000 Subject: [PATCH 147/375] Use `User.is_internal_user?` instead --- lib/pleroma/web/activity_pub/side_effects.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 60ab8733d..fb6275450 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@ -34,10 +33,11 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do # - Stream out the announce def handle(%{data: %{"type" => "Announce"}} = object, meta) do announced_object = Object.get_by_ap_id(object.data["object"]) + user = User.get_cached_by_ap_id(object.data["actor"]) Utils.add_announce_to_object(object, announced_object) - if object.data["actor"] != Relay.relay_ap_id() do + if !User.is_internal_user?(user) do Notification.create_notifications(object) ActivityPub.stream_out(object) end From c86a88edec75223f650faa2bb442c09aa95ad694 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 29 May 2020 15:24:41 +0200 Subject: [PATCH 148/375] Streamer: Add a chat message stream. --- lib/pleroma/web/streamer/streamer.ex | 23 ++++++++++++++++++++++- lib/pleroma/web/views/streamer_view.ex | 19 +++++++++++++++++++ test/web/streamer/streamer_test.exs | 24 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 49a400df7..331490a78 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.Streamer do alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility @@ -22,7 +23,7 @@ defmodule Pleroma.Web.Streamer do def registry, do: @registry @public_streams ["public", "public:local", "public:media", "public:local:media"] - @user_streams ["user", "user:notification", "direct"] + @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] @doc "Expands and authorizes a stream, and registers the process for streaming." @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) :: @@ -200,6 +201,26 @@ defp do_stream(topic, %Notification{} = item) end) end + defp do_stream(topic, %{data: %{"type" => "ChatMessage"}} = object) + when topic in ["user", "user:pleroma_chat"] do + recipients = [object.data["actor"] | object.data["to"]] + + topics = + %{ap_id: recipients, local: true} + |> Pleroma.User.Query.build() + |> Repo.all() + |> Enum.map(fn %{id: id} = user -> {user, "#{topic}:#{id}"} end) + + Enum.each(topics, fn {user, topic} -> + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _auth} -> + text = StreamerView.render("chat_update.json", object, user, recipients) + send(pid, {:text, text}) + end) + end) + end) + end + defp do_stream("user", item) do Logger.debug("Trying to push to users") diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 237b29ded..949e2ed37 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -6,11 +6,30 @@ defmodule Pleroma.Web.StreamerView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.User alias Pleroma.Web.MastodonAPI.NotificationView + def render("chat_update.json", object, user, recipients) do + chat = Chat.get(user.id, hd(recipients -- [user.ap_id])) + + representation = + Pleroma.Web.PleromaAPI.ChatMessageView.render( + "show.json", + %{object: object, chat: chat} + ) + + %{ + event: "pleroma:chat_update", + payload: + representation + |> Jason.encode!() + } + |> Jason.encode!() + end + def render("update.json", %Activity{} = activity, %User{} = user) do %{ event: "update", diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 115ba4703..ffbff35ca 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -9,9 +9,11 @@ defmodule Pleroma.Web.StreamerTest do alias Pleroma.Conversation.Participation alias Pleroma.List + alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.Streamer + alias Pleroma.Web.StreamerView @moduletag needs_streamer: true, capture_log: true @@ -126,6 +128,28 @@ test "it sends notify to in the 'user:notification' stream", %{user: user, notif refute Streamer.filtered_by_user?(user, notify) end + test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + object = Object.normalize(create_activity, false) + Streamer.get_topic_and_add_socket("user:pleroma_chat", user) + Streamer.stream("user:pleroma_chat", object) + text = StreamerView.render("chat_update.json", object, user, [user.ap_id, other_user.ap_id]) + assert_receive {:text, ^text} + end + + test "it sends chat messages to the 'user' stream", %{user: user} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + object = Object.normalize(create_activity, false) + Streamer.get_topic_and_add_socket("user", user) + Streamer.stream("user", object) + text = StreamerView.render("chat_update.json", object, user, [user.ap_id, other_user.ap_id]) + assert_receive {:text, ^text} + end + test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do other_user = insert(:user) From 863c02b25d1c6128fab88c33d2c4c3565a6c378f Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 29 May 2020 15:44:03 +0200 Subject: [PATCH 149/375] SideEffects: Stream out chat messages. --- lib/pleroma/web/activity_pub/side_effects.ex | 2 ++ test/web/activity_pub/side_effects_test.exs | 21 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index f0f0659c2..a4de8691e 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Streamer def handle(object, meta \\ []) @@ -126,6 +127,7 @@ def handle(object, meta) do def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do + Streamer.stream(["user", "user:pleroma_chat"], object) actor = User.get_cached_by_ap_id(object.data["actor"]) recipient = User.get_cached_by_ap_id(hd(object.data["to"])) diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index fb4411c07..210ba6ef0 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -309,6 +309,27 @@ test "notifies the recipient" do assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) end + test "it streams the created ChatMessage" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + with_mock Pleroma.Web.Streamer, [], stream: fn _, _ -> nil end do + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + object = Object.normalize(create_activity, false) + + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], object)) + end + end + test "it creates a Chat for the local users and bumps the unread count, except for the author" do author = insert(:user, local: true) recipient = insert(:user, local: true) From 767ce8b8030562935ccd9f7c3d9ed83af0735db0 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 29 May 2020 16:02:45 +0200 Subject: [PATCH 150/375] StreamerView: Actually send Chats, not ChatMessages. --- lib/pleroma/web/pleroma_api/views/chat_view.ex | 2 +- lib/pleroma/web/views/streamer_view.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 08d5110c3..223b64987 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do def render("show.json", %{chat: %Chat{} = chat} = opts) do recipient = User.get_cached_by_ap_id(chat.recipient) - last_message = Chat.last_message_for_chat(chat) + last_message = opts[:message] || Chat.last_message_for_chat(chat) %{ id: chat.id |> to_string(), diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 949e2ed37..5e953d770 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -16,9 +16,9 @@ def render("chat_update.json", object, user, recipients) do chat = Chat.get(user.id, hd(recipients -- [user.ap_id])) representation = - Pleroma.Web.PleromaAPI.ChatMessageView.render( + Pleroma.Web.PleromaAPI.ChatView.render( "show.json", - %{object: object, chat: chat} + %{message: object, chat: chat} ) %{ From b08baf905b09ac49ed908eff8b43593d890612dd Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 29 May 2020 16:03:55 +0200 Subject: [PATCH 151/375] Docs: Document streaming differences --- docs/API/differences_in_mastoapi_responses.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index e65fd5da4..a9d1f2f38 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -226,3 +226,7 @@ Has theses additional parameters (which are the same as in Pleroma-API): Has these additional fields under the `pleroma` object: - `unread_count`: contains number unread notifications + +## Streaming + +There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. From 3898dd69a69d3af9793f2e1d442b409c84b319a8 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 29 May 2020 16:05:02 +0200 Subject: [PATCH 152/375] SideEffects: Ensure a chat is present before streaming something out. --- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index a4de8691e..02296b210 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -127,7 +127,6 @@ def handle(object, meta) do def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do - Streamer.stream(["user", "user:pleroma_chat"], object) actor = User.get_cached_by_ap_id(object.data["actor"]) recipient = User.get_cached_by_ap_id(hd(object.data["to"])) @@ -142,6 +141,7 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do end end) + Streamer.stream(["user", "user:pleroma_chat"], object) {:ok, object, meta} end end From 32431ad1ee88d260b720fab05fce76eb75bfe107 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 29 May 2020 16:07:40 +0200 Subject: [PATCH 153/375] Docs: Also add the streaming docs to the Chat api doc. --- docs/API/chats.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/API/chats.md b/docs/API/chats.md index 2e415e4da..2eca5adf6 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -220,3 +220,7 @@ There's a new `pleroma:chat_mention` notification, which has this form: "created_at": "somedate" } ``` + +### Streaming + +There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. From b3b367b894d1605202625310e7d8b1ed6ed5eb13 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 7 May 2020 21:52:45 +0200 Subject: [PATCH 154/375] Bugfix: Reuse Controller.Helper pagination for APC2S --- .../activity_pub/activity_pub_controller.ex | 3 ++ .../web/activity_pub/views/user_view.ex | 34 +++++-------- lib/pleroma/web/controller_helper.ex | 48 +++++++++++------- .../controllers/timeline_controller.ex | 4 +- .../activity_pub_controller_test.exs | 50 ++++++++++++++++++- .../web/activity_pub/views/user_view_test.exs | 31 ------------ 6 files changed, 94 insertions(+), 76 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 28727d619..b624d4255 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.ControllerHelper alias Pleroma.Web.Endpoint alias Pleroma.Web.FederatingPlug alias Pleroma.Web.Federator @@ -251,6 +252,7 @@ def outbox( |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, + pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}), iri: "#{user.ap_id}/outbox" }) end @@ -368,6 +370,7 @@ def read_inbox( |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, + pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}), iri: "#{user.ap_id}/inbox" }) end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 34590b16d..4a02b09a1 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -213,34 +213,24 @@ def render("activity_collection.json", %{iri: iri}) do |> Map.merge(Utils.make_json_ld_header()) end - def render("activity_collection_page.json", %{activities: activities, iri: iri}) do - # this is sorted chronologically, so first activity is the newest (max) - {max_id, min_id, collection} = - if length(activities) > 0 do - { - Enum.at(activities, 0).id, - Enum.at(Enum.reverse(activities), 0).id, - Enum.map(activities, fn act -> - {:ok, data} = Transmogrifier.prepare_outgoing(act.data) - data - end) - } - else - { - 0, - 0, - [] - } - end + def render("activity_collection_page.json", %{ + activities: activities, + iri: iri, + pagination: pagination + }) do + collection = + Enum.map(activities, fn activity -> + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + data + end) %{ - "id" => "#{iri}?max_id=#{max_id}&page=true", "type" => "OrderedCollectionPage", "partOf" => iri, - "orderedItems" => collection, - "next" => "#{iri}?max_id=#{min_id}&page=true" + "orderedItems" => collection } |> Map.merge(Utils.make_json_ld_header()) + |> Map.merge(pagination) end defp maybe_put_total_items(map, false, _total), do: map diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5a1316a5f..2d35bb56c 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller + alias Pleroma.Pagination + # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] @@ -46,6 +48,16 @@ def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, do: conn def add_link_headers(conn, activities, extra_params) do + case get_pagination_fields(conn, activities, extra_params) do + %{"next" => next_url, "prev" => prev_url} -> + put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") + + _ -> + conn + end + end + + def get_pagination_fields(conn, activities, extra_params \\ %{}) do case List.last(activities) do %{id: max_id} -> params = @@ -54,29 +66,29 @@ def add_link_headers(conn, activities, extra_params) do |> Map.drop(["since_id", "max_id", "min_id"]) |> Map.merge(extra_params) - limit = - params - |> Map.get("limit", "20") - |> String.to_integer() - min_id = - if length(activities) <= limit do - activities - |> List.first() - |> Map.get(:id) - else - activities - |> Enum.at(limit * -1) - |> Map.get(:id) - end + activities + |> List.first() + |> Map.get(:id) - next_url = current_url(conn, Map.merge(params, %{max_id: max_id})) - prev_url = current_url(conn, Map.merge(params, %{min_id: min_id})) + fields = %{ + "next" => current_url(conn, Map.put(params, :max_id, max_id)), + "prev" => current_url(conn, Map.put(params, :min_id, min_id)) + } - put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") + # Generating an `id` without already present pagination keys would + # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` + # instead of the `q.id > ^min_id` and `q.id < ^max_id`. + # This is because we only have ids present inside of the page, while + # `min_id`, `since_id` and `max_id` requires to know one outside of it. + if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do + Map.put(fields, "id", current_url(conn, conn.params)) + else + fields + end _ -> - conn + %{} end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 958567510..c852082a5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -51,10 +51,8 @@ def home(%{assigns: %{user: user}} = conn, params) do |> Map.put("reply_filtering_user", user) |> Map.put("user", user) - recipients = [user.ap_id | User.following(user)] - activities = - recipients + [user.ap_id | User.following(user)] |> ActivityPub.fetch_activities(params) |> Enum.reverse() diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 24edab41a..3f48553c9 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -804,17 +804,63 @@ test "it requires authentication", %{conn: conn} do end describe "GET /users/:nickname/outbox" do + test "it paginates correctly", %{conn: conn} do + user = insert(:user) + conn = assign(conn, :user, user) + outbox_endpoint = user.ap_id <> "/outbox" + + _posts = + for i <- 0..15 do + {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) + activity + end + + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint <> "?page=true") + |> json_response(200) + + result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) + assert length(result["orderedItems"]) == 10 + assert length(result_ids) == 10 + assert result["next"] + assert String.starts_with?(result["next"], outbox_endpoint) + + result_next = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result["next"]) + |> json_response(200) + + result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) + assert length(result_next["orderedItems"]) == 6 + assert length(result_next_ids) == 6 + refute Enum.find(result_next_ids, fn x -> x in result_ids end) + refute Enum.find(result_ids, fn x -> x in result_next_ids end) + assert String.starts_with?(result["id"], outbox_endpoint) + + result_next_again = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result_next["id"]) + |> json_response(200) + + assert result_next == result_next_again + end + test "it returns 200 even if there're no activities", %{conn: conn} do user = insert(:user) + outbox_endpoint = user.ap_id <> "/outbox" conn = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/outbox") + |> get(outbox_endpoint) result = json_response(conn, 200) - assert user.ap_id <> "/outbox" == result["id"] + assert outbox_endpoint == result["id"] end test "it returns a note activity in a collection", %{conn: conn} do diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 20b0f223c..bec15a996 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -158,35 +158,4 @@ test "sets correct totalItems when follows are hidden but the follow counter is assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) end end - - test "activity collection page aginates correctly" do - user = insert(:user) - - posts = - for i <- 0..25 do - {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) - activity - end - - # outbox sorts chronologically, newest first, with ten per page - posts = Enum.reverse(posts) - - %{"next" => next_url} = - UserView.render("activity_collection_page.json", %{ - iri: "#{user.ap_id}/outbox", - activities: Enum.take(posts, 10) - }) - - next_id = Enum.at(posts, 9).id - assert next_url =~ next_id - - %{"next" => next_url} = - UserView.render("activity_collection_page.json", %{ - iri: "#{user.ap_id}/outbox", - activities: Enum.take(Enum.drop(posts, 10), 10) - }) - - next_id = Enum.at(posts, 19).id - assert next_url =~ next_id - end end From 2c18830d0dbd7f63cd20dcf5167254fede538930 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 8 May 2020 03:08:11 +0200 Subject: [PATCH 155/375] Bugfix: router: allow basic_auth for outbox --- lib/pleroma/web/router.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e493a4153..d65af23d9 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -571,13 +571,6 @@ defmodule Pleroma.Web.Router do get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end - scope "/", Pleroma.Web.ActivityPub do - # XXX: not really ostatus - pipe_through(:ostatus) - - get("/users/:nickname/outbox", ActivityPubController, :outbox) - end - pipeline :ap_service_actor do plug(:accepts, ["activity+json", "json"]) end @@ -602,6 +595,7 @@ defmodule Pleroma.Web.Router do get("/api/ap/whoami", ActivityPubController, :whoami) get("/users/:nickname/inbox", ActivityPubController, :read_inbox) + get("/users/:nickname/outbox", ActivityPubController, :outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/api/ap/upload_media", ActivityPubController, :upload_media) From a43b435c0ad8a1198241fbd18e1a5f1be830f4b5 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 8 May 2020 03:05:56 +0200 Subject: [PATCH 156/375] AP C2S: allow limit & order on outbox & read_inbox --- .../activity_pub/activity_pub_controller.ex | 45 +++++++++---------- lib/pleroma/web/controller_helper.ex | 2 +- .../activity_pub_controller_test.exs | 6 +-- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index b624d4255..5b8441384 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -231,28 +231,22 @@ def outbox( 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, 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 - "include_poll_votes" => true, - "limit" => 10 - }) - else - ActivityPub.fetch_user_activities(user, for_user, %{ - "limit" => 10, - "include_poll_votes" => true - }) - end + # "include_poll_votes" 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 + params = + params + |> Map.drop(["nickname", "page"]) + |> Map.put("include_poll_votes", true) + + activities = ActivityPub.fetch_user_activities(user, for_user, params) conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, - pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}), + pagination: ControllerHelper.get_pagination_fields(conn, activities), iri: "#{user.ap_id}/outbox" }) end @@ -355,22 +349,23 @@ def read_inbox( %{"nickname" => nickname, "page" => page?} = params ) when page? in [true, "true"] do + params = + params + |> Map.drop(["nickname", "page"]) + |> Map.put("blocking_user", user) + |> Map.put("user", user) + activities = - if params["max_id"] do - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{ - "max_id" => params["max_id"], - "limit" => 10 - }) - else - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10}) - end + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, - pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}), + pagination: ControllerHelper.get_pagination_fields(conn, activities), iri: "#{user.ap_id}/inbox" }) end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 2d35bb56c..9e5444817 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -63,8 +63,8 @@ def get_pagination_fields(conn, activities, extra_params \\ %{}) do params = conn.params |> Map.drop(Map.keys(conn.path_params)) - |> Map.drop(["since_id", "max_id", "min_id"]) |> Map.merge(extra_params) + |> Map.drop(Pagination.page_keys() -- ["limit", "order"]) min_id = activities diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 3f48553c9..e490a5744 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -810,7 +810,7 @@ test "it paginates correctly", %{conn: conn} do outbox_endpoint = user.ap_id <> "/outbox" _posts = - for i <- 0..15 do + for i <- 0..25 do {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) activity end @@ -822,8 +822,8 @@ test "it paginates correctly", %{conn: conn} do |> json_response(200) result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) - assert length(result["orderedItems"]) == 10 - assert length(result_ids) == 10 + assert length(result["orderedItems"]) == 20 + assert length(result_ids) == 20 assert result["next"] assert String.starts_with?(result["next"], outbox_endpoint) From 1b586ff3aece21d277e40f95cc5c60fc15818a87 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 29 May 2020 10:17:06 -0500 Subject: [PATCH 157/375] Document new database vacuum tasks --- docs/administration/CLI_tasks/database.md | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/administration/CLI_tasks/database.md b/docs/administration/CLI_tasks/database.md index ff400c8ed..647f6f274 100644 --- a/docs/administration/CLI_tasks/database.md +++ b/docs/administration/CLI_tasks/database.md @@ -69,3 +69,32 @@ mix pleroma.database update_users_following_followers_counts ```sh tab="From Source" mix pleroma.database fix_likes_collections ``` + +## Vacuum the database + +### Analyze + +Running an `analyze` vacuum job can improve performance by updating statistics used by the query planner. **It is safe to cancel this.** + +```sh tab="OTP" +./bin/pleroma_ctl database vacuum analyze +``` + +```sh tab="From Source" +mix pleroma.database vacuum analyze +``` + +### Full + +Running a `full` vacuum job rebuilds your entire database by reading all of the data and rewriting it into smaller +and more compact files with an optimized layout. This process will take a long time and use additional disk space as +it builds the files side-by-side the existing database files. It can make your database faster and use less disk space, +but should only be run if necessary. **It is safe to cancel this.** + +```sh tab="OTP" +./bin/pleroma_ctl database vacuum full +``` + +```sh tab="From Source" +mix pleroma.database vacuum full +``` \ No newline at end of file From da1e31fae3f7a7e0063c3a6fb4315e1578d72daa Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 29 May 2020 17:17:02 +0200 Subject: [PATCH 158/375] http_security_plug.ex: Fix non-proxied media --- lib/pleroma/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 2208d1d6c..4b926e867 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -75,7 +75,7 @@ defp csp_string do sources = get_proxy_and_attachment_sources() {[img_src, sources], [media_src, sources]} else - {img_src, media_src} + {img_src <> " https:", media_src <> " https:"} end connect_src = ["connect-src 'self' ", static_url, ?\s, websocket_url] From de0e2628391ca039ac0d029c251136d53b6f8e63 Mon Sep 17 00:00:00 2001 From: kPherox Date: Mon, 25 May 2020 23:21:43 +0900 Subject: [PATCH 159/375] Fix argument error in streamer `Repo.exists` can't use `nil` as it is unsafe. Use parent object instead of activity because currently Announce activity's context is null. --- lib/pleroma/web/streamer/streamer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 49a400df7..0cf41189b 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -136,7 +136,7 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), true <- thread_containment(item, user), - false <- CommonAPI.thread_muted?(user, item) do + false <- CommonAPI.thread_muted?(user, parent) do false else _ -> true From 9ca978494fee4be96ec9b6b93e74afe08dd05fcc Mon Sep 17 00:00:00 2001 From: kPherox Date: Fri, 29 May 2020 21:08:09 +0900 Subject: [PATCH 160/375] Add test for stream boosts of mastodon user --- test/web/streamer/streamer_test.exs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index cb4595bb6..4cf640ce8 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -112,6 +112,25 @@ test "it streams boosts of the user in the 'user' stream", %{user: user} do refute Streamer.filtered_by_user?(user, announce) end + test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do + Streamer.get_topic_and_add_socket("user", user) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", user.ap_id) + + {:ok, %Pleroma.Activity{data: data, local: false} = announce} = + Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) + + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + refute Streamer.filtered_by_user?(user, announce) + end + test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do Streamer.get_topic_and_add_socket("user", user) Streamer.stream("user", notify) From d38f28870e7ba1c8c1b315d52e68a83fb1a68b6d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 29 May 2020 10:33:31 -0500 Subject: [PATCH 161/375] Add blob: to connect-src CSP --- CHANGELOG.md | 1 + lib/pleroma/plugs/http_security_plug.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dabc2a85a..839bf90ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix follower/blocks import when nicknames starts with @ - Filtering of push notifications on activities from blocked domains - Resolving Peertube accounts with Webfinger +- `blob:` urls not being allowed by connect-src CSP ## [Unreleased (patch)] diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 2208d1d6c..41e3a31f4 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -78,7 +78,7 @@ defp csp_string do {img_src, media_src} end - connect_src = ["connect-src 'self' ", static_url, ?\s, websocket_url] + connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] connect_src = if Pleroma.Config.get(:env) == :dev do From c181e555db4a90f770418af67b1073ec958adb4d Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 29 May 2020 22:03:14 +0300 Subject: [PATCH 162/375] [#1794] Improvements to hashtags extraction from search query. --- .../controllers/search_controller.ex | 40 ++++++++++++++----- .../controllers/search_controller_test.exs | 36 ++++++++++++++++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 77e2224e4..23fe378a6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -113,22 +113,44 @@ defp resource_search(:v2, "hashtags", query, _options) do query |> prepare_tags() |> Enum.map(fn tag -> - tag = String.trim_leading(tag, "#") %{name: tag, url: tags_path <> tag} end) end defp resource_search(:v1, "hashtags", query, _options) do - query - |> prepare_tags() - |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) + prepare_tags(query) end - defp prepare_tags(query) do - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + defp prepare_tags(query, add_joined_tag \\ true) do + tags = + query + |> String.split(~r/[^#\w]+/, trim: true) + |> Enum.uniq_by(&String.downcase/1) + + explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end) + + tags = + if Enum.any?(explicit_tags) do + explicit_tags + else + tags + end + + tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end) + + if Enum.empty?(explicit_tags) && add_joined_tag do + tags + |> Kernel.++([joined_tag(tags)]) + |> Enum.uniq_by(&String.downcase/1) + else + tags + end + end + + defp joined_tag(tags) do + tags + |> Enum.map(fn tag -> String.capitalize(tag) end) + |> Enum.join() end defp with_fallback(f, fallback \\ []) do diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 7d0cafccc..498290377 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -75,6 +75,40 @@ test "search", %{conn: conn} do assert status["id"] == to_string(activity.id) end + test "constructs hashtags from search query", %{conn: conn} do + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"}, + %{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "john", "url" => "#{Web.base_url()}/tag/john"}, + %{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"}, + %{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"}, + %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, + %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} + ] + end + test "excludes a blocked users from search results", %{conn: conn} do user = insert(:user) user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) @@ -179,7 +213,7 @@ test "search", %{conn: conn} do [account | _] = results["accounts"] assert account["id"] == to_string(user_three.id) - assert results["hashtags"] == [] + assert results["hashtags"] == ["2hu"] [status] = results["statuses"] assert status["id"] == to_string(activity.id) From 0a83af330b7f33601848bca79bd1651b45eaea87 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 29 May 2020 23:05:03 +0300 Subject: [PATCH 163/375] fix unused var warning --- test/web/streamer/streamer_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 4cf640ce8..3f012259a 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -124,7 +124,7 @@ test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do |> Map.put("object", activity.data["object"]) |> Map.put("actor", user.ap_id) - {:ok, %Pleroma.Activity{data: data, local: false} = announce} = + {:ok, %Pleroma.Activity{data: _data, local: false} = announce} = Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} From 109af93227f65d308641e345c68c3884addb0181 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 29 May 2020 21:15:07 +0000 Subject: [PATCH 164/375] Apply suggestion to lib/pleroma/plugs/http_security_plug.ex --- lib/pleroma/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 4b926e867..589072535 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -75,7 +75,7 @@ defp csp_string do sources = get_proxy_and_attachment_sources() {[img_src, sources], [media_src, sources]} else - {img_src <> " https:", media_src <> " https:"} + {[img_src, " https:"], [media_src, " https:"]} end connect_src = ["connect-src 'self' ", static_url, ?\s, websocket_url] From d2a1975e565e2e83859a607af29320226877cc4d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 30 May 2020 00:18:17 +0300 Subject: [PATCH 165/375] mix.lock: update hackney to 1.16.0 Closes #1612 --- mix.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mix.lock b/mix.lock index 470b401a3..5383c2c6e 100644 --- a/mix.lock +++ b/mix.lock @@ -12,7 +12,7 @@ "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"]}, "castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"}, - "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, + "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, @@ -50,12 +50,12 @@ "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"}, "gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]}, - "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, + "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]}, "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, + "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, @@ -102,7 +102,7 @@ "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, @@ -112,7 +112,7 @@ "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"}, "ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "db9fbfb5ac707bc4f85a297758406340bf0358b4af737a88113c1a9eee120ac7"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "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", []}, From 24f40b8a26f95ee7f50b6023176d361660ceb35c Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 30 May 2020 10:29:08 +0300 Subject: [PATCH 166/375] [#1794] Fixed search query splitting regex to deal with Unicode. Adjusted a test. --- lib/pleroma/web/mastodon_api/controllers/search_controller.ex | 2 +- test/web/mastodon_api/controllers/search_controller_test.exs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 23fe378a6..8840fc19c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -124,7 +124,7 @@ defp resource_search(:v1, "hashtags", query, _options) do defp prepare_tags(query, add_joined_tag \\ true) do tags = query - |> String.split(~r/[^#\w]+/, trim: true) + |> String.split(~r/[^#\w]+/u, trim: true) |> Enum.uniq_by(&String.downcase/1) explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end) diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 498290377..84d46895e 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -71,6 +71,10 @@ test "search", %{conn: conn} do get(conn, "/api/v2/search?q=天子") |> json_response_and_validate_schema(200) + assert results["hashtags"] == [ + %{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} + ] + [status] = results["statuses"] assert status["id"] == to_string(activity.id) end From 6d4b80822b15f5958518f4c6006862fb1f92354a Mon Sep 17 00:00:00 2001 From: Steven Fuchs Date: Sat, 30 May 2020 10:02:37 +0000 Subject: [PATCH 167/375] Conversation pagination --- .../controllers/conversation_controller.ex | 17 ++ .../conversation_controller_test.exs | 165 +++++++++--------- 2 files changed, 100 insertions(+), 82 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index f35ec3596..69f0e3846 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do @doc "GET /api/v1/conversations" def index(%{assigns: %{user: user}} = conn, params) do + params = stringify_pagination_params(params) participations = Participation.for_user_with_last_activity_id(user, params) conn @@ -36,4 +37,20 @@ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do render(conn, "participation.json", participation: participation, for: user) end end + + defp stringify_pagination_params(params) do + atom_keys = + Pleroma.Pagination.page_keys() + |> Enum.map(&String.to_atom(&1)) + + str_keys = + params + |> Map.take(atom_keys) + |> Enum.map(fn {key, value} -> {to_string(key), value} end) + |> Enum.into(%{}) + + params + |> Map.delete(atom_keys) + |> Map.merge(str_keys) + end end diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs index 693ba51e5..3e21e6bf1 100644 --- a/test/web/mastodon_api/controllers/conversation_controller_test.exs +++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs @@ -12,84 +12,88 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do setup do: oauth_access(["read:statuses"]) - test "returns a list of conversations", %{user: user_one, conn: conn} do - user_two = insert(:user) - user_three = insert(:user) + describe "returns a list of conversations" do + setup(%{user: user_one, conn: conn}) do + user_two = insert(:user) + user_three = insert(:user) - {:ok, user_two} = User.follow(user_two, user_one) + {:ok, user_two} = User.follow(user_two, user_one) - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + {:ok, %{user: user_one, user_two: user_two, user_three: user_three, conn: conn}} + end - {:ok, direct} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}, @#{user_three.nickname}!", - visibility: "direct" - }) + test "returns correct conversations", %{ + user: user_one, + user_two: user_two, + user_three: user_three, + conn: conn + } do + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + {:ok, direct} = create_direct_message(user_one, [user_two, user_three]) - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 - {:ok, _follower_only} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "private" - }) + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "private" + }) - res_conn = get(conn, "/api/v1/conversations") + res_conn = get(conn, "/api/v1/conversations") - assert response = json_response_and_validate_schema(res_conn, 200) + assert response = json_response_and_validate_schema(res_conn, 200) - assert [ - %{ - "id" => res_id, - "accounts" => res_accounts, - "last_status" => res_last_status, - "unread" => unread - } - ] = response + assert [ + %{ + "id" => res_id, + "accounts" => res_accounts, + "last_status" => res_last_status, + "unread" => unread + } + ] = response - account_ids = Enum.map(res_accounts, & &1["id"]) - assert length(res_accounts) == 2 - assert user_two.id in account_ids - assert user_three.id in account_ids - assert is_binary(res_id) - assert unread == false - assert res_last_status["id"] == direct.id - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + account_ids = Enum.map(res_accounts, & &1["id"]) + assert length(res_accounts) == 2 + assert user_two.id in account_ids + assert user_three.id in account_ids + assert is_binary(res_id) + assert unread == false + assert res_last_status["id"] == direct.id + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + end + + test "observes limit params", %{ + user: user_one, + user_two: user_two, + user_three: user_three, + conn: conn + } do + {:ok, _} = create_direct_message(user_one, [user_two, user_three]) + {:ok, _} = create_direct_message(user_two, [user_one, user_three]) + {:ok, _} = create_direct_message(user_three, [user_two, user_one]) + + res_conn = get(conn, "/api/v1/conversations?limit=1") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert Enum.count(response) == 1 + + res_conn = get(conn, "/api/v1/conversations?limit=2") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert Enum.count(response) == 2 + end end test "filters conversations by recipients", %{user: user_one, conn: conn} do user_two = insert(:user) user_three = insert(:user) - - {:ok, direct1} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "direct" - }) - - {:ok, _direct2} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_three.nickname}!", - visibility: "direct" - }) - - {:ok, direct3} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}, @#{user_three.nickname}!", - visibility: "direct" - }) - - {:ok, _direct4} = - CommonAPI.post(user_two, %{ - status: "Hi @#{user_three.nickname}!", - visibility: "direct" - }) - - {:ok, direct5} = - CommonAPI.post(user_two, %{ - status: "Hi @#{user_one.nickname}!", - visibility: "direct" - }) + {:ok, direct1} = create_direct_message(user_one, [user_two]) + {:ok, _direct2} = create_direct_message(user_one, [user_three]) + {:ok, direct3} = create_direct_message(user_one, [user_two, user_three]) + {:ok, _direct4} = create_direct_message(user_two, [user_three]) + {:ok, direct5} = create_direct_message(user_two, [user_one]) assert [conversation1, conversation2] = conn @@ -109,12 +113,7 @@ test "filters conversations by recipients", %{user: user_one, conn: conn} do test "updates the last_status on reply", %{user: user_one, conn: conn} do user_two = insert(:user) - - {:ok, direct} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}", - visibility: "direct" - }) + {:ok, direct} = create_direct_message(user_one, [user_two]) {:ok, direct_reply} = CommonAPI.post(user_two, %{ @@ -133,12 +132,7 @@ test "updates the last_status on reply", %{user: user_one, conn: conn} do test "the user marks a conversation as read", %{user: user_one, conn: conn} do user_two = insert(:user) - - {:ok, direct} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}", - visibility: "direct" - }) + {:ok, direct} = create_direct_message(user_one, [user_two]) assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 @@ -194,15 +188,22 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do user_two = insert(:user) - - {:ok, direct} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "direct" - }) + {:ok, direct} = create_direct_message(user_one, [user_two]) res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context") assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) end + + defp create_direct_message(sender, recips) do + hellos = + recips + |> Enum.map(fn s -> "@#{s.nickname}" end) + |> Enum.join(", ") + + CommonAPI.post(sender, %{ + status: "Hi #{hellos}!", + visibility: "direct" + }) + end end From 2c9465cc51160546ae054d1a1912fbb8e9add8e8 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 30 May 2020 12:17:18 +0200 Subject: [PATCH 168/375] SafeText: Let through basic html. --- .../object_validators/types/safe_text.ex | 2 +- test/web/activity_pub/object_validator_test.exs | 14 ++++++++++++++ .../object_validators/types/safe_text_test.exs | 7 +++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex index 822e8d2c1..95c948123 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex +++ b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText do def type, do: :string def cast(str) when is_binary(str) do - {:ok, HTML.strip_tags(str)} + {:ok, HTML.filter_tags(str)} end def cast(_), do: :error diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 929fdbc9b..31224abe0 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -113,6 +113,20 @@ test "it is invalid if the object data has a different `to` or `actor` field" do %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} end + test "let's through some basic html", %{user: user, recipient: recipient} do + {:ok, valid_chat_message, _} = + Builder.chat_message( + user, + recipient.ap_id, + "hey example " + ) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["content"] == + "hey example alert('uguu')" + end + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs index 59ed0a1fe..d4a574554 100644 --- a/test/web/activity_pub/object_validators/types/safe_text_test.exs +++ b/test/web/activity_pub/object_validators/types/safe_text_test.exs @@ -17,6 +17,13 @@ test "it removes html tags from text" do assert {:ok, "hey look xss alert('foo')"} == SafeText.cast(text) end + test "it keeps basic html tags" do + text = "hey look xss " + + assert {:ok, "hey look xss alert('foo')"} == + SafeText.cast(text) + end + test "errors for non-text" do assert :error == SafeText.cast(1) end From 8bdf18d7c10f0e740b2f5e0fa5063c522b8b3872 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 30 May 2020 12:30:31 +0200 Subject: [PATCH 169/375] CommonAPI: Linkify chat messages. --- lib/pleroma/web/common_api/common_api.ex | 7 ++++++- test/web/common_api/common_api_test.exs | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 764fa4f4f..173353aa5 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -50,7 +50,12 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) defp format_chat_content(nil), do: nil defp format_chat_content(content) do - content |> Formatter.html_escape("text/plain") + {text, _, _} = + content + |> Formatter.html_escape("text/plain") + |> Formatter.linkify() + + text end defp validate_chat_content_length(_, true), do: :ok diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 9e129e5a7..41c6909de 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -50,6 +50,29 @@ test "it posts a chat message without content but with an attachment" do assert activity end + test "it linkifies" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "https://example.org is the site of @#{other_user.nickname} #2hu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == + "https://example.org is the site of @#{other_user.nickname} #2hu" + end + test "it posts a chat message" do author = insert(:user) recipient = insert(:user) From 0cb7b0ea8477bdd7af2e5e9071843be5b8623dff Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 30 May 2020 13:59:04 +0300 Subject: [PATCH 170/375] hackney adapter helper: support tlsv1.3 and remove custom opts - partitial_chain is no longer exported, but it seems to be the default anyway. - The bug that caused sni to not be sent automatically seems to be fixed - https://github.com/benoitc/hackney/issues/612 --- lib/pleroma/http/adapter_helper/hackney.ex | 17 +---------------- test/http/adapter_helper/hackney_test.exs | 12 ------------ 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index dcb4cac71..3972a03a9 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -22,22 +22,7 @@ def options(connection_opts \\ [], %URI{} = uri) do |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) end - defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts - - defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do - ssl_opts = [ - ssl_options: [ - # Workaround for remote server certificate chain issues - partial_chain: &:hackney_connect.partial_chain/1, - - # We don't support TLS v1.3 yet - versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"], - server_name_indication: to_charlist(host) - ] - ] - - Keyword.merge(opts, ssl_opts) - end + defp add_scheme_opts(opts, _), do: opts def after_request(_), do: :ok end diff --git a/test/http/adapter_helper/hackney_test.exs b/test/http/adapter_helper/hackney_test.exs index 3f7e708e0..f2361ff0b 100644 --- a/test/http/adapter_helper/hackney_test.exs +++ b/test/http/adapter_helper/hackney_test.exs @@ -31,17 +31,5 @@ test "respect connection opts and no proxy", %{uri: uri} do assert opts[:b] == 1 refute Keyword.has_key?(opts, :proxy) end - - test "add opts for https" do - uri = URI.parse("https://domain.com") - - opts = Hackney.options(uri) - - assert opts[:ssl_options] == [ - partial_chain: &:hackney_connect.partial_chain/1, - versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"], - server_name_indication: 'domain.com' - ] - end end end From b973d0b2f0809e7a96c39f6eef1d86050c9d421b Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 30 May 2020 16:47:09 +0300 Subject: [PATCH 171/375] Fix config setting to not affect other tests --- test/user_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/user_test.exs b/test/user_test.exs index 3556ef1b4..6b344158d 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1802,7 +1802,7 @@ test "avatar fallback" do user = insert(:user) assert User.avatar_url(user) =~ "/images/avi.png" - Pleroma.Config.put([:assets, :default_user_avatar], "avatar.png") + clear_config([:assets, :default_user_avatar], "avatar.png") user = User.get_cached_by_nickname_or_id(user.nickname) assert User.avatar_url(user) =~ "avatar.png" From 9460983032257022ff29c063901f6b714e4fbf59 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 1 Jun 2020 13:03:22 +0200 Subject: [PATCH 172/375] AccountController: Federate user account changes. Hotfixy commit, will be moved to the pipeline. --- .../controllers/account_controller.ex | 23 +++++++++-- .../update_credentials_test.exs | 38 +++++++++++-------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 47649d41d..97295a52f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -139,9 +139,7 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do end @doc "PATCH /api/v1/accounts/update_credentials" - def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do - user = original_user - + def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do params = params |> Enum.filter(fn {_, value} -> not is_nil(value) end) @@ -183,12 +181,31 @@ def update_credentials(%{assigns: %{user: original_user}, body_params: params} = changeset = User.update_changeset(user, user_params) with {:ok, user} <- User.update_and_set_cache(changeset) do + user + |> build_update_activity_params() + |> ActivityPub.update() + render(conn, "show.json", user: user, for: user, with_pleroma_settings: true) else _e -> render_error(conn, :forbidden, "Invalid request") end end + # Hotfix, handling will be redone with the pipeline + defp build_update_activity_params(user) do + object = + Pleroma.Web.ActivityPub.UserView.render("user.json", user: user) + |> Map.delete("@context") + + %{ + local: true, + to: [user.follower_address], + cc: [], + object: object, + actor: user.ap_id + } + end + defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do with true <- is_map(params), true <- Map.has_key?(params, params_field), 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 696228203..7c420985d 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 @@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do use Pleroma.Web.ConnCase + import Mock import Pleroma.Factory setup do: clear_config([:instance, :max_account_fields]) @@ -52,24 +53,31 @@ test "sets user settings in a generic way", %{conn: conn} do user = Repo.get(User, user_data["id"]) - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "blub" + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _activity -> :ok end do + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "blub" + } } - } - }) + }) - assert user_data = json_response_and_validate_schema(res_conn, 200) + assert user_data = json_response_and_validate_schema(res_conn, 200) - assert user_data["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "blub"} - } + assert user_data["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "blub"} + } + + assert_called(Pleroma.Web.Federator.publish(:_)) + end end test "updates the user's bio", %{conn: conn} do From d4d4b92f758979fbc22cd56a9f30435df5c40ab6 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 1 Jun 2020 13:17:56 +0200 Subject: [PATCH 173/375] TimelineController: Only return `Create` in public timelines. --- .../mastodon_api/controllers/timeline_controller.ex | 2 +- .../controllers/timeline_controller_test.exs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 958567510..f67f75430 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -111,7 +111,7 @@ def public(%{assigns: %{user: user}} = conn, params) do else activities = params - |> Map.put("type", ["Create", "Announce"]) + |> Map.put("type", ["Create"]) |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 2375ac8e8..65b4079fe 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -60,9 +60,9 @@ test "the home timeline when the direct messages are excluded", %{user: user, co describe "public" do @tag capture_log: true test "the public timeline", %{conn: conn} do - following = insert(:user) + user = insert(:user) - {:ok, _activity} = CommonAPI.post(following, %{status: "test"}) + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) _activity = insert(:note_activity, local: false) @@ -77,6 +77,13 @@ test "the public timeline", %{conn: conn} do conn = get(build_conn(), "/api/v1/timelines/public?local=1") assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) + + # does not contain repeats + {:ok, _} = CommonAPI.repeat(activity.id, user) + + conn = get(build_conn(), "/api/v1/timelines/public?local=true") + + assert [_] = json_response_and_validate_schema(conn, :ok) end test "the public timeline includes only public statuses for an authenticated user" do From ac31f687c0fbe06251257acb72b67146b472d22f Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 1 Jun 2020 13:35:39 +0200 Subject: [PATCH 174/375] Config: Default to Hackney again Gun needs some server setting changes (files) and has problems with OTP 23 (wildcards), so use Hackney as a default again for now. --- config/config.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index d15998715..9a9fbb436 100644 --- a/config/config.exs +++ b/config/config.exs @@ -171,7 +171,8 @@ "application/ld+json" => ["activity+json"] } -config :tesla, adapter: Tesla.Adapter.Gun +config :tesla, adapter: Tesla.Adapter.Hackney + # Configures http settings, upstream proxy etc. config :pleroma, :http, proxy_url: nil, From af9090238e1f71e6b081fbd09c09a5975d2ed99e Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 1 Jun 2020 15:14:22 +0200 Subject: [PATCH 175/375] CommonAPI: Newlines -> br for chat messages. --- lib/pleroma/web/common_api/common_api.ex | 3 +++ test/web/common_api/common_api_test.exs | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 173353aa5..e0987b1a7 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -54,6 +54,9 @@ defp format_chat_content(content) do content |> Formatter.html_escape("text/plain") |> Formatter.linkify() + |> (fn {text, mentions, tags} -> + {String.replace(text, ~r/\r?\n/, "
"), mentions, tags} + end).() text end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 41c6909de..611a9ae66 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -50,6 +50,26 @@ test "it posts a chat message without content but with an attachment" do assert activity end + test "it adds html newlines" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "uguu\nuguuu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == "uguu
uguuu" + end + test "it linkifies" do author = insert(:user) recipient = insert(:user) From 7e6ec778d965419ed4083428d4d39b2a689f7619 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 20 May 2020 17:45:06 +0300 Subject: [PATCH 176/375] exclude replies on blocked domains --- benchmarks/load_testing/activities.ex | 365 ++++++++++-------- benchmarks/load_testing/fetcher.ex | 71 ++++ benchmarks/load_testing/users.ex | 22 +- .../mix/tasks/pleroma/benchmarks/tags.ex | 38 +- lib/pleroma/web/activity_pub/activity_pub.ex | 27 ++ .../api_spec/operations/timeline_operation.ex | 7 + .../controllers/timeline_controller.ex | 13 +- ...ients_contain_blocked_domains_function.exs | 33 ++ .../controllers/timeline_controller_test.exs | 68 ++++ 9 files changed, 469 insertions(+), 175 deletions(-) create mode 100644 priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index ff0d481a8..074ded457 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -22,8 +22,21 @@ defmodule Pleroma.LoadTesting.Activities do @max_concurrency 10 @visibility ~w(public private direct unlisted) - @types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote) - @groups ~w(user friends non_friends) + @types [ + :simple, + :emoji, + :mentions, + :hell_thread, + :attachment, + :tag, + :like, + :reblog, + :simple_thread + ] + @groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local] + @remote_groups [:friends_remote, :non_friends_remote] + @friends_groups [:friends_local, :friends_remote] + @non_friends_groups [:non_friends_local, :non_friends_remote] @spec generate(User.t(), keyword()) :: :ok def generate(user, opts \\ []) do @@ -34,33 +47,24 @@ def generate(user, opts \\ []) do opts = Keyword.merge(@defaults, opts) - friends = - user - |> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true) - |> Enum.shuffle() + users = Users.prepare_users(user, opts) - non_friends = - user - |> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false) - |> Enum.shuffle() + {:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote) task_data = for visibility <- @visibility, type <- @types, - group <- @groups, + group <- [:user | @groups], do: {visibility, type, group} IO.puts("Starting generating #{opts[:iterations]} iterations of activities...") - friends_thread = Enum.take(friends, 5) - non_friends_thread = Enum.take(friends, 5) - public_long_thread = fn -> - generate_long_thread("public", user, friends_thread, non_friends_thread, opts) + generate_long_thread("public", users, opts) end private_long_thread = fn -> - generate_long_thread("private", user, friends_thread, non_friends_thread, opts) + generate_long_thread("private", users, opts) end iterations = opts[:iterations] @@ -73,10 +77,10 @@ def generate(user, opts \\ []) do i when i == iterations - 2 -> spawn(public_long_thread) spawn(private_long_thread) - generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) + generate_activities(users, Enum.shuffle(task_data), opts) _ -> - generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) + generate_activities(users, Enum.shuffle(task_data), opts) end ) end) @@ -127,16 +131,16 @@ def generate_tagged_activities(opts \\ []) do end) end - defp generate_long_thread(visibility, user, friends, non_friends, _opts) do + defp generate_long_thread(visibility, users, _opts) do group = if visibility == "public", - do: "friends", - else: "user" + do: :friends_local, + else: :user tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50) {:ok, activity} = - CommonAPI.post(user, %{ + CommonAPI.post(users[:user], %{ status: "Start of #{visibility} long thread", visibility: visibility }) @@ -150,31 +154,28 @@ defp generate_long_thread(visibility, user, friends, non_friends, _opts) do Map.put(state, key, activity) end) - acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]} - insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) + acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]} + insert_replies_for_long_thread(tasks, visibility, users, acc) IO.puts("Generating #{visibility} long thread ended\n") end - defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do + defp insert_replies_for_long_thread(tasks, visibility, users, acc) do Enum.reduce(tasks, acc, fn - "friend", {id, data} -> - friend = Enum.random(friends) - insert_reply(friend, List.delete(data, "@" <> friend.nickname), id, visibility) - - "non_friend", {id, data} -> - non_friend = Enum.random(non_friends) - insert_reply(non_friend, List.delete(data, "@" <> non_friend.nickname), id, visibility) - - "user", {id, data} -> + :user, {id, data} -> + user = users[:user] insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility) + + group, {id, data} -> + replier = Enum.random(users[group]) + insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility) end) end - defp generate_activities(user, friends, non_friends, task_data, opts) do + defp generate_activities(users, task_data, opts) do Task.async_stream( task_data, fn {visibility, type, group} -> - insert_activity(type, visibility, group, user, friends, non_friends, opts) + insert_activity(type, visibility, group, users, opts) end, max_concurrency: @max_concurrency, timeout: 30_000 @@ -182,67 +183,104 @@ defp generate_activities(user, friends, non_friends, task_data, opts) do |> Stream.run() end - defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do - {:ok, _activity} = + defp insert_local_activity(visibility, group, users, status) do + {:ok, _} = group - |> get_actor(user, friends, non_friends) - |> CommonAPI.post(%{status: "Simple status", visibility: visibility}) + |> get_actor(users) + |> CommonAPI.post(%{status: status, visibility: visibility}) end - defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do - {:ok, _activity} = - group - |> get_actor(user, friends, non_friends) - |> CommonAPI.post(%{ - status: "Simple status with emoji :firefox:", - visibility: visibility - }) + defp insert_remote_activity(visibility, group, users, status) do + actor = get_actor(group, users) + {act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user]) + {activity_data, object_data} = other_data(actor, status) + + activity_data + |> Map.merge(act_data) + |> Map.put("object", Map.merge(object_data, obj_data)) + |> Pleroma.Web.ActivityPub.ActivityPub.insert(false) end - defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do + defp user_mentions(users) do user_mentions = - get_random_mentions(friends, Enum.random(0..3)) ++ - get_random_mentions(non_friends, Enum.random(0..3)) + Enum.reduce( + @groups, + [], + fn group, acc -> + acc ++ get_random_mentions(users[group], Enum.random(0..2)) + end + ) - user_mentions = - if Enum.random([true, false]), - do: ["@" <> user.nickname | user_mentions], - else: user_mentions - - {:ok, _activity} = - group - |> get_actor(user, friends, non_friends) - |> CommonAPI.post(%{ - status: Enum.join(user_mentions, ", ") <> " simple status with mentions", - visibility: visibility - }) + if Enum.random([true, false]), + do: ["@" <> users[:user].nickname | user_mentions], + else: user_mentions end - defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do - mentions = - with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do - cached = - ([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10)) - |> Enum.map(&"@#{&1.nickname}") - |> Enum.join(", ") + defp hell_thread_mentions(users) do + with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do + cached = + @groups + |> Enum.reduce([users[:user]], fn group, acc -> + acc ++ Enum.take(users[group], 5) + end) + |> Enum.map(&"@#{&1.nickname}") + |> Enum.join(", ") - Cachex.put(:user_cache, "hell_thread_mentions", cached) - cached - else - {:ok, cached} -> cached - end - - {:ok, _activity} = - group - |> get_actor(user, friends, non_friends) - |> CommonAPI.post(%{ - status: mentions <> " hell thread status", - visibility: visibility - }) + Cachex.put(:user_cache, "hell_thread_mentions", cached) + cached + else + {:ok, cached} -> cached + end end - defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do - actor = get_actor(group, user, friends, non_friends) + defp insert_activity(:simple, visibility, group, users, _opts) + when group in @remote_groups do + insert_remote_activity(visibility, group, users, "Remote status") + end + + defp insert_activity(:simple, visibility, group, users, _opts) do + insert_local_activity(visibility, group, users, "Simple status") + end + + defp insert_activity(:emoji, visibility, group, users, _opts) + when group in @remote_groups do + insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:") + end + + defp insert_activity(:emoji, visibility, group, users, _opts) do + insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:") + end + + defp insert_activity(:mentions, visibility, group, users, _opts) + when group in @remote_groups do + mentions = user_mentions(users) + + status = Enum.join(mentions, ", ") <> " remote status with mentions" + + insert_remote_activity(visibility, group, users, status) + end + + defp insert_activity(:mentions, visibility, group, users, _opts) do + mentions = user_mentions(users) + + status = Enum.join(mentions, ", ") <> " simple status with mentions" + insert_remote_activity(visibility, group, users, status) + end + + defp insert_activity(:hell_thread, visibility, group, users, _) + when group in @remote_groups do + mentions = hell_thread_mentions(users) + insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status") + end + + defp insert_activity(:hell_thread, visibility, group, users, _opts) do + mentions = hell_thread_mentions(users) + + insert_local_activity(visibility, group, users, mentions <> " hell thread status") + end + + defp insert_activity(:attachment, visibility, group, users, _opts) do + actor = get_actor(group, users) obj_data = %{ "actor" => actor.ap_id, @@ -268,67 +306,54 @@ defp insert_activity("attachment", visibility, group, user, friends, non_friends }) end - defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do - {:ok, _activity} = - group - |> get_actor(user, friends, non_friends) - |> CommonAPI.post(%{status: "Status with #tag", visibility: visibility}) + defp insert_activity(:tag, visibility, group, users, _opts) do + insert_local_activity(visibility, group, users, "Status with #tag") end - defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do - actor = get_actor(group, user, friends, non_friends) + defp insert_activity(:like, visibility, group, users, opts) do + actor = get_actor(group, users) with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do :ok else {:error, _} -> - insert_activity("like", visibility, group, user, friends, non_friends, opts) + insert_activity(:like, visibility, group, users, opts) nil -> Process.sleep(15) - insert_activity("like", visibility, group, user, friends, non_friends, opts) + insert_activity(:like, visibility, group, users, opts) end end - defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do - actor = get_actor(group, user, friends, non_friends) + defp insert_activity(:reblog, visibility, group, users, opts) do + actor = get_actor(group, users) with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), - {:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do + {:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do :ok else {:error, _} -> - insert_activity("reblog", visibility, group, user, friends, non_friends, opts) + insert_activity(:reblog, visibility, group, users, opts) nil -> Process.sleep(15) - insert_activity("reblog", visibility, group, user, friends, non_friends, opts) + insert_activity(:reblog, visibility, group, users, opts) end end - defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts) - when visibility in ["public", "unlisted", "private"] do - actor = get_actor(group, user, friends, non_friends) - tasks = get_reply_tasks(visibility, group) - - {:ok, activity} = CommonAPI.post(user, %{status: "Simple status", visibility: visibility}) - - acc = {activity.id, ["@" <> actor.nickname, "reply to status"]} - insert_replies(tasks, visibility, user, friends, non_friends, acc) - end - - defp insert_activity("simple_thread", "direct", group, user, friends, non_friends, _opts) do - actor = get_actor(group, user, friends, non_friends) + defp insert_activity(:simple_thread, "direct", group, users, _opts) do + actor = get_actor(group, users) tasks = get_reply_tasks("direct", group) list = case group do - "non_friends" -> - Enum.take(non_friends, 3) + :user -> + group = Enum.random(@friends_groups) + Enum.take(users[group], 3) _ -> - Enum.take(friends, 3) + Enum.take(users[group], 3) end data = Enum.map(list, &("@" <> &1.nickname)) @@ -339,40 +364,30 @@ defp insert_activity("simple_thread", "direct", group, user, friends, non_friend visibility: "direct" }) - acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]} - insert_direct_replies(tasks, user, list, acc) + acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]} + insert_direct_replies(tasks, users[:user], list, acc) end - defp insert_activity("remote", _, "user", _, _, _, _), do: :ok + defp insert_activity(:simple_thread, visibility, group, users, _opts) do + actor = get_actor(group, users) + tasks = get_reply_tasks(visibility, group) - defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do - remote_friends = - Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true) + {:ok, activity} = + CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility}) - remote_non_friends = - Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false) - - actor = get_actor(group, user, remote_friends, remote_non_friends) - - {act_data, obj_data} = prepare_activity_data(actor, visibility, user) - {activity_data, object_data} = other_data(actor) - - activity_data - |> Map.merge(act_data) - |> Map.put("object", Map.merge(object_data, obj_data)) - |> Pleroma.Web.ActivityPub.ActivityPub.insert(false) + acc = {activity.id, ["@" <> actor.nickname, "reply to status"]} + insert_replies(tasks, visibility, users, acc) end - defp get_actor("user", user, _friends, _non_friends), do: user - defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends) - defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends) + defp get_actor(:user, %{user: user}), do: user + defp get_actor(group, users), do: Enum.random(users[group]) - defp other_data(actor) do + defp other_data(actor, content) do %{host: host} = URI.parse(actor.ap_id) datetime = DateTime.utc_now() - context_id = "http://#{host}:4000/contexts/#{UUID.generate()}" - activity_id = "http://#{host}:4000/activities/#{UUID.generate()}" - object_id = "http://#{host}:4000/objects/#{UUID.generate()}" + context_id = "https://#{host}/contexts/#{UUID.generate()}" + activity_id = "https://#{host}/activities/#{UUID.generate()}" + object_id = "https://#{host}/objects/#{UUID.generate()}" activity_data = %{ "actor" => actor.ap_id, @@ -389,7 +404,7 @@ defp other_data(actor) do "attributedTo" => actor.ap_id, "bcc" => [], "bto" => [], - "content" => "Remote post", + "content" => content, "context" => context_id, "conversation" => context_id, "emoji" => %{}, @@ -475,51 +490,65 @@ defp prepare_activity_data(_actor, "direct", mention) do {act_data, obj_data} end - defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user) - defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend) - defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend) + defp get_reply_tasks("public", :user) do + [:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user] + end - defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"], - do: ~w(friend user friend) + defp get_reply_tasks("public", group) when group in @friends_groups do + [:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote] + end - defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"], - do: ~w(user friend user) + defp get_reply_tasks("public", group) when group in @non_friends_groups do + [:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote] + end - defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"], - do: [] + defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do + [:friends_local, :friends_remote, :user, :friends_local, :friends_remote] + end - defp get_reply_tasks("direct", "user"), do: ~w(friend user friend) - defp get_reply_tasks("direct", "friends"), do: ~w(user friend user) - defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user) + defp get_reply_tasks(visibility, group) + when visibility in ["unlisted", "private"] and group in @friends_groups do + [:user, :friends_remote, :friends_local, :user] + end - defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do + defp get_reply_tasks(visibility, group) + when visibility in ["unlisted", "private"] and + group in @non_friends_groups, + do: [] + + defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote] + + defp get_reply_tasks("direct", group) when group in @friends_groups, + do: [:user, group, :user] + + defp get_reply_tasks("direct", group) when group in @non_friends_groups do + [:user, :non_friends_remote, :user, :non_friends_local] + end + + defp insert_replies(tasks, visibility, users, acc) do Enum.reduce(tasks, acc, fn - "friend", {id, data} -> - friend = Enum.random(friends) - insert_reply(friend, data, id, visibility) + :user, {id, data} -> + insert_reply(users[:user], data, id, visibility) - "non_friend", {id, data} -> - non_friend = Enum.random(non_friends) - insert_reply(non_friend, data, id, visibility) - - "user", {id, data} -> - insert_reply(user, data, id, visibility) + group, {id, data} -> + replier = Enum.random(users[group]) + insert_reply(replier, data, id, visibility) end) end defp insert_direct_replies(tasks, user, list, acc) do Enum.reduce(tasks, acc, fn - group, {id, data} when group in ["friend", "non_friend"] -> + :user, {id, data} -> + {reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct") + {reply_id, data} + + _, {id, data} -> actor = Enum.random(list) {reply_id, _} = insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct") {reply_id, data} - - "user", {id, data} -> - {reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct") - {reply_id, data} end) end diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index 0de4924bc..b278faf9f 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -36,6 +36,7 @@ defp fetch_timelines(user) do fetch_home_timeline(user) fetch_direct_timeline(user) fetch_public_timeline(user) + fetch_public_timeline(user, :with_blocks) fetch_public_timeline(user, :local) fetch_public_timeline(user, :tag) fetch_notifications(user) @@ -227,6 +228,76 @@ defp fetch_public_timeline(user, :only_media) do fetch_public_timeline(opts, "public timeline only media") end + # TODO: remove using `:method` after benchmarks + defp fetch_public_timeline(user, :with_blocks) do + opts = opts_for_public_timeline(user) + + remote_non_friends = Agent.get(:non_friends_remote, & &1) + + Benchee.run( + %{ + "public timeline without blocks" => fn opts -> + ActivityPub.fetch_public_activities(opts) + end + }, + inputs: %{ + "old filtering" => Map.delete(opts, :method), + "with psql fun" => Map.put(opts, :method, :fun), + "with unnest" => Map.put(opts, :method, :unnest) + } + ) + + Enum.each(remote_non_friends, fn non_friend -> + {:ok, _} = User.block(user, non_friend) + end) + + user = User.get_by_id(user.id) + + opts = Map.put(opts, "blocking_user", user) + + Benchee.run( + %{ + "public timeline with user block" => fn opts -> + ActivityPub.fetch_public_activities(opts) + end + }, + inputs: %{ + "old filtering" => Map.delete(opts, :method), + "with psql fun" => Map.put(opts, :method, :fun), + "with unnest" => Map.put(opts, :method, :unnest) + } + ) + + domains = + Enum.reduce(remote_non_friends, [], fn non_friend, domains -> + {:ok, _user} = User.unblock(user, non_friend) + %{host: host} = URI.parse(non_friend.ap_id) + [host | domains] + end) + + domains = Enum.uniq(domains) + + Enum.each(domains, fn domain -> + {:ok, _} = User.block_domain(user, domain) + end) + + user = User.get_by_id(user.id) + opts = Map.put(opts, "blocking_user", user) + + Benchee.run( + %{ + "public timeline with domain block" => fn opts -> + ActivityPub.fetch_public_activities(opts) + end + }, + inputs: %{ + "old filtering" => Map.delete(opts, :method), + "with psql fun" => Map.put(opts, :method, :fun), + "with unnest" => Map.put(opts, :method, :unnest) + } + ) + end + defp fetch_public_timeline(opts, title) when is_binary(title) do first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex index e4d0b22ff..6cf3958c1 100644 --- a/benchmarks/load_testing/users.ex +++ b/benchmarks/load_testing/users.ex @@ -27,7 +27,7 @@ def generate(opts \\ []) do make_friends(main_user, opts[:friends]) - Repo.get(User, main_user.id) + User.get_by_id(main_user.id) end def generate_users(max) do @@ -166,4 +166,24 @@ defp run_stream(users, main_user) do ) |> Stream.run() end + + @spec prepare_users(User.t(), keyword()) :: map() + def prepare_users(user, opts) do + friends_limit = opts[:friends_used] + non_friends_limit = opts[:non_friends_used] + + %{ + user: user, + friends_local: fetch_users(user, friends_limit, :local, true), + friends_remote: fetch_users(user, friends_limit, :external, true), + non_friends_local: fetch_users(user, non_friends_limit, :local, false), + non_friends_remote: fetch_users(user, non_friends_limit, :external, false) + } + end + + defp fetch_users(user, limit, local, friends?) do + user + |> get_users(limit: limit, local: local, friends?: friends?) + |> Enum.shuffle() + end end diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex index 657403202..1162b2e06 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex @@ -5,7 +5,6 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do import Ecto.Query alias Pleroma.Repo - alias Pleroma.Web.MastodonAPI.TimelineController def run(_args) do Mix.Pleroma.start_pleroma() @@ -37,7 +36,7 @@ def run(_args) do Benchee.run( %{ "Hashtag fetching, any" => fn tags -> - TimelineController.hashtag_fetching( + hashtag_fetching( %{ "any" => tags }, @@ -47,7 +46,7 @@ def run(_args) do end, # Will always return zero results because no overlapping hashtags are generated. "Hashtag fetching, all" => fn tags -> - TimelineController.hashtag_fetching( + hashtag_fetching( %{ "all" => tags }, @@ -67,7 +66,7 @@ def run(_args) do Benchee.run( %{ "Hashtag fetching" => fn tag -> - TimelineController.hashtag_fetching( + hashtag_fetching( %{ "tag" => tag }, @@ -80,4 +79,35 @@ def run(_args) do time: 5 ) end + + defp hashtag_fetching(params, user, local_only) do + tags = + [params["tag"], params["any"]] + |> List.flatten() + |> Enum.uniq() + |> Enum.filter(& &1) + |> Enum.map(&String.downcase(&1)) + + tag_all = + params + |> Map.get("all", []) + |> Enum.map(&String.downcase(&1)) + + tag_reject = + params + |> Map.get("none", []) + |> Enum.map(&String.downcase(&1)) + + _activities = + params + |> Map.put("type", "Create") + |> Map.put("local_only", local_only) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + |> Map.put("tag", tags) + |> Map.put("tag_all", tag_all) + |> Map.put("tag_reject", tag_reject) + |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index b8a2873d8..e7958f7a8 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -932,6 +932,33 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do query = if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) + # TODO: update after benchmarks + query = + case opts[:method] do + :fun -> + from(a in query, + where: + fragment( + "recipients_contain_blocked_domains(?, ?) = false", + a.recipients, + ^domain_blocks + ) + ) + + :unnest -> + from(a in query, + where: + fragment( + "NOT ? && (SELECT ARRAY(SELECT split_part(UNNEST(?), '/', 3)))", + ^domain_blocks, + a.recipients + ) + ) + + _ -> + query + end + from( [activity, object: o] in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index 8e19bace7..375b441a1 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -62,6 +62,13 @@ def public_operation do only_media_param(), with_muted_param(), exclude_visibilities_param(), + # TODO: remove after benchmarks + Operation.parameter( + :method, + :query, + %Schema{type: :string}, + "Temp parameter" + ), reply_visibility_param() | pagination_params() ], operationId: "TimelineController.public", diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 958567510..1734df4b5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -109,14 +109,23 @@ def public(%{assigns: %{user: user}} = conn, params) do if restrict? and is_nil(user) do render_error(conn, :unauthorized, "authorization required for timeline view") else - activities = + # TODO: return back after benchmarks + params = params |> Map.put("type", ["Create", "Announce"]) |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) |> Map.put("reply_filtering_user", user) - |> ActivityPub.fetch_public_activities() + + params = + if params["method"] do + Map.put(params, :method, String.to_existing_atom(params["method"])) + else + params + end + + activities = ActivityPub.fetch_public_activities(params) conn |> add_link_headers(activities, %{"local" => local_only}) diff --git a/priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs b/priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs new file mode 100644 index 000000000..14e873125 --- /dev/null +++ b/priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs @@ -0,0 +1,33 @@ +defmodule Pleroma.Repo.Migrations.AddRecipientsContainBlockedDomainsFunction do + use Ecto.Migration + @disable_ddl_transaction true + + def up do + statement = """ + CREATE OR REPLACE FUNCTION recipients_contain_blocked_domains(recipients varchar[], blocked_domains varchar[]) RETURNS boolean AS $$ + DECLARE + recipient_domain varchar; + recipient varchar; + BEGIN + FOREACH recipient IN ARRAY recipients LOOP + recipient_domain = split_part(recipient, '/', 3)::varchar; + + IF recipient_domain = ANY(blocked_domains) THEN + RETURN TRUE; + END IF; + END LOOP; + + RETURN FALSE; + END; + $$ LANGUAGE plpgsql; + """ + + execute(statement) + end + + def down do + execute( + "drop function if exists recipients_contain_blocked_domains(recipients varchar[], blocked_domains varchar[])" + ) + end +end diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 2375ac8e8..3474c0cf9 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -90,6 +90,74 @@ test "the public timeline includes only public statuses for an authenticated use res_conn = get(conn, "/api/v1/timelines/public") assert length(json_response_and_validate_schema(res_conn, 200)) == 1 end + + test "doesn't return replies if follower is posting with blocked user" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + [blockee, friend] = insert_list(2, :user) + {:ok, blocker} = User.follow(blocker, friend) + {:ok, _} = User.block(blocker, blockee) + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + res_conn = get(conn, "/api/v1/timelines/public") + [%{"id" => ^activity_id}] = json_response_and_validate_schema(res_conn, 200) + end + + # TODO: update after benchmarks + test "doesn't return replies if follow is posting with users from blocked domain" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + friend = insert(:user) + blockee = insert(:user, ap_id: "https://example.com/users/blocked") + {:ok, blocker} = User.follow(blocker, friend) + {:ok, blocker} = User.block_domain(blocker, "example.com") + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + res_conn = get(conn, "/api/v1/timelines/public?method=fun") + + activities = json_response_and_validate_schema(res_conn, 200) + [%{"id" => ^activity_id}] = activities + end + + # TODO: update after benchmarks + test "doesn't return replies if follow is posting with users from blocked domain with unnest param" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + friend = insert(:user) + blockee = insert(:user, ap_id: "https://example.com/users/blocked") + {:ok, blocker} = User.follow(blocker, friend) + {:ok, blocker} = User.block_domain(blocker, "example.com") + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + res_conn = get(conn, "/api/v1/timelines/public?method=unnest") + + activities = json_response_and_validate_schema(res_conn, 200) + [%{"id" => ^activity_id}] = activities + end end defp local_and_remote_activities do From 19f468c5bc230d6790b00aa87e509a07e709aaa7 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 2 Jun 2020 08:50:24 +0300 Subject: [PATCH 177/375] replies filtering for blocked domains --- benchmarks/load_testing/fetcher.ex | 30 ++++------------- lib/pleroma/web/activity_pub/activity_pub.ex | 33 ++++--------------- .../api_spec/operations/timeline_operation.ex | 7 ---- .../controllers/timeline_controller.ex | 13 ++------ .../controllers/timeline_controller_test.exs | 27 +-------------- 5 files changed, 15 insertions(+), 95 deletions(-) diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index b278faf9f..22a06e472 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -228,24 +228,16 @@ defp fetch_public_timeline(user, :only_media) do fetch_public_timeline(opts, "public timeline only media") end - # TODO: remove using `:method` after benchmarks defp fetch_public_timeline(user, :with_blocks) do opts = opts_for_public_timeline(user) remote_non_friends = Agent.get(:non_friends_remote, & &1) - Benchee.run( - %{ - "public timeline without blocks" => fn opts -> - ActivityPub.fetch_public_activities(opts) - end - }, - inputs: %{ - "old filtering" => Map.delete(opts, :method), - "with psql fun" => Map.put(opts, :method, :fun), - "with unnest" => Map.put(opts, :method, :unnest) - } - ) + Benchee.run(%{ + "public timeline without blocks" => fn -> + ActivityPub.fetch_public_activities(opts) + end + }) Enum.each(remote_non_friends, fn non_friend -> {:ok, _} = User.block(user, non_friend) @@ -257,15 +249,10 @@ defp fetch_public_timeline(user, :with_blocks) do Benchee.run( %{ - "public timeline with user block" => fn opts -> + "public timeline with user block" => fn -> ActivityPub.fetch_public_activities(opts) end }, - inputs: %{ - "old filtering" => Map.delete(opts, :method), - "with psql fun" => Map.put(opts, :method, :fun), - "with unnest" => Map.put(opts, :method, :unnest) - } ) domains = @@ -289,11 +276,6 @@ defp fetch_public_timeline(user, :with_blocks) do "public timeline with domain block" => fn opts -> ActivityPub.fetch_public_activities(opts) end - }, - inputs: %{ - "old filtering" => Map.delete(opts, :method), - "with psql fun" => Map.put(opts, :method, :fun), - "with unnest" => Map.put(opts, :method, :unnest) } ) end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e7958f7a8..673b10b22 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -932,37 +932,16 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do query = if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) - # TODO: update after benchmarks - query = - case opts[:method] do - :fun -> - from(a in query, - where: - fragment( - "recipients_contain_blocked_domains(?, ?) = false", - a.recipients, - ^domain_blocks - ) - ) - - :unnest -> - from(a in query, - where: - fragment( - "NOT ? && (SELECT ARRAY(SELECT split_part(UNNEST(?), '/', 3)))", - ^domain_blocks, - a.recipients - ) - ) - - _ -> - query - end - from( [activity, object: o] in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), + where: + fragment( + "recipients_contain_blocked_domains(?, ?) = false", + activity.recipients, + ^domain_blocks + ), where: fragment( "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index 375b441a1..8e19bace7 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -62,13 +62,6 @@ def public_operation do only_media_param(), with_muted_param(), exclude_visibilities_param(), - # TODO: remove after benchmarks - Operation.parameter( - :method, - :query, - %Schema{type: :string}, - "Temp parameter" - ), reply_visibility_param() | pagination_params() ], operationId: "TimelineController.public", diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 1734df4b5..958567510 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -109,23 +109,14 @@ def public(%{assigns: %{user: user}} = conn, params) do if restrict? and is_nil(user) do render_error(conn, :unauthorized, "authorization required for timeline view") else - # TODO: return back after benchmarks - params = + activities = params |> Map.put("type", ["Create", "Announce"]) |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) |> Map.put("reply_filtering_user", user) - - params = - if params["method"] do - Map.put(params, :method, String.to_existing_atom(params["method"])) - else - params - end - - activities = ActivityPub.fetch_public_activities(params) + |> ActivityPub.fetch_public_activities() conn |> add_link_headers(activities, %{"local" => local_only}) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 3474c0cf9..2ad6828ad 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -111,7 +111,6 @@ test "doesn't return replies if follower is posting with blocked user" do [%{"id" => ^activity_id}] = json_response_and_validate_schema(res_conn, 200) end - # TODO: update after benchmarks test "doesn't return replies if follow is posting with users from blocked domain" do %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) friend = insert(:user) @@ -129,31 +128,7 @@ test "doesn't return replies if follow is posting with users from blocked domain {:ok, _reply_from_friend} = CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - res_conn = get(conn, "/api/v1/timelines/public?method=fun") - - activities = json_response_and_validate_schema(res_conn, 200) - [%{"id" => ^activity_id}] = activities - end - - # TODO: update after benchmarks - test "doesn't return replies if follow is posting with users from blocked domain with unnest param" do - %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) - friend = insert(:user) - blockee = insert(:user, ap_id: "https://example.com/users/blocked") - {:ok, blocker} = User.follow(blocker, friend) - {:ok, blocker} = User.block_domain(blocker, "example.com") - - conn = assign(conn, :user, blocker) - - {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, reply_from_blockee} = - CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) - - {:ok, _reply_from_friend} = - CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - res_conn = get(conn, "/api/v1/timelines/public?method=unnest") + res_conn = get(conn, "/api/v1/timelines/public") activities = json_response_and_validate_schema(res_conn, 200) [%{"id" => ^activity_id}] = activities From 81fb45a71ba1d606e6a522ac746f3c7a7dd8136b Mon Sep 17 00:00:00 2001 From: Fristi Date: Mon, 1 Jun 2020 16:25:57 +0000 Subject: [PATCH 178/375] Translated using Weblate (Dutch) Currently translated at 29.2% (31 of 106 strings) Translation: Pleroma/Pleroma backend Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma/nl/ --- priv/gettext/nl/LC_MESSAGES/errors.po | 84 ++++++++++++++------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/priv/gettext/nl/LC_MESSAGES/errors.po b/priv/gettext/nl/LC_MESSAGES/errors.po index 7e12ff96c..3118f6b5d 100644 --- a/priv/gettext/nl/LC_MESSAGES/errors.po +++ b/priv/gettext/nl/LC_MESSAGES/errors.po @@ -3,14 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-05-15 09:37+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" +"PO-Revision-Date: 2020-06-02 07:36+0000\n" +"Last-Translator: Fristi \n" +"Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Translate Toolkit 2.5.1\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.0.4\n" ## This file is a PO Template file. ## @@ -23,142 +25,142 @@ msgstr "" ## effect: edit them in PO (`.po`) files instead. ## From Ecto.Changeset.cast/4 msgid "can't be blank" -msgstr "" +msgstr "kan niet leeg zijn" ## From Ecto.Changeset.unique_constraint/3 msgid "has already been taken" -msgstr "" +msgstr "is al bezet" ## From Ecto.Changeset.put_change/3 msgid "is invalid" -msgstr "" +msgstr "is ongeldig" ## From Ecto.Changeset.validate_format/3 msgid "has invalid format" -msgstr "" +msgstr "heeft een ongeldig formaat" ## From Ecto.Changeset.validate_subset/3 msgid "has an invalid entry" -msgstr "" +msgstr "heeft een ongeldige entry" ## From Ecto.Changeset.validate_exclusion/3 msgid "is reserved" -msgstr "" +msgstr "is gereserveerd" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" -msgstr "" +msgstr "komt niet overeen met bevestiging" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" -msgstr "" +msgstr "is nog geassocieerd met deze entry" msgid "are still associated with this entry" -msgstr "" +msgstr "zijn nog geassocieerd met deze entry" ## From Ecto.Changeset.validate_length/3 msgid "should be %{count} character(s)" msgid_plural "should be %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dient %{count} karakter te bevatten" +msgstr[1] "dient %{count} karakters te bevatten" msgid "should have %{count} item(s)" msgid_plural "should have %{count} item(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dient %{count} item te bevatten" +msgstr[1] "dient %{count} items te bevatten" msgid "should be at least %{count} character(s)" msgid_plural "should be at least %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dient ten minste %{count} karakter te bevatten" +msgstr[1] "dient ten minste %{count} karakters te bevatten" msgid "should have at least %{count} item(s)" msgid_plural "should have at least %{count} item(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dient ten minste %{count} item te bevatten" +msgstr[1] "dient ten minste %{count} items te bevatten" msgid "should be at most %{count} character(s)" msgid_plural "should be at most %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dient niet meer dan %{count} karakter te bevatten" +msgstr[1] "dient niet meer dan %{count} karakters te bevatten" msgid "should have at most %{count} item(s)" msgid_plural "should have at most %{count} item(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dient niet meer dan %{count} item te bevatten" +msgstr[1] "dient niet meer dan %{count} items te bevatten" ## From Ecto.Changeset.validate_number/3 msgid "must be less than %{number}" -msgstr "" +msgstr "dient kleiner te zijn dan %{number}" msgid "must be greater than %{number}" -msgstr "" +msgstr "dient groter te zijn dan %{number}" msgid "must be less than or equal to %{number}" -msgstr "" +msgstr "dient kleiner dan of gelijk te zijn aan %{number}" msgid "must be greater than or equal to %{number}" -msgstr "" +msgstr "dient groter dan of gelijk te zijn aan %{number}" msgid "must be equal to %{number}" -msgstr "" +msgstr "dient gelijk te zijn aan %{number}" #: lib/pleroma/web/common_api/common_api.ex:421 #, elixir-format msgid "Account not found" -msgstr "" +msgstr "Account niet gevonden" #: lib/pleroma/web/common_api/common_api.ex:249 #, elixir-format msgid "Already voted" -msgstr "" +msgstr "Al gestemd" #: lib/pleroma/web/oauth/oauth_controller.ex:360 #, elixir-format msgid "Bad request" -msgstr "" +msgstr "Bad request" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 #, elixir-format msgid "Can't delete object" -msgstr "" +msgstr "Object kan niet verwijderd worden" #: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196 #, elixir-format msgid "Can't delete this post" -msgstr "" +msgstr "Bericht kan niet verwijderd worden" #: lib/pleroma/web/controller_helper.ex:95 #: lib/pleroma/web/controller_helper.ex:101 #, elixir-format msgid "Can't display this activity" -msgstr "" +msgstr "Activiteit kan niet worden getoond" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254 #, elixir-format msgid "Can't find user" -msgstr "" +msgstr "Gebruiker kan niet gevonden worden" #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114 #, elixir-format msgid "Can't get favorites" -msgstr "" +msgstr "Favorieten konden niet opgehaald worden" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437 #, elixir-format msgid "Can't like object" -msgstr "" +msgstr "Object kan niet geliked worden" #: lib/pleroma/web/common_api/utils.ex:556 #, elixir-format msgid "Cannot post an empty status without attachments" -msgstr "" +msgstr "Status kan niet geplaatst worden zonder tekst of bijlagen" #: lib/pleroma/web/common_api/utils.ex:504 #, elixir-format msgid "Comment must be up to %{max_size} characters" -msgstr "" +msgstr "Opmerking dient maximaal %{max_size} karakters te bevatten" #: lib/pleroma/config/config_db.ex:222 #, elixir-format From 165a4b2a690ff7809ebbae65cddff3221d52489a Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 1 Jun 2020 22:18:20 +0300 Subject: [PATCH 179/375] Do not include activities of invisible users unless explicitly requested Closes #1833 --- lib/pleroma/user/query.ex | 6 +++--- lib/pleroma/web/activity_pub/activity_pub.ex | 12 ++++++++++++ lib/pleroma/web/admin_api/search.ex | 3 +-- test/tasks/relay_test.exs | 3 ++- .../controllers/admin_api_controller_test.exs | 13 +++++-------- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 293bbc082..66ffe9090 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -45,7 +45,7 @@ defmodule Pleroma.User.Query do is_admin: boolean(), is_moderator: boolean(), super_users: boolean(), - exclude_service_users: boolean(), + invisible: boolean(), followers: User.t(), friends: User.t(), recipients_from_activity: [String.t()], @@ -89,8 +89,8 @@ defp compose_query({key, value}, query) where(query, [u], ilike(field(u, ^key), ^"%#{value}%")) end - defp compose_query({:exclude_service_users, _}, query) do - where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch")) + defp compose_query({:invisible, bool}, query) when is_boolean(bool) do + where(query, [u], u.invisible == ^bool) end defp compose_query({key, value}, query) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index b8a2873d8..a38f9a3c8 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1030,6 +1030,17 @@ defp exclude_poll_votes(query, _) do end end + defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query + + defp exclude_invisible_actors(query, _opts) do + invisible_ap_ids = + User.Query.build(%{invisible: true, select: [:ap_id]}) + |> Repo.all() + |> Enum.map(fn %{ap_id: ap_id} -> ap_id end) + + from([activity] in query, where: activity.actor not in ^invisible_ap_ids) + end + defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do from(activity in query, where: activity.id != ^id) end @@ -1135,6 +1146,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_instance(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) + |> exclude_invisible_actors(opts) |> exclude_visibility(opts) end diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index c28efadd5..0bfb8f022 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -21,7 +21,7 @@ def user(params \\ %{}) do query = params |> Map.drop([:page, :page_size]) - |> Map.put(:exclude_service_users, true) + |> Map.put(:invisible, false) |> User.Query.build() |> order_by([u], u.nickname) @@ -31,7 +31,6 @@ def user(params \\ %{}) do count = Repo.aggregate(query, :count, :id) results = Repo.all(paginated_query) - {:ok, results, count} end end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index d3d88467d..678288854 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -65,7 +65,8 @@ test "relay is unfollowed" do "type" => "Undo", "actor_id" => follower_id, "limit" => 1, - "skip_preload" => true + "skip_preload" => true, + "invisible_actors" => true }) assert undo_activity.data["type"] == "Undo" diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index ead840186..193690469 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -757,8 +757,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do end test "pagination works correctly with service users", %{conn: conn} do - service1 = insert(:user, ap_id: Web.base_url() <> "/relay") - service2 = insert(:user, ap_id: Web.base_url() <> "/internal/fetch") + service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido") + insert_list(25, :user) assert %{"count" => 26, "page_size" => 10, "users" => users1} = @@ -767,8 +767,7 @@ test "pagination works correctly with service users", %{conn: conn} do |> json_response(200) assert Enum.count(users1) == 10 - assert service1 not in [users1] - assert service2 not in [users1] + assert service1 not in users1 assert %{"count" => 26, "page_size" => 10, "users" => users2} = conn @@ -776,8 +775,7 @@ test "pagination works correctly with service users", %{conn: conn} do |> json_response(200) assert Enum.count(users2) == 10 - assert service1 not in [users2] - assert service2 not in [users2] + assert service1 not in users2 assert %{"count" => 26, "page_size" => 10, "users" => users3} = conn @@ -785,8 +783,7 @@ test "pagination works correctly with service users", %{conn: conn} do |> json_response(200) assert Enum.count(users3) == 6 - assert service1 not in [users3] - assert service2 not in [users3] + assert service1 not in users3 end test "renders empty array for the second page", %{conn: conn} do From 805ab86933d90d4284c83e4a8ebfd6bf4b0395b3 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 2 Jun 2020 13:24:34 +0200 Subject: [PATCH 180/375] Notifications: Make notifications save their type. --- lib/pleroma/following_relationship.ex | 11 ++-- lib/pleroma/notification.ex | 61 ++++++++++++++++++- .../web/activity_pub/transmogrifier.ex | 3 + .../operations/notification_operation.ex | 1 + lib/pleroma/web/common_api/common_api.ex | 1 + .../mastodon_api/views/notification_view.ex | 20 +----- ...200602094828_add_type_to_notifications.exs | 9 +++ test/notification_test.exs | 11 +++- 8 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 priv/repo/migrations/20200602094828_add_type_to_notifications.exs diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 3a3082e72..0343a20d4 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -62,10 +62,13 @@ def update(%User{} = follower, %User{} = following, state) do follow(follower, following, state) following_relationship -> - following_relationship - |> cast(%{state: state}, [:state]) - |> validate_required([:state]) - |> Repo.update() + {:ok, relationship} = + following_relationship + |> cast(%{state: state}, [:state]) + |> validate_required([:state]) + |> Repo.update() + + {:ok, relationship} end end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index efafbce48..41ac53505 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -30,12 +30,26 @@ defmodule Pleroma.Notification do schema "notifications" do field(:seen, :boolean, default: false) + field(:type, :string) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) timestamps() end + def update_notification_type(user, activity) do + with %__MODULE__{} = notification <- + Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do + type = + activity + |> type_from_activity() + + notification + |> changeset(%{type: type}) + |> Repo.update() + end + end + @spec unread_notifications_count(User.t()) :: integer() def unread_notifications_count(%User{id: user_id}) do from(q in __MODULE__, @@ -46,7 +60,7 @@ def unread_notifications_count(%User{id: user_id}) do def changeset(%Notification{} = notification, attrs) do notification - |> cast(attrs, [:seen]) + |> cast(attrs, [:seen, :type]) end @spec last_read_query(User.t()) :: Ecto.Queryable.t() @@ -330,12 +344,55 @@ defp do_create_notifications(%Activity{} = activity) do {:ok, notifications} end + defp type_from_activity(%{data: %{"type" => type}} = activity) do + case type do + "Follow" -> + if Activity.follow_accepted?(activity) do + "follow" + else + "follow_request" + end + + "Announce" -> + "reblog" + + "Like" -> + "favourite" + + "Move" -> + "move" + + "EmojiReact" -> + "pleroma:emoji_reaction" + + "Create" -> + activity + |> type_from_activity_object() + + t -> + raise "No notification type for activity type #{t}" + end + end + + defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do + object = Object.normalize(activity, false) + + case object.data["type"] do + "ChatMessage" -> "pleroma:chat_mention" + _ -> "mention" + end + end + # TODO move to sql, too. def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do unless skip?(activity, user) do {:ok, %{notification: notification}} = Multi.new() - |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity}) + |> Multi.insert(:notification, %Notification{ + user_id: user.id, + activity: activity, + type: type_from_activity(activity) + }) |> Marker.multi_set_last_read_id(user, "notifications") |> Repo.transaction() diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4ac0d43fc..886403fcd 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Activity alias Pleroma.EarmarkRenderer alias Pleroma.FollowingRelationship + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -595,6 +596,8 @@ def handle_incoming( User.update_follower_count(followed) User.update_following_count(follower) + Notification.update_notification_type(followed, follow_activity) + ActivityPub.accept(%{ to: follow_activity.data["to"], type: "Accept", diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 46e72f8bf..c966b553a 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -185,6 +185,7 @@ defp notification_type do "mention", "poll", "pleroma:emoji_reaction", + "pleroma:chat_mention", "move", "follow_request" ], diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index e0987b1a7..5a194910d 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -121,6 +121,7 @@ def accept_follow_request(follower, followed) do object: follow_activity.data["id"], type: "Accept" }) do + Notification.update_notification_type(followed, follow_activity) {:ok, follower} end end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 07d55a3e9..c090be8ad 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -81,22 +81,6 @@ def render( end end - # This returns the notification type by activity, but both chats and statuses - # are in "Create" activities. - mastodon_type = - case Activity.mastodon_notification_type(activity) do - "mention" -> - object = Object.normalize(activity) - - case object do - %{data: %{"type" => "ChatMessage"}} -> "pleroma:chat_mention" - _ -> "mention" - end - - type -> - type - end - # Note: :relationships contain user mutes (needed for :muted flag in :status) status_render_opts = %{relationships: opts[:relationships]} @@ -107,7 +91,7 @@ def render( ) do response = %{ id: to_string(notification.id), - type: mastodon_type, + type: notification.type, created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), account: account, pleroma: %{ @@ -115,7 +99,7 @@ def render( } } - case mastodon_type do + case notification.type do "mention" -> put_status(response, activity, reading_user, status_render_opts) diff --git a/priv/repo/migrations/20200602094828_add_type_to_notifications.exs b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs new file mode 100644 index 000000000..19c733628 --- /dev/null +++ b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddTypeToNotifications do + use Ecto.Migration + + def change do + alter table(:notifications) do + add(:type, :string) + end + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs index 37c255fee..421b7fc40 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -31,6 +31,7 @@ test "creates a notification for an emoji reaction" do {:ok, [notification]} = Notification.create_notifications(activity) assert notification.user_id == user.id + assert notification.type == "pleroma:emoji_reaction" end test "notifies someone when they are directly addressed" do @@ -48,6 +49,7 @@ test "notifies someone when they are directly addressed" do notified_ids = Enum.sort([notification.user_id, other_notification.user_id]) assert notified_ids == [other_user.id, third_user.id] assert notification.activity_id == activity.id + assert notification.type == "mention" assert other_notification.activity_id == activity.id assert [%Pleroma.Marker{unread_count: 2}] = @@ -335,9 +337,12 @@ test "it creates `follow_request` notification for pending Follow activity" do # After request is accepted, the same notification is rendered with type "follow": assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) - notification_id = notification.id - assert [%{id: ^notification_id}] = Notification.for_user(followed_user) - assert %{type: "follow"} = NotificationView.render("show.json", render_opts) + notification = + Repo.get(Notification, notification.id) + |> Repo.preload(:activity) + + assert %{type: "follow"} = + NotificationView.render("show.json", notification: notification, for: followed_user) end test "it doesn't create a notification for follow-unfollow-follow chains" do From 127ccc4e1c76c2782b26a0cfbb154bc1317f31b3 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 2 Jun 2020 14:05:53 +0200 Subject: [PATCH 181/375] NotificationController: Don't return chat_mentions by default. --- docs/API/chats.md | 2 +- .../controllers/notification_controller.ex | 14 ++++++++++++- lib/pleroma/web/mastodon_api/mastodon_api.ex | 15 ++----------- .../notification_controller_test.exs | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 2eca5adf6..d1d39f495 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -204,7 +204,7 @@ Returned data is the deleted message. ### Notifications -There's a new `pleroma:chat_mention` notification, which has this form: +There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`: ```json { diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index bcd12c73f..e25cef30b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -42,8 +42,20 @@ def index(conn, %{account_id: account_id} = params) do end end + @default_notification_types ~w{ + mention + follow + follow_request + reblog + favourite + move + pleroma:emoji_reaction + } def index(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {k, v} -> {to_string(k), v} end) + params = + Map.new(params, fn {k, v} -> {to_string(k), v} end) + |> Map.put_new("include_types", @default_notification_types) + notifications = MastodonAPI.get_notifications(user, params) conn diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 70da64a7a..694bf5ca8 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do import Ecto.Query import Ecto.Changeset - alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.Pagination alias Pleroma.ScheduledActivity @@ -82,15 +81,11 @@ defp cast_params(params) do end defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do - ap_types = convert_and_filter_mastodon_types(mastodon_types) - - where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) + where(query, [n], n.type in ^mastodon_types) 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)) + where(query, [n], n.type not in ^mastodon_types) end defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do @@ -98,10 +93,4 @@ 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/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index e278d61f5..698c99711 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -54,6 +54,27 @@ test "list of notifications" do assert response == expected_response end + test "by default, does not contain pleroma:chat_mention" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + result = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert [] == result + + result = + conn + |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention") + |> json_response_and_validate_schema(200) + + assert [_] = result + end + test "getting a single notification" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) From 37542a9dfa99cc4324f211b45254acea758ac1ae Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 2 Jun 2020 14:22:16 +0200 Subject: [PATCH 182/375] Activity: Remove notifications-related functions. --- lib/pleroma/activity.ex | 36 ------------------- .../mastodon_api/views/notification_view.ex | 15 ++++---- lib/pleroma/web/push/impl.ex | 14 +++----- test/web/push/impl_test.exs | 25 +++++++------ 4 files changed, 26 insertions(+), 64 deletions(-) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 6213d0eb7..da1be20b3 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -24,16 +24,6 @@ defmodule Pleroma.Activity do @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} - # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 - @mastodon_notification_types %{ - "Create" => "mention", - "Follow" => ["follow", "follow_request"], - "Announce" => "reblog", - "Like" => "favourite", - "Move" => "move", - "EmojiReact" => "pleroma:emoji_reaction" - } - schema "activities" do field(:data, :map) field(:local, :boolean, default: true) @@ -300,32 +290,6 @@ def follow_accepted?( def follow_accepted?(_), do: false - @spec mastodon_notification_type(Activity.t()) :: String.t() | nil - - for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do - def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}), - do: unquote(type) - end - - def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do - if follow_accepted?(activity) do - "follow" - else - "follow_request" - end - end - - def mastodon_notification_type(%Activity{}), do: nil - - @spec from_mastodon_notification_type(String.t()) :: String.t() | nil - @doc "Converts Mastodon notification type to AR activity type" - def from_mastodon_notification_type(type) do - with {k, _v} <- - Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do - k - end - end - def all_by_actor_and_id(actor, status_ids \\ []) def all_by_actor_and_id(_actor, []), do: [] diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index c090be8ad..af15bba48 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -16,18 +16,17 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.PleromaAPI.ChatMessageView + @parent_types ~w{Like Announce EmojiReact} + def render("index.json", %{notifications: notifications, for: reading_user} = opts) do activities = Enum.map(notifications, & &1.activity) parent_activities = activities - |> Enum.filter( - &(Activity.mastodon_notification_type(&1) in [ - "favourite", - "reblog", - "pleroma:emoji_reaction" - ]) - ) + |> Enum.filter(fn + %{data: %{"type" => type}} -> + type in @parent_types + end) |> Enum.map(& &1.data["object"]) |> Activity.create_by_object_ap_id() |> Activity.with_preloaded_object(:left) @@ -44,7 +43,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op true -> move_activities_targets = activities - |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) + |> Enum.filter(&(&1.data["type"] == "Move")) |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) actors = diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 691725702..125f33755 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -16,8 +16,6 @@ defmodule Pleroma.Web.Push.Impl do require Logger import Ecto.Query - defdelegate mastodon_notification_type(activity), to: Activity - @types ["Create", "Follow", "Announce", "Like", "Move"] @doc "Performs sending notifications for user subscriptions" @@ -31,7 +29,7 @@ def perform( when activity_type in @types do actor = User.get_cached_by_ap_id(notification.activity.data["actor"]) - mastodon_type = mastodon_notification_type(notification.activity) + mastodon_type = notification.type gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) avatar_url = User.avatar_url(actor) object = Object.normalize(activity) @@ -116,7 +114,7 @@ def build_content( end def build_content(notification, actor, object, mastodon_type) do - mastodon_type = mastodon_type || mastodon_notification_type(notification.activity) + mastodon_type = mastodon_type || notification.type %{ title: format_title(notification, mastodon_type), @@ -151,7 +149,7 @@ def format_body( mastodon_type ) when type in ["Follow", "Like"] do - mastodon_type = mastodon_type || mastodon_notification_type(notification.activity) + mastodon_type = mastodon_type || notification.type case mastodon_type do "follow" -> "@#{actor.nickname} has followed you" @@ -166,10 +164,8 @@ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_typ "New Direct Message" end - def format_title(%{activity: activity}, mastodon_type) do - mastodon_type = mastodon_type || mastodon_notification_type(activity) - - case mastodon_type do + def format_title(%{type: type}, mastodon_type) do + case mastodon_type || type do "mention" -> "New Mention" "follow" -> "New Follower" "follow_request" -> "New Follow Request" diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index a826b24c9..26c65bc82 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -60,7 +60,8 @@ test "performs sending notifications" do notif = insert(:notification, user: user, - activity: activity + activity: activity, + type: "mention" ) assert Impl.perform(notif) == {:ok, [:ok, :ok]} @@ -126,7 +127,7 @@ test "renders title and body for create activity" do ) == "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - assert Impl.format_title(%{activity: activity}) == + assert Impl.format_title(%{activity: activity, type: "mention"}) == "New Mention" end @@ -136,9 +137,10 @@ test "renders title and body for follow activity" do {:ok, _, _, activity} = CommonAPI.follow(user, other_user) object = Object.normalize(activity, false) - assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you" + assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) == + "@Bob has followed you" - assert Impl.format_title(%{activity: activity}) == + assert Impl.format_title(%{activity: activity, type: "follow"}) == "New Follower" end @@ -157,7 +159,7 @@ test "renders title and body for announce activity" do assert Impl.format_body(%{activity: announce_activity}, user, object) == "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - assert Impl.format_title(%{activity: announce_activity}) == + assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) == "New Repeat" end @@ -173,9 +175,10 @@ test "renders title and body for like activity" do {:ok, activity} = CommonAPI.favorite(user, activity.id) object = Object.normalize(activity) - assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post" + assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) == + "@Bob has favorited your post" - assert Impl.format_title(%{activity: activity}) == + assert Impl.format_title(%{activity: activity, type: "favourite"}) == "New Favorite" end @@ -218,7 +221,7 @@ test "hides details for notifications when privacy option enabled" do status: "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." }) - notif = insert(:notification, user: user2, activity: activity) + notif = insert(:notification, user: user2, activity: activity, type: "mention") actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) object = Object.normalize(activity) @@ -281,7 +284,7 @@ test "returns regular content for notifications with privacy option disabled" do {:ok, activity} = CommonAPI.favorite(user, activity.id) - notif = insert(:notification, user: user2, activity: activity) + notif = insert(:notification, user: user2, activity: activity, type: "favourite") actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) object = Object.normalize(activity) From 38dce485c47e9315663c5c9cfd67dab4164b1bbe Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 2 Jun 2020 14:49:56 +0200 Subject: [PATCH 183/375] Notification: Add function to backfill notification types --- lib/pleroma/notification.ex | 20 ++++++++++++++++++++ test/notification_test.exs | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 41ac53505..c8b964400 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -37,6 +37,26 @@ defmodule Pleroma.Notification do timestamps() end + def fill_in_notification_types() do + query = + from(n in __MODULE__, + where: is_nil(n.type), + preload: :activity + ) + + query + |> Repo.all() + |> Enum.each(fn notification -> + type = + notification.activity + |> type_from_activity() + + notification + |> changeset(%{type: type}) + |> Repo.update() + end) + end + def update_notification_type(user, activity) do with %__MODULE__{} = notification <- Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do diff --git a/test/notification_test.exs b/test/notification_test.exs index 421b7fc40..6bc2b6904 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -20,6 +20,34 @@ defmodule Pleroma.NotificationTest do alias Pleroma.Web.Push alias Pleroma.Web.Streamer + describe "fill_in_notification_types" do + test "it fills in missing notification types" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) + {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") + {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + {:ok, like} = CommonAPI.favorite(other_user, post.id) + + assert {4, nil} = Repo.update_all(Notification, set: [type: nil]) + + Notification.fill_in_notification_types() + + assert %{type: "mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) + + assert %{type: "favourite"} = + Repo.get_by(Notification, user_id: user.id, activity_id: like.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react.id) + + assert %{type: "pleroma:chat_mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) + end + end + describe "create_notifications" do test "creates a notification for an emoji reaction" do user = insert(:user) From 6cd2fa2a4cbffaaab7c911f1051d4917e8a06c78 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 2 Jun 2020 15:13:19 +0200 Subject: [PATCH 184/375] Migrations: Add a migration to backfill notification types. --- lib/pleroma/notification.ex | 23 +++++++++++++++---- ...0602125218_backfill_notification_types.exs | 10 ++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 priv/repo/migrations/20200602125218_backfill_notification_types.exs diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index c8b964400..d89ee4645 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -49,7 +49,7 @@ def fill_in_notification_types() do |> Enum.each(fn notification -> type = notification.activity - |> type_from_activity() + |> type_from_activity(no_cachex: true) notification |> changeset(%{type: type}) @@ -364,10 +364,23 @@ defp do_create_notifications(%Activity{} = activity) do {:ok, notifications} end - defp type_from_activity(%{data: %{"type" => type}} = activity) do + defp type_from_activity(%{data: %{"type" => type}} = activity, opts \\ []) do case type do "Follow" -> - if Activity.follow_accepted?(activity) do + accepted_function = + if Keyword.get(opts, :no_cachex, false) do + # A special function to make this usable in a migration. + fn activity -> + with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), + %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do + Pleroma.FollowingRelationship.following?(follower, followed) + end + end + else + &Activity.follow_accepted?/1 + end + + if accepted_function.(activity) do "follow" else "follow_request" @@ -394,8 +407,10 @@ defp type_from_activity(%{data: %{"type" => type}} = activity) do end end + defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention" + defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do - object = Object.normalize(activity, false) + object = Object.get_by_ap_id(activity.data["object"]) case object.data["type"] do "ChatMessage" -> "pleroma:chat_mention" diff --git a/priv/repo/migrations/20200602125218_backfill_notification_types.exs b/priv/repo/migrations/20200602125218_backfill_notification_types.exs new file mode 100644 index 000000000..493c0280c --- /dev/null +++ b/priv/repo/migrations/20200602125218_backfill_notification_types.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do + use Ecto.Migration + + def up do + Pleroma.Notification.fill_in_notification_types() + end + + def down do + end +end From 2c6ebe709a9fb84bedb5d50c24715fd4532272f9 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 2 Jun 2020 15:14:52 +0200 Subject: [PATCH 185/375] Credo fixes --- lib/pleroma/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d89ee4645..0f33d282d 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -37,7 +37,7 @@ defmodule Pleroma.Notification do timestamps() end - def fill_in_notification_types() do + def fill_in_notification_types do query = from(n in __MODULE__, where: is_nil(n.type), From 7922e63825e2e25ccb52ae6e0a6c0011207a598d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 2 Jun 2020 19:07:17 +0400 Subject: [PATCH 186/375] Update OpenAPI spec for AdminAPI.StatusController --- lib/pleroma/web/admin_api/controllers/status_controller.ex | 4 +--- lib/pleroma/web/api_spec/operations/admin/status_operation.ex | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index c91fbc771..574196be8 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -41,9 +41,7 @@ def index(%{assigns: %{user: _admin}} = conn, params) do def show(conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id) do - conn - |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("show.json", %{activity: activity}) + render(conn, "show.json", %{activity: activity}) else nil -> {:error, :not_found} end diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index 0b138dc79..2947e6b34 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -74,7 +74,7 @@ def show_operation do parameters: [id_param()], security: [%{"oAuth" => ["read:statuses"]}], responses: %{ - 200 => Operation.response("Status", "application/json", Status), + 200 => Operation.response("Status", "application/json", status()), 404 => Operation.response("Not Found", "application/json", ApiError) } } From 030240ee8f80472c8dab0c1f9bb2f30f4271272f Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Wed, 3 Jun 2020 04:36:09 +0000 Subject: [PATCH 187/375] docs: clients.md: Add Husky --- docs/clients.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/clients.md b/docs/clients.md index 7f98dc7b1..ea751637e 100644 --- a/docs/clients.md +++ b/docs/clients.md @@ -42,6 +42,12 @@ Feel free to contact us to be added to this list! - Platforms: SailfishOS - Features: No Streaming +### Husky +- Source code: +- Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky) +- Platforms: Android +- Features: No Streaming, Emoji Reactions, Text Formatting, FE Stickers + ### Nekonium - Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/) - Source: From aa22fce8f46cf2e7f871b3584fbfff7ac2ebe4c2 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 12:30:12 +0200 Subject: [PATCH 188/375] ChatMessageReference: Introduce and switch in chat controller. --- lib/pleroma/chat.ex | 5 ++ lib/pleroma/chat_message_reference.ex | 80 +++++++++++++++++++ lib/pleroma/web/activity_pub/side_effects.ex | 9 ++- .../controllers/chat_controller.ex | 60 ++++++++------ ...view.ex => chat_message_reference_view.ex} | 21 +++-- ...02150528_create_chat_message_reference.exs | 20 +++++ test/web/activity_pub/side_effects_test.exs | 13 ++- .../controllers/chat_controller_test.exs | 22 +++-- ...s => chat_message_reference_view_test.exs} | 20 +++-- 9 files changed, 205 insertions(+), 45 deletions(-) create mode 100644 lib/pleroma/chat_message_reference.ex rename lib/pleroma/web/pleroma_api/views/{chat_message_view.ex => chat_message_reference_view.ex} (67%) create mode 100644 priv/repo/migrations/20200602150528_create_chat_message_reference.exs rename test/web/pleroma_api/views/{chat_message_view_test.exs => chat_message_reference_view_test.exs} (67%) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 4c92a58c7..211b872f9 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -72,6 +72,11 @@ def creation_cng(struct, params) do |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) end + def get_by_id(id) do + __MODULE__ + |> Repo.get(id) + end + def get(user_id, recipient) do __MODULE__ |> Repo.get_by(user_id: user_id, recipient: recipient) diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat_message_reference.ex new file mode 100644 index 000000000..e9ca3dfe8 --- /dev/null +++ b/lib/pleroma/chat_message_reference.ex @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ChatMessageReference do + @moduledoc """ + A reference that builds a relation between an AP chat message that a user can see and whether it has been seen + by them, or should be displayed to them. Used to build the chat view that is presented to the user. + """ + + use Ecto.Schema + + alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Repo + + import Ecto.Changeset + import Ecto.Query + + @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + + schema "chat_message_references" do + belongs_to(:object, Object) + belongs_to(:chat, Chat) + + field(:seen, :boolean, default: false) + + timestamps() + end + + def changeset(struct, params) do + struct + |> cast(params, [:object_id, :chat_id, :seen]) + |> validate_required([:object_id, :chat_id, :seen]) + end + + def get_by_id(id) do + __MODULE__ + |> Repo.get(id) + |> Repo.preload(:object) + end + + def delete(cm_ref) do + cm_ref + |> Repo.delete() + end + + def delete_for_object(%{id: object_id}) do + from(cr in __MODULE__, + where: cr.object_id == ^object_id + ) + |> Repo.delete_all() + end + + def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do + __MODULE__ + |> Repo.get_by(chat_id: chat_id, object_id: object_id) + |> Repo.preload(:object) + end + + def for_chat_query(chat) do + from(cr in __MODULE__, + where: cr.chat_id == ^chat.id, + order_by: [desc: :id], + preload: [:object] + ) + end + + def create(chat, object, seen) do + params = %{ + chat_id: chat.id, + object_id: object.id, + seen: seen + } + + %__MODULE__{} + |> changeset(params) + |> Repo.insert() + end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index a34bf6a05..cda52b00e 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do """ alias Pleroma.Activity alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -104,6 +105,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, Object.decrease_replies_count(in_reply_to) end + ChatMessageReference.delete_for_object(deleted_object) + ActivityPub.stream_out(object) ActivityPub.stream_out_participations(deleted_object, user) :ok @@ -137,9 +140,11 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do |> Enum.each(fn [user, other_user] -> if user.local do if user.ap_id == actor.ap_id do - Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + ChatMessageReference.create(chat, object, true) else - Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + ChatMessageReference.create(chat, object, false) end end end) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 210c8ec4a..c54681054 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -6,14 +6,15 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Activity alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Object alias Pleroma.Pagination alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.ChatMessageView alias Pleroma.Web.PleromaAPI.ChatView + alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView import Ecto.Query import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] @@ -35,28 +36,38 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation - def delete_message(%{assigns: %{user: %{ap_id: actor} = user}} = conn, %{ - message_id: id + def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ + message_id: message_id, + id: chat_id }) do - with %Object{ - data: %{ - "actor" => ^actor, - "id" => object, - "to" => [recipient], - "type" => "ChatMessage" - } - } = message <- Object.get_by_id(id), - %Chat{} = chat <- Chat.get(user.id, recipient), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object), - {:ok, _delete} <- CommonAPI.delete(activity.id, user) do + with %ChatMessageReference{} = cm_ref <- + ChatMessageReference.get_by_id(message_id), + ^chat_id <- cm_ref.chat_id |> to_string(), + %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), + {:ok, _} <- remove_or_delete(cm_ref, user) do conn - |> put_view(ChatMessageView) - |> render("show.json", for: user, object: message, chat: chat) + |> put_view(ChatMessageReferenceView) + |> render("show.json", chat_message_reference: cm_ref) else - _e -> {:error, :could_not_delete} + _e -> + {:error, :could_not_delete} end end + defp remove_or_delete( + %{object: %{data: %{"actor" => actor, "id" => id}}}, + %{ap_id: actor} = user + ) do + with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do + CommonAPI.delete(activity.id, user) + end + end + + defp remove_or_delete(cm_ref, _) do + cm_ref + |> ChatMessageReference.delete() + end + def post_chat_message( %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn, %{ @@ -69,10 +80,11 @@ def post_chat_message( CommonAPI.post_chat_message(user, recipient, params[:content], media_id: params[:media_id] ), - message <- Object.normalize(activity) do + message <- Object.normalize(activity, false), + cm_ref <- ChatMessageReference.for_chat_and_object(chat, message) do conn - |> put_view(ChatMessageView) - |> render("show.json", for: user, object: message, chat: chat) + |> put_view(ChatMessageReferenceView) + |> render("show.json", for: user, chat_message_reference: cm_ref) end end @@ -87,14 +99,14 @@ def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do - messages = + cm_refs = chat - |> Chat.messages_for_chat_query() + |> ChatMessageReference.for_chat_query() |> Pagination.fetch_paginated(params |> stringify_keys()) conn - |> put_view(ChatMessageView) - |> render("index.json", for: user, objects: messages, chat: chat) + |> put_view(ChatMessageReferenceView) + |> render("index.json", for: user, chat_message_references: cm_refs) else _ -> conn diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex similarity index 67% rename from lib/pleroma/web/pleroma_api/views/chat_message_view.ex rename to lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex index b088a8734..ff170e162 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex @@ -2,10 +2,9 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatMessageView do +defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceView do use Pleroma.Web, :view - alias Pleroma.Chat alias Pleroma.User alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.StatusView @@ -13,8 +12,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageView do def render( "show.json", %{ - object: %{id: id, data: %{"type" => "ChatMessage"} = chat_message}, - chat: %Chat{id: chat_id} + chat_message_reference: %{ + id: id, + object: %{data: chat_message}, + chat_id: chat_id, + seen: seen + } } ) do %{ @@ -26,11 +29,17 @@ def render( emojis: StatusView.build_emojis(chat_message["emoji"]), attachment: chat_message["attachment"] && - StatusView.render("attachment.json", attachment: chat_message["attachment"]) + StatusView.render("attachment.json", attachment: chat_message["attachment"]), + seen: seen } end def render("index.json", opts) do - render_many(opts[:objects], __MODULE__, "show.json", Map.put(opts, :as, :object)) + render_many( + opts[:chat_message_references], + __MODULE__, + "show.json", + Map.put(opts, :as, :chat_message_reference) + ) end end diff --git a/priv/repo/migrations/20200602150528_create_chat_message_reference.exs b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs new file mode 100644 index 000000000..6f9148b7c --- /dev/null +++ b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.CreateChatMessageReference do + use Ecto.Migration + + def change do + create table(:chat_message_references, primary_key: false) do + add(:id, :uuid, primary_key: true) + add(:chat_id, references(:chats, on_delete: :delete_all), null: false) + add(:object_id, references(:objects, on_delete: :delete_all), null: false) + add(:seen, :boolean, default: false, null: false) + + timestamps() + end + + create(index(:chat_message_references, [:chat_id, "id desc"])) + end +end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 210ba6ef0..ff6b3ac15 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do alias Pleroma.Activity alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -330,7 +331,7 @@ test "it streams the created ChatMessage" do end end - test "it creates a Chat for the local users and bumps the unread count, except for the author" do + test "it creates a Chat and ChatMessageReferences for the local users and bumps the unread count, except for the author" do author = insert(:user, local: true) recipient = insert(:user, local: true) @@ -347,8 +348,18 @@ test "it creates a Chat for the local users and bumps the unread count, except f chat = Chat.get(author.id, recipient.ap_id) assert chat.unread == 0 + [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.seen == true + chat = Chat.get(recipient.id, author.ap_id) assert chat.unread == 1 + + [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.seen == false end test "it creates a Chat for the local users and bumps the unread count" do diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index d79aa3148..bd4024c09 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -95,7 +96,7 @@ test "it works with an attachment", %{conn: conn, user: user} do describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do setup do: oauth_access(["write:statuses"]) - test "it deletes a message for the author of the message", %{conn: conn, user: user} do + test "it deletes a message from the chat", %{conn: conn, user: user} do recipient = insert(:user) {:ok, message} = @@ -107,23 +108,32 @@ test "it deletes a message for the author of the message", %{conn: conn, user: u chat = Chat.get(user.id, recipient.ap_id) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + + # Deleting your own message removes the message and the reference result = conn |> put_req_header("content-type", "application/json") - |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{object.id}") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") |> json_response_and_validate_schema(200) - assert result["id"] == to_string(object.id) + assert result["id"] == cm_ref.id + refute ChatMessageReference.get_by_id(cm_ref.id) + assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) + # Deleting other people's messages just removes the reference object = Object.normalize(other_message, false) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) result = conn |> put_req_header("content-type", "application/json") - |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{object.id}") - |> json_response(400) + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) - assert result == %{"error" => "could_not_delete"} + assert result["id"] == cm_ref.id + refute ChatMessageReference.get_by_id(cm_ref.id) + assert Object.get_by_id(object.id) end end diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_reference_view_test.exs similarity index 67% rename from test/web/pleroma_api/views/chat_message_view_test.exs rename to test/web/pleroma_api/views/chat_message_reference_view_test.exs index d7a2d10a5..00024d52c 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -2,14 +2,15 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do +defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do use Pleroma.DataCase alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView import Pleroma.Factory @@ -30,25 +31,32 @@ test "it displays a chat message" do object = Object.normalize(activity) - chat_message = ChatMessageView.render("show.json", object: object, for: user, chat: chat) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert chat_message[:id] == object.id |> to_string() + chat_message = ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message[:id] == cm_ref.id assert chat_message[:content] == "kippis :firefox:" assert chat_message[:account_id] == user.id assert chat_message[:chat_id] assert chat_message[:created_at] + assert chat_message[:seen] == true assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id) object = Object.normalize(activity) - chat_message_two = ChatMessageView.render("show.json", object: object, for: user, chat: chat) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert chat_message_two[:id] == object.id |> to_string() + chat_message_two = + ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message_two[:id] == cm_ref.id assert chat_message_two[:content] == "gkgkgk" assert chat_message_two[:account_id] == recipient.id assert chat_message_two[:chat_id] == chat_message[:chat_id] assert chat_message_two[:attachment] + assert chat_message_two[:seen] == false end end From f3ccd50a33c9eec3661bf2116fe38542f04986aa Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 12:49:53 +0200 Subject: [PATCH 189/375] ChatMessageReferences: Adjust views --- lib/pleroma/chat_message_reference.ex | 7 +++++++ lib/pleroma/web/mastodon_api/views/notification_view.ex | 8 +++++--- lib/pleroma/web/pleroma_api/views/chat_view.ex | 8 +++++--- lib/pleroma/web/views/streamer_view.ex | 8 +++++++- test/web/mastodon_api/views/notification_view_test.exs | 7 +++++-- test/web/pleroma_api/views/chat_view_test.exs | 7 +++++-- 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat_message_reference.ex index e9ca3dfe8..6808d1365 100644 --- a/lib/pleroma/chat_message_reference.ex +++ b/lib/pleroma/chat_message_reference.ex @@ -66,6 +66,13 @@ def for_chat_query(chat) do ) end + def last_message_for_chat(chat) do + chat + |> for_chat_query() + |> limit(1) + |> Repo.one() + end + def create(chat, object, seen) do params = %{ chat_id: chat.id, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index af15bba48..2ae82eb2d 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.ChatMessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User @@ -14,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView @parent_types ~w{Like Announce EmojiReact} @@ -138,8 +139,9 @@ defp put_chat_message(response, activity, reading_user, opts) do object = Object.normalize(activity) author = User.get_cached_by_ap_id(object.data["actor"]) chat = Pleroma.Chat.get(reading_user.id, author.ap_id) - render_opts = Map.merge(opts, %{object: object, for: reading_user, chat: chat}) - chat_message_render = ChatMessageView.render("show.json", render_opts) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref}) + chat_message_render = ChatMessageReferenceView.render("show.json", render_opts) Map.put(response, :chat_message, chat_message_render) end diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 223b64987..331c1d282 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -6,22 +6,24 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do use Pleroma.Web, :view alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.User alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView def render("show.json", %{chat: %Chat{} = chat} = opts) do recipient = User.get_cached_by_ap_id(chat.recipient) - last_message = opts[:message] || Chat.last_message_for_chat(chat) + last_message = opts[:last_message] || ChatMessageReference.last_message_for_chat(chat) %{ id: chat.id |> to_string(), account: AccountView.render("show.json", Map.put(opts, :user, recipient)), unread: chat.unread, last_message: - last_message && ChatMessageView.render("show.json", chat: chat, object: last_message), + last_message && + ChatMessageReferenceView.render("show.json", chat_message_reference: last_message), updated_at: Utils.to_masto_date(chat.updated_at) } end diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 5e953d770..616e0c4f2 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.StreamerView do alias Pleroma.Activity alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.User @@ -15,10 +16,15 @@ defmodule Pleroma.Web.StreamerView do def render("chat_update.json", object, user, recipients) do chat = Chat.get(user.id, hd(recipients -- [user.ap_id])) + # Explicitly giving the cmr for the object here, so we don't accidentally + # send a later 'last_message' that was inserted between inserting this and + # streaming it out + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + representation = Pleroma.Web.PleromaAPI.ChatView.render( "show.json", - %{message: object, chat: chat} + %{last_message: cm_ref, chat: chat} ) %{ diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index 384fe7253..c5691341a 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Activity alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -16,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView import Pleroma.Factory defp test_notifications_rendering(notifications, user, expected_result) do @@ -44,13 +45,15 @@ test "ChatMessage notification" do object = Object.normalize(activity) chat = Chat.get(recipient.id, user.ap_id) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + expected = %{ id: to_string(notification.id), pleroma: %{is_seen: false}, type: "pleroma:chat_mention", account: AccountView.render("show.json", %{user: user, for: recipient}), chat_message: - ChatMessageView.render("show.json", %{object: object, for: recipient, chat: chat}), + ChatMessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), created_at: Utils.to_masto_date(notification.inserted_at) } diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index 6062a0cfe..f3bd12616 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -6,11 +6,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do use Pleroma.DataCase alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.ChatMessageView + alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView alias Pleroma.Web.PleromaAPI.ChatView import Pleroma.Factory @@ -39,7 +40,9 @@ test "it represents a chat" do represented_chat = ChatView.render("show.json", chat: chat) + cm_ref = ChatMessageReference.for_chat_and_object(chat, chat_message) + assert represented_chat[:last_message] == - ChatMessageView.render("show.json", chat: chat, object: chat_message) + ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref) end end From 8a43611e01cef670c6eac8457be95c5d20efcbc8 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 3 Jun 2020 14:53:46 +0400 Subject: [PATCH 190/375] Use AdminAPI.StatusView in api/admin/users --- lib/pleroma/web/admin_api/controllers/admin_api_controller.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 9f499e202..cc93fb509 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -30,7 +30,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint - alias Pleroma.Web.MastodonAPI alias Pleroma.Web.Router require Logger @@ -279,7 +278,7 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do }) conn - |> put_view(MastodonAPI.StatusView) + |> put_view(AdminAPI.StatusView) |> render("index.json", %{activities: activities, as: :activity}) else _ -> {:error, :not_found} From 2591745fc2417771f96340ed3f36177c0da194c3 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 12:56:39 +0200 Subject: [PATCH 191/375] ChatMessageReferences: Move tests --- lib/pleroma/chat.ex | 34 ---------------------------- test/chat_message_reference_test.exs | 29 ++++++++++++++++++++++++ test/chat_test.exs | 16 ------------- 3 files changed, 29 insertions(+), 50 deletions(-) create mode 100644 test/chat_message_reference_test.exs diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 211b872f9..65938c7a4 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -6,9 +6,7 @@ defmodule Pleroma.Chat do use Ecto.Schema import Ecto.Changeset - import Ecto.Query - alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -26,38 +24,6 @@ defmodule Pleroma.Chat do timestamps() end - def last_message_for_chat(chat) do - messages_for_chat_query(chat) - |> order_by(desc: :id) - |> limit(1) - |> Repo.one() - end - - def messages_for_chat_query(chat) do - chat = - chat - |> Repo.preload(:user) - - from(o in Object, - where: fragment("?->>'type' = ?", o.data, "ChatMessage"), - where: - fragment( - """ - (?->>'actor' = ? and ?->'to' = ?) - OR (?->>'actor' = ? and ?->'to' = ?) - """, - o.data, - ^chat.user.ap_id, - o.data, - ^[chat.recipient], - o.data, - ^chat.recipient, - o.data, - ^[chat.user.ap_id] - ) - ) - end - def creation_cng(struct, params) do struct |> cast(params, [:user_id, :recipient, :unread]) diff --git a/test/chat_message_reference_test.exs b/test/chat_message_reference_test.exs new file mode 100644 index 000000000..963a0e225 --- /dev/null +++ b/test/chat_message_reference_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ChatMessageReferencTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + alias Pleroma.ChatMessageReference + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "messages" do + test "it returns the last message in a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey") + {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho") + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + message = ChatMessageReference.last_message_for_chat(chat) + + assert message.object.data["content"] == "ho" + end + end +end diff --git a/test/chat_test.exs b/test/chat_test.exs index dfcb6422e..42e01fe27 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -10,22 +10,6 @@ defmodule Pleroma.ChatTest do import Pleroma.Factory - describe "messages" do - test "it returns the last message in a chat" do - user = insert(:user) - recipient = insert(:user) - - {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey") - {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho") - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - message = Chat.last_message_for_chat(chat) - - assert message.data["content"] == "ho" - end - end - describe "creation and getting" do test "it only works if the recipient is a valid user (for now)" do user = insert(:user) From 6413e06a861bd383196c79d7754a67d96cd5e2a4 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 13:13:44 +0200 Subject: [PATCH 192/375] Migrations: Add unique index to ChatMessageReferences. --- ...add_unique_index_to_chat_message_references.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs diff --git a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs new file mode 100644 index 000000000..1101be94f --- /dev/null +++ b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs @@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.BackfillChatMessageReferences do + use Ecto.Migration + + alias Pleroma.Chat + alias Pleroma.ChatMessageReference + alias Pleroma.Object + alias Pleroma.Repo + + import Ecto.Query + + def change do + create(unique_index(:chat_message_references, [:object_id, :chat_id])) + end +end From 73127cff750736c5ebe15606db2f928a8924499a Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 13:17:29 +0200 Subject: [PATCH 193/375] Credo fixes. --- lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index c54681054..f22f33de9 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -13,8 +13,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.ChatView import Ecto.Query import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] From 8edead7c1dc33457dc30b301b544d71482ef0f28 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 13:19:38 +0200 Subject: [PATCH 194/375] Migration: Remove superfluous imports --- ...3105113_add_unique_index_to_chat_message_references.exs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs index 1101be94f..623ac6c85 100644 --- a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs +++ b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs @@ -1,13 +1,6 @@ defmodule Pleroma.Repo.Migrations.BackfillChatMessageReferences do use Ecto.Migration - alias Pleroma.Chat - alias Pleroma.ChatMessageReference - alias Pleroma.Object - alias Pleroma.Repo - - import Ecto.Query - def change do create(unique_index(:chat_message_references, [:object_id, :chat_id])) end From 7f5c5b11a5baeddec36ccc01b4954ac8aa9f8590 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 14:26:50 +0200 Subject: [PATCH 195/375] Chats: Remove `unread` from the db, calculate from unseen messages. --- lib/pleroma/chat.ex | 13 +++---------- lib/pleroma/chat_message_reference.ex | 16 ++++++++++++++++ lib/pleroma/web/activity_pub/side_effects.ex | 9 ++------- .../pleroma_api/controllers/chat_controller.ex | 2 +- lib/pleroma/web/pleroma_api/views/chat_view.ex | 2 +- .../20200603120448_remove_unread_from_chats.exs | 9 +++++++++ test/chat_test.exs | 6 +----- test/web/activity_pub/side_effects_test.exs | 2 -- .../controllers/chat_controller_test.exs | 11 +++++++---- 9 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 priv/repo/migrations/20200603120448_remove_unread_from_chats.exs diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 65938c7a4..5aefddc5e 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -19,14 +19,13 @@ defmodule Pleroma.Chat do schema "chats" do belongs_to(:user, User, type: FlakeId.Ecto.CompatType) field(:recipient, :string) - field(:unread, :integer, default: 0, read_after_writes: true) timestamps() end def creation_cng(struct, params) do struct - |> cast(params, [:user_id, :recipient, :unread]) + |> cast(params, [:user_id, :recipient]) |> validate_change(:recipient, fn :recipient, recipient -> case User.get_cached_by_ap_id(recipient) do @@ -61,16 +60,10 @@ def get_or_create(user_id, recipient) do def bump_or_create(user_id, recipient) do %__MODULE__{} - |> creation_cng(%{user_id: user_id, recipient: recipient, unread: 1}) + |> creation_cng(%{user_id: user_id, recipient: recipient}) |> Repo.insert( - on_conflict: [set: [updated_at: NaiveDateTime.utc_now()], inc: [unread: 1]], + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], conflict_target: [:user_id, :recipient] ) end - - def mark_as_read(chat) do - chat - |> change(%{unread: 0}) - |> Repo.update() - end end diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat_message_reference.ex index 6808d1365..ad174b294 100644 --- a/lib/pleroma/chat_message_reference.ex +++ b/lib/pleroma/chat_message_reference.ex @@ -84,4 +84,20 @@ def create(chat, object, seen) do |> changeset(params) |> Repo.insert() end + + def unread_count_for_chat(chat) do + chat + |> for_chat_query() + |> where([cmr], cmr.seen == false) + |> Repo.aggregate(:count) + end + + def set_all_seen_for_chat(chat) do + chat + |> for_chat_query() + |> exclude(:order_by) + |> exclude(:preload) + |> where([cmr], cmr.seen == false) + |> Repo.update_all(set: [seen: true]) + end end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index cda52b00e..884d399d0 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -139,13 +139,8 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do [[actor, recipient], [recipient, actor]] |> Enum.each(fn [user, other_user] -> if user.local do - if user.ap_id == actor.ap_id do - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - ChatMessageReference.create(chat, object, true) - else - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - ChatMessageReference.create(chat, object, false) - end + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + ChatMessageReference.create(chat, object, user.ap_id == actor.ap_id) end end) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index f22f33de9..29922da99 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -90,7 +90,7 @@ def post_chat_message( def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), - {:ok, chat} <- Chat.mark_as_read(chat) do + {_n, _} <- ChatMessageReference.set_all_seen_for_chat(chat) do conn |> put_view(ChatView) |> render("show.json", chat: chat) diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 331c1d282..c903a71fd 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -20,7 +20,7 @@ def render("show.json", %{chat: %Chat{} = chat} = opts) do %{ id: chat.id |> to_string(), account: AccountView.render("show.json", Map.put(opts, :user, recipient)), - unread: chat.unread, + unread: ChatMessageReference.unread_count_for_chat(chat), last_message: last_message && ChatMessageReferenceView.render("show.json", chat_message_reference: last_message), diff --git a/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs new file mode 100644 index 000000000..6322137d5 --- /dev/null +++ b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.RemoveUnreadFromChats do + use Ecto.Migration + + def change do + alter table(:chats) do + remove(:unread, :integer, default: 0) + end + end +end diff --git a/test/chat_test.exs b/test/chat_test.exs index 42e01fe27..332f2180a 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.ChatTest do use Pleroma.DataCase, async: true alias Pleroma.Chat - alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -35,7 +34,6 @@ test "it returns and bumps a chat for a user and recipient if it already exists" {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) assert chat.id == chat_two.id - assert chat_two.unread == 2 end test "it returns a chat for a user and recipient if it already exists" do @@ -48,15 +46,13 @@ test "it returns a chat for a user and recipient if it already exists" do assert chat.id == chat_two.id end - test "a returning chat will have an updated `update_at` field and an incremented unread count" do + test "a returning chat will have an updated `update_at` field" do user = insert(:user) other_user = insert(:user) {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - assert chat.unread == 1 :timer.sleep(1500) {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) - assert chat_two.unread == 2 assert chat.id == chat_two.id assert chat.updated_at != chat_two.updated_at diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index ff6b3ac15..f2fa062b4 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -346,7 +346,6 @@ test "it creates a Chat and ChatMessageReferences for the local users and bumps SideEffects.handle(create_activity, local: false, object_data: chat_message_data) chat = Chat.get(author.id, recipient.ap_id) - assert chat.unread == 0 [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() @@ -354,7 +353,6 @@ test "it creates a Chat and ChatMessageReferences for the local users and bumps assert cm_ref.seen == true chat = Chat.get(recipient.id, author.ap_id) - assert chat.unread == 1 [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index bd4024c09..e62b71799 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -19,9 +19,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do test "it marks all messages in a chat as read", %{conn: conn, user: user} do other_user = insert(:user) - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert chat.unread == 1 + assert cm_ref.seen == false result = conn @@ -30,9 +33,9 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do assert result["unread"] == 0 - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert chat.unread == 0 + assert cm_ref.seen == true end end From 1e9efcf7c3de2aa4d57d4292dfa5843761bff111 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 14:27:54 +0200 Subject: [PATCH 196/375] Migrations: Fix migration module name --- ...200603105113_add_unique_index_to_chat_message_references.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs index 623ac6c85..fdf85132e 100644 --- a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs +++ b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs @@ -1,4 +1,4 @@ -defmodule Pleroma.Repo.Migrations.BackfillChatMessageReferences do +defmodule Pleroma.Repo.Migrations.AddUniqueIndexToChatMessageReferences do use Ecto.Migration def change do From 7b79871e9721dca9b134598c182df890b909047c Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 14:32:19 +0200 Subject: [PATCH 197/375] Migrations: Add chat_id, seen index to ChatMessageReferences This ensures fast count of unseen messages --- ...732_add_seen_index_to_chat_message_references.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs diff --git a/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs new file mode 100644 index 000000000..a5065d612 --- /dev/null +++ b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs @@ -0,0 +1,12 @@ +defmodule Pleroma.Repo.Migrations.AddSeenIndexToChatMessageReferences do + use Ecto.Migration + + def change do + create( + index(:chat_message_references, [:chat_id], + where: "seen = false", + name: "unseen_messages_count_index" + ) + ) + end +end From 903955b189561d3a95d5955feda723999078b894 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 14:40:44 +0200 Subject: [PATCH 198/375] FollowingRelationship: Remove meaningless change --- lib/pleroma/following_relationship.ex | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 0343a20d4..3a3082e72 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -62,13 +62,10 @@ def update(%User{} = follower, %User{} = following, state) do follow(follower, following, state) following_relationship -> - {:ok, relationship} = - following_relationship - |> cast(%{state: state}, [:state]) - |> validate_required([:state]) - |> Repo.update() - - {:ok, relationship} + following_relationship + |> cast(%{state: state}, [:state]) + |> validate_required([:state]) + |> Repo.update() end end From fb4ae9c720054372c1f0e41e3227fb8ad24e6c2d Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 16:45:04 +0200 Subject: [PATCH 199/375] Streamer, SideEffects: Stream out ChatMessageReferences Saves us a few calles to fetch things from the DB that we already have. --- lib/pleroma/web/activity_pub/side_effects.ex | 8 +++- lib/pleroma/web/streamer/streamer.ex | 21 +++------ lib/pleroma/web/views/streamer_view.ex | 46 +++++++++----------- test/web/activity_pub/side_effects_test.exs | 5 +-- test/web/streamer/streamer_test.exs | 28 +++++++++--- 5 files changed, 58 insertions(+), 50 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 884d399d0..0c5709356 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -140,11 +140,15 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do |> Enum.each(fn [user, other_user] -> if user.local do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - ChatMessageReference.create(chat, object, user.ap_id == actor.ap_id) + {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id == actor.ap_id) + + Streamer.stream( + ["user", "user:pleroma_chat"], + {user, %{cm_ref | chat: chat, object: object}} + ) end end) - Streamer.stream(["user", "user:pleroma_chat"], object) {:ok, object, meta} end end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 2201cbfef..5e37e2cf2 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -6,11 +6,11 @@ defmodule Pleroma.Web.Streamer do require Logger alias Pleroma.Activity + alias Pleroma.ChatMessageReference alias Pleroma.Config alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object - alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility @@ -201,22 +201,15 @@ defp do_stream(topic, %Notification{} = item) end) end - defp do_stream(topic, %{data: %{"type" => "ChatMessage"}} = object) + defp do_stream(topic, {user, %ChatMessageReference{} = cm_ref}) when topic in ["user", "user:pleroma_chat"] do - recipients = [object.data["actor"] | object.data["to"]] + topic = "#{topic}:#{user.id}" - topics = - %{ap_id: recipients, local: true} - |> Pleroma.User.Query.build() - |> Repo.all() - |> Enum.map(fn %{id: id} = user -> {user, "#{topic}:#{id}"} end) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) - Enum.each(topics, fn {user, topic} -> - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _auth} -> - text = StreamerView.render("chat_update.json", object, user, recipients) - send(pid, {:text, text}) - end) + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _auth} -> + send(pid, {:text, text}) end) end) end diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 616e0c4f2..a6efd0109 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -6,36 +6,11 @@ defmodule Pleroma.Web.StreamerView do use Pleroma.Web, :view alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.ChatMessageReference alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.User alias Pleroma.Web.MastodonAPI.NotificationView - def render("chat_update.json", object, user, recipients) do - chat = Chat.get(user.id, hd(recipients -- [user.ap_id])) - - # Explicitly giving the cmr for the object here, so we don't accidentally - # send a later 'last_message' that was inserted between inserting this and - # streaming it out - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - - representation = - Pleroma.Web.PleromaAPI.ChatView.render( - "show.json", - %{last_message: cm_ref, chat: chat} - ) - - %{ - event: "pleroma:chat_update", - payload: - representation - |> Jason.encode!() - } - |> Jason.encode!() - end - def render("update.json", %Activity{} = activity, %User{} = user) do %{ event: "update", @@ -76,6 +51,27 @@ def render("update.json", %Activity{} = activity) do |> Jason.encode!() end + def render("chat_update.json", %{chat_message_reference: cm_ref}) do + # Explicitly giving the cmr for the object here, so we don't accidentally + # send a later 'last_message' that was inserted between inserting this and + # streaming it out + Logger.debug("Trying to stream out #{inspect(cm_ref)}") + + representation = + Pleroma.Web.PleromaAPI.ChatView.render( + "show.json", + %{last_message: cm_ref, chat: cm_ref.chat} + ) + + %{ + event: "pleroma:chat_update", + payload: + representation + |> Jason.encode!() + } + |> Jason.encode!() + end + def render("conversation.json", %Participation{} = participation) do %{ event: "conversation", diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index f2fa062b4..92c266d84 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -325,9 +325,8 @@ test "it streams the created ChatMessage" do {:ok, _create_activity, _meta} = SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - object = Object.normalize(create_activity, false) - - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], object)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], {author, :_})) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], {recipient, :_})) end end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index bcb05a02d..893ae5449 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.Web.StreamerTest do import Pleroma.Factory + alias Pleroma.Chat + alias Pleroma.ChatMessageReference alias Pleroma.Conversation.Participation alias Pleroma.List alias Pleroma.Object @@ -150,22 +152,36 @@ test "it sends notify to in the 'user:notification' stream", %{user: user, notif test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do other_user = insert(:user) - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + Streamer.get_topic_and_add_socket("user:pleroma_chat", user) - Streamer.stream("user:pleroma_chat", object) - text = StreamerView.render("chat_update.json", object, user, [user.ap_id, other_user.ap_id]) + Streamer.stream("user:pleroma_chat", {user, cm_ref}) + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + assert text =~ "hey cirno" assert_receive {:text, ^text} end test "it sends chat messages to the 'user' stream", %{user: user} do other_user = insert(:user) - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + Streamer.get_topic_and_add_socket("user", user) - Streamer.stream("user", object) - text = StreamerView.render("chat_update.json", object, user, [user.ap_id, other_user.ap_id]) + Streamer.stream("user", {user, cm_ref}) + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + assert text =~ "hey cirno" assert_receive {:text, ^text} end From 9d572f2f66d600d77cf74e40547dea0f959fe357 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 21 May 2020 19:43:56 +0400 Subject: [PATCH 200/375] Move report actions to AdminAPI.ReportController --- .../controllers/admin_api_controller.ex | 97 ----- .../controllers/report_controller.ex | 129 ++++++ lib/pleroma/web/router.ex | 10 +- .../controllers/admin_api_controller_test.exs | 341 ---------------- .../controllers/report_controller_test.exs | 368 ++++++++++++++++++ 5 files changed, 502 insertions(+), 443 deletions(-) create mode 100644 lib/pleroma/web/admin_api/controllers/report_controller.ex create mode 100644 test/web/admin_api/controllers/report_controller_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index cc93fb509..467d05375 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -7,28 +7,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Activity alias Pleroma.Config alias Pleroma.ConfigDB alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.ReportNote alias Pleroma.Stats alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView - alias Pleroma.Web.AdminAPI.Report - alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search - alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Web.Router @@ -71,18 +65,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] ) - plug( - OAuthScopesPlug, - %{scopes: ["read:reports"], admin: true} - when action in [:list_reports, :report_show] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:reports"], admin: true} - when action in [:reports_update, :report_notes_create, :report_notes_delete] - ) - plug( OAuthScopesPlug, %{scopes: ["read:statuses"], admin: true} @@ -645,85 +627,6 @@ def update_user_credentials( end end - def list_reports(conn, params) do - {page, page_size} = page_params(params) - - reports = Utils.get_reports(params, page, page_size) - - conn - |> put_view(ReportView) - |> render("index.json", %{reports: reports}) - end - - def report_show(conn, %{"id" => id}) do - with %Activity{} = report <- Activity.get_by_id(id) do - conn - |> put_view(ReportView) - |> render("show.json", Report.extract_report_info(report)) - else - _ -> {:error, :not_found} - end - end - - def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do - result = - reports - |> Enum.map(fn report -> - with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do - ModerationLog.insert_log(%{ - action: "report_update", - actor: admin, - subject: activity - }) - - activity - else - {:error, message} -> %{id: report["id"], error: message} - end - end) - - case Enum.any?(result, &Map.has_key?(&1, :error)) do - true -> json_response(conn, :bad_request, result) - false -> json_response(conn, :no_content, "") - end - end - - def report_notes_create(%{assigns: %{user: user}} = conn, %{ - "id" => report_id, - "content" => content - }) do - with {:ok, _} <- ReportNote.create(user.id, report_id, content) do - ModerationLog.insert_log(%{ - action: "report_note", - actor: user, - subject: Activity.get_by_id(report_id), - text: content - }) - - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end - - def report_notes_delete(%{assigns: %{user: user}} = conn, %{ - "id" => note_id, - "report_id" => report_id - }) do - with {:ok, note} <- ReportNote.destroy(note_id) do - ModerationLog.insert_log(%{ - action: "report_note_delete", - actor: user, - subject: Activity.get_by_id(report_id), - text: note.content - }) - - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end - def list_log(conn, params) do {page, page_size} = page_params(params) diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex new file mode 100644 index 000000000..23f0174d4 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -0,0 +1,129 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.ReportNote + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.CommonAPI + + require Logger + + @users_page_size 50 + + plug(OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:index, :show]) + + plug( + OAuthScopesPlug, + %{scopes: ["write:reports"], admin: true} + when action in [:update, :notes_create, :notes_delete] + ) + + action_fallback(AdminAPI.FallbackController) + + def index(conn, params) do + {page, page_size} = page_params(params) + + reports = Utils.get_reports(params, page, page_size) + + render(conn, "index.json", reports: reports) + end + + def show(conn, %{"id" => id}) do + with %Activity{} = report <- Activity.get_by_id(id) do + render(conn, "show.json", Report.extract_report_info(report)) + else + _ -> {:error, :not_found} + end + end + + def update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do + result = + reports + |> Enum.map(fn report -> + with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: activity + }) + + activity + else + {:error, message} -> %{id: report["id"], error: message} + end + end) + + case Enum.any?(result, &Map.has_key?(&1, :error)) do + true -> json_response(conn, :bad_request, result) + false -> json_response(conn, :no_content, "") + end + end + + def notes_create(%{assigns: %{user: user}} = conn, %{ + "id" => report_id, + "content" => content + }) do + with {:ok, _} <- ReportNote.create(user.id, report_id, content) do + ModerationLog.insert_log(%{ + action: "report_note", + actor: user, + subject: Activity.get_by_id(report_id), + text: content + }) + + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end + + def notes_delete(%{assigns: %{user: user}} = conn, %{ + "id" => note_id, + "report_id" => report_id + }) do + with {:ok, note} <- ReportNote.destroy(note_id) do + ModerationLog.insert_log(%{ + action: "report_note_delete", + actor: user, + subject: Activity.get_by_id(report_id), + text: note.content + }) + + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end + + defp page_params(params) do + {get_page(params["page"]), get_page_size(params["page_size"])} + end + + defp get_page(page_string) when is_nil(page_string), do: 1 + + defp get_page(page_string) do + case Integer.parse(page_string) do + {page, _} -> page + :error -> 1 + end + end + + defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size + + defp get_page_size(page_size_string) do + case Integer.parse(page_size_string) do + {page_size, _} -> page_size + :error -> @users_page_size + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 369c11138..80ea28364 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -183,11 +183,11 @@ defmodule Pleroma.Web.Router do patch("/users/confirm_email", AdminAPIController, :confirm_email) patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) - get("/reports", AdminAPIController, :list_reports) - get("/reports/:id", AdminAPIController, :report_show) - patch("/reports", AdminAPIController, :reports_update) - post("/reports/:id/notes", AdminAPIController, :report_notes_create) - delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) + get("/reports", ReportController, :index) + get("/reports/:id", ReportController, :show) + patch("/reports", ReportController, :update) + post("/reports/:id/notes", ReportController, :notes_create) + delete("/reports/:report_id/notes/:id", ReportController, :notes_delete) get("/statuses/:id", StatusController, :show) put("/statuses/:id", StatusController, :update) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index d72851c9e..a1bff5688 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -17,7 +17,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Repo - alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web @@ -1198,286 +1197,6 @@ test "returns 404 if user not found", %{conn: conn} do end end - describe "GET /api/pleroma/admin/reports/:id" do - test "returns report by its id", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - response = - conn - |> get("/api/pleroma/admin/reports/#{report_id}") - |> json_response(:ok) - - assert response["id"] == report_id - end - - test "returns 404 when report id is invalid", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/reports/test") - - assert json_response(conn, :not_found) == %{"error" => "Not found"} - end - end - - describe "PATCH /api/pleroma/admin/reports" do - setup do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel very offended", - status_ids: [activity.id] - }) - - %{ - id: report_id, - second_report_id: second_report_id - } - end - - test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do - read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) - write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) - - response = - conn - |> assign(:token, read_token) - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [%{"state" => "resolved", "id" => id}] - }) - |> json_response(403) - - assert response == %{ - "error" => "Insufficient permissions: admin:write:reports." - } - - conn - |> assign(:token, write_token) - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [%{"state" => "resolved", "id" => id}] - }) - |> json_response(:no_content) - end - - test "mark report as resolved", %{conn: conn, id: id, admin: admin} do - conn - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "resolved", "id" => id} - ] - }) - |> json_response(:no_content) - - activity = Activity.get_by_id(id) - assert activity.data["state"] == "resolved" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated report ##{id} with 'resolved' state" - end - - test "closes report", %{conn: conn, id: id, admin: admin} do - conn - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "closed", "id" => id} - ] - }) - |> json_response(:no_content) - - activity = Activity.get_by_id(id) - assert activity.data["state"] == "closed" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated report ##{id} with 'closed' state" - end - - test "returns 400 when state is unknown", %{conn: conn, id: id} do - conn = - conn - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "test", "id" => id} - ] - }) - - assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state" - end - - test "returns 404 when report is not exist", %{conn: conn} do - conn = - conn - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "closed", "id" => "test"} - ] - }) - - assert hd(json_response(conn, :bad_request))["error"] == "not_found" - end - - test "updates state of multiple reports", %{ - conn: conn, - id: id, - admin: admin, - second_report_id: second_report_id - } do - conn - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "resolved", "id" => id}, - %{"state" => "closed", "id" => second_report_id} - ] - }) - |> json_response(:no_content) - - activity = Activity.get_by_id(id) - second_activity = Activity.get_by_id(second_report_id) - assert activity.data["state"] == "resolved" - assert second_activity.data["state"] == "closed" - - [first_log_entry, second_log_entry] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(first_log_entry) == - "@#{admin.nickname} updated report ##{id} with 'resolved' state" - - assert ModerationLog.get_log_entry_message(second_log_entry) == - "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" - end - end - - describe "GET /api/pleroma/admin/reports" do - test "returns empty response when no reports created", %{conn: conn} do - response = - conn - |> get("/api/pleroma/admin/reports") - |> json_response(:ok) - - assert Enum.empty?(response["reports"]) - assert response["total"] == 0 - end - - test "returns reports", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - response = - conn - |> get("/api/pleroma/admin/reports") - |> json_response(:ok) - - [report] = response["reports"] - - assert length(response["reports"]) == 1 - assert report["id"] == report_id - - assert response["total"] == 1 - end - - test "returns reports with specified state", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: first_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I don't like this user" - }) - - CommonAPI.update_report_state(second_report_id, "closed") - - response = - conn - |> get("/api/pleroma/admin/reports", %{ - "state" => "open" - }) - |> json_response(:ok) - - [open_report] = response["reports"] - - assert length(response["reports"]) == 1 - assert open_report["id"] == first_report_id - - assert response["total"] == 1 - - response = - conn - |> get("/api/pleroma/admin/reports", %{ - "state" => "closed" - }) - |> json_response(:ok) - - [closed_report] = response["reports"] - - assert length(response["reports"]) == 1 - assert closed_report["id"] == second_report_id - - assert response["total"] == 1 - - response = - conn - |> get("/api/pleroma/admin/reports", %{ - "state" => "resolved" - }) - |> json_response(:ok) - - assert Enum.empty?(response["reports"]) - assert response["total"] == 0 - end - - test "returns 403 when requested by a non-admin" do - user = insert(:user) - token = insert(:oauth_token, user: user) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/pleroma/admin/reports") - - assert json_response(conn, :forbidden) == - %{"error" => "User is not an admin or OAuth admin scope is not granted."} - end - - test "returns 403 when requested by anonymous" do - conn = get(build_conn(), "/api/pleroma/admin/reports") - - assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."} - end - end - describe "GET /api/pleroma/admin/config" do setup do: clear_config(:configurable_from_database, true) @@ -3195,66 +2914,6 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do end end - describe "POST /reports/:id/notes" do - setup %{conn: conn, admin: admin} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ - content: "this is disgusting!" - }) - - post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ - content: "this is disgusting2!" - }) - - %{ - admin_id: admin.id, - report_id: report_id - } - end - - test "it creates report note", %{admin_id: admin_id, report_id: report_id} do - [note, _] = Repo.all(ReportNote) - - assert %{ - activity_id: ^report_id, - content: "this is disgusting!", - user_id: ^admin_id - } = note - end - - test "it returns reports with notes", %{conn: conn, admin: admin} do - conn = get(conn, "/api/pleroma/admin/reports") - - response = json_response(conn, 200) - notes = hd(response["reports"])["notes"] - [note, _] = notes - - assert note["user"]["nickname"] == admin.nickname - assert note["content"] == "this is disgusting!" - assert note["created_at"] - assert response["total"] == 1 - end - - test "it deletes the note", %{conn: conn, report_id: report_id} do - assert ReportNote |> Repo.all() |> length() == 2 - - [note, _] = Repo.all(ReportNote) - - delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") - - assert ReportNote |> Repo.all() |> length() == 1 - end - end - describe "GET /api/pleroma/admin/config/descriptions" do test "structure", %{conn: conn} do admin = insert(:user, is_admin: true) diff --git a/test/web/admin_api/controllers/report_controller_test.exs b/test/web/admin_api/controllers/report_controller_test.exs new file mode 100644 index 000000000..0eddb369c --- /dev/null +++ b/test/web/admin_api/controllers/report_controller_test.exs @@ -0,0 +1,368 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.ReportNote + alias Pleroma.Web.CommonAPI + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/reports/:id" do + test "returns report by its id", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + response = + conn + |> get("/api/pleroma/admin/reports/#{report_id}") + |> json_response(:ok) + + assert response["id"] == report_id + end + + test "returns 404 when report id is invalid", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/reports/test") + + assert json_response(conn, :not_found) == %{"error" => "Not found"} + end + end + + describe "PATCH /api/pleroma/admin/reports" do + setup do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel very offended", + status_ids: [activity.id] + }) + + %{ + id: report_id, + second_report_id: second_report_id + } + end + + test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do + read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) + + response = + conn + |> assign(:token, read_token) + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response(403) + + assert response == %{ + "error" => "Insufficient permissions: admin:write:reports." + } + + conn + |> assign(:token, write_token) + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response(:no_content) + end + + test "mark report as resolved", %{conn: conn, id: id, admin: admin} do + conn + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id} + ] + }) + |> json_response(:no_content) + + activity = Activity.get_by_id(id) + assert activity.data["state"] == "resolved" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" + end + + test "closes report", %{conn: conn, id: id, admin: admin} do + conn + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "closed", "id" => id} + ] + }) + |> json_response(:no_content) + + activity = Activity.get_by_id(id) + assert activity.data["state"] == "closed" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'closed' state" + end + + test "returns 400 when state is unknown", %{conn: conn, id: id} do + conn = + conn + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "test", "id" => id} + ] + }) + + assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state" + end + + test "returns 404 when report is not exist", %{conn: conn} do + conn = + conn + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "closed", "id" => "test"} + ] + }) + + assert hd(json_response(conn, :bad_request))["error"] == "not_found" + end + + test "updates state of multiple reports", %{ + conn: conn, + id: id, + admin: admin, + second_report_id: second_report_id + } do + conn + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id}, + %{"state" => "closed", "id" => second_report_id} + ] + }) + |> json_response(:no_content) + + activity = Activity.get_by_id(id) + second_activity = Activity.get_by_id(second_report_id) + assert activity.data["state"] == "resolved" + assert second_activity.data["state"] == "closed" + + [first_log_entry, second_log_entry] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(first_log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" + + assert ModerationLog.get_log_entry_message(second_log_entry) == + "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" + end + end + + describe "GET /api/pleroma/admin/reports" do + test "returns empty response when no reports created", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/reports") + |> json_response(:ok) + + assert Enum.empty?(response["reports"]) + assert response["total"] == 0 + end + + test "returns reports", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + response = + conn + |> get("/api/pleroma/admin/reports") + |> json_response(:ok) + + [report] = response["reports"] + + assert length(response["reports"]) == 1 + assert report["id"] == report_id + + assert response["total"] == 1 + end + + test "returns reports with specified state", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: first_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I don't like this user" + }) + + CommonAPI.update_report_state(second_report_id, "closed") + + response = + conn + |> get("/api/pleroma/admin/reports", %{ + "state" => "open" + }) + |> json_response(:ok) + + [open_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert open_report["id"] == first_report_id + + assert response["total"] == 1 + + response = + conn + |> get("/api/pleroma/admin/reports", %{ + "state" => "closed" + }) + |> json_response(:ok) + + [closed_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert closed_report["id"] == second_report_id + + assert response["total"] == 1 + + response = + conn + |> get("/api/pleroma/admin/reports", %{ + "state" => "resolved" + }) + |> json_response(:ok) + + assert Enum.empty?(response["reports"]) + assert response["total"] == 0 + end + + test "returns 403 when requested by a non-admin" do + user = insert(:user) + token = insert(:oauth_token, user: user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == + %{"error" => "User is not an admin or OAuth admin scope is not granted."} + end + + test "returns 403 when requested by anonymous" do + conn = get(build_conn(), "/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."} + end + end + + describe "POST /api/pleroma/admin/reports/:id/notes" do + setup %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting!" + }) + + post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting2!" + }) + + %{ + admin_id: admin.id, + report_id: report_id + } + end + + test "it creates report note", %{admin_id: admin_id, report_id: report_id} do + [note, _] = Repo.all(ReportNote) + + assert %{ + activity_id: ^report_id, + content: "this is disgusting!", + user_id: ^admin_id + } = note + end + + test "it returns reports with notes", %{conn: conn, admin: admin} do + conn = get(conn, "/api/pleroma/admin/reports") + + response = json_response(conn, 200) + notes = hd(response["reports"])["notes"] + [note, _] = notes + + assert note["user"]["nickname"] == admin.nickname + assert note["content"] == "this is disgusting!" + assert note["created_at"] + assert response["total"] == 1 + end + + test "it deletes the note", %{conn: conn, report_id: report_id} do + assert ReportNote |> Repo.all() |> length() == 2 + + [note, _] = Repo.all(ReportNote) + + delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") + + assert ReportNote |> Repo.all() |> length() == 1 + end + end +end From c16315d055d07206dddb228583956d5b718ecdd4 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 3 Jun 2020 19:10:11 +0400 Subject: [PATCH 201/375] Add OpenAPI spec for AdminAPI.ReportController --- docs/API/admin_api.md | 4 +- lib/pleroma/web/activity_pub/utils.ex | 1 + .../controllers/report_controller.ex | 74 ++---- .../operations/admin/report_operation.ex | 237 ++++++++++++++++++ .../operations/admin/status_operation.ex | 2 +- .../controllers/report_controller_test.exs | 80 +++--- 6 files changed, 310 insertions(+), 88 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/admin/report_operation.ex diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 639c3224d..92816baf9 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -547,7 +547,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ```json { - "totalReports" : 1, + "total" : 1, "reports": [ { "account": { @@ -768,7 +768,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - 400 Bad Request `"Invalid parameters"` when `status` is missing - On success: `204`, empty response -## `POST /api/pleroma/admin/reports/:report_id/notes/:id` +## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id` ### Delete report note diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index f2375bcc4..a76a699ee 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -740,6 +740,7 @@ defp build_flag_object(_), do: [] def get_reports(params, page, page_size) do params = params + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Flag") |> Map.put("skip_preload", true) |> Map.put("preload_report_notes", true) diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex index 23f0174d4..4c011e174 100644 --- a/lib/pleroma/web/admin_api/controllers/report_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -18,8 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do require Logger - @users_page_size 50 - + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:index, :show]) plug( @@ -30,15 +29,15 @@ defmodule Pleroma.Web.AdminAPI.ReportController do action_fallback(AdminAPI.FallbackController) - def index(conn, params) do - {page, page_size} = page_params(params) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation - reports = Utils.get_reports(params, page, page_size) + def index(conn, params) do + reports = Utils.get_reports(params, params.page, params.page_size) render(conn, "index.json", reports: reports) end - def show(conn, %{"id" => id}) do + def show(conn, %{id: id}) do with %Activity{} = report <- Activity.get_by_id(id) do render(conn, "show.json", Report.extract_report_info(report)) else @@ -46,32 +45,33 @@ def show(conn, %{"id" => id}) do end end - def update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do + def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do result = - reports - |> Enum.map(fn report -> - with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do - ModerationLog.insert_log(%{ - action: "report_update", - actor: admin, - subject: activity - }) + Enum.map(reports, fn report -> + case CommonAPI.update_report_state(report.id, report.state) do + {:ok, activity} -> + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: activity + }) - activity - else - {:error, message} -> %{id: report["id"], error: message} + activity + + {:error, message} -> + %{id: report.id, error: message} end end) - case Enum.any?(result, &Map.has_key?(&1, :error)) do - true -> json_response(conn, :bad_request, result) - false -> json_response(conn, :no_content, "") + if Enum.any?(result, &Map.has_key?(&1, :error)) do + json_response(conn, :bad_request, result) + else + json_response(conn, :no_content, "") end end - def notes_create(%{assigns: %{user: user}} = conn, %{ - "id" => report_id, - "content" => content + def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{ + id: report_id }) do with {:ok, _} <- ReportNote.create(user.id, report_id, content) do ModerationLog.insert_log(%{ @@ -88,8 +88,8 @@ def notes_create(%{assigns: %{user: user}} = conn, %{ end def notes_delete(%{assigns: %{user: user}} = conn, %{ - "id" => note_id, - "report_id" => report_id + id: note_id, + report_id: report_id }) do with {:ok, note} <- ReportNote.destroy(note_id) do ModerationLog.insert_log(%{ @@ -104,26 +104,4 @@ def notes_delete(%{assigns: %{user: user}} = conn, %{ _ -> json_response(conn, :bad_request, "") end end - - defp page_params(params) do - {get_page(params["page"]), get_page_size(params["page_size"])} - end - - defp get_page(page_string) when is_nil(page_string), do: 1 - - defp get_page(page_string) do - case Integer.parse(page_string) do - {page, _} -> page - :error -> 1 - end - end - - defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size - - defp get_page_size(page_size_string) do - case Integer.parse(page_size_string) do - {page_size, _} -> page_size - :error -> @users_page_size - end - end end diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex new file mode 100644 index 000000000..15e78bfaf --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -0,0 +1,237 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Status + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Get a list of reports", + operationId: "AdminAPI.ReportController.index", + security: [%{"oAuth" => ["read:reports"]}], + parameters: [ + Operation.parameter( + :state, + :query, + report_state(), + "Filter by report state" + ), + Operation.parameter( + :limit, + :query, + %Schema{type: :integer}, + "The number of records to retrieve" + ), + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page number" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number number of log entries per page" + ) + ], + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{ + total: %Schema{type: :integer}, + reports: %Schema{ + type: :array, + items: report() + } + } + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Get an individual report", + operationId: "AdminAPI.ReportController.show", + parameters: [id_param()], + security: [%{"oAuth" => ["read:reports"]}], + responses: %{ + 200 => Operation.response("Report", "application/json", report()), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Change the state of one or multiple reports", + operationId: "AdminAPI.ReportController.update", + security: [%{"oAuth" => ["write:reports"]}], + requestBody: request_body("Parameters", update_request(), required: true), + responses: %{ + 204 => no_content_response(), + 400 => Operation.response("Bad Request", "application/json", update_400_response()), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def notes_create_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Create report note", + operationId: "AdminAPI.ReportController.notes_create", + parameters: [id_param()], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + content: %Schema{type: :string, description: "The message"} + } + }), + security: [%{"oAuth" => ["write:reports"]}], + responses: %{ + 204 => no_content_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def notes_delete_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Delete report note", + operationId: "AdminAPI.ReportController.notes_delete", + parameters: [ + Operation.parameter(:report_id, :path, :string, "Report ID"), + Operation.parameter(:id, :path, :string, "Note ID") + ], + security: [%{"oAuth" => ["write:reports"]}], + responses: %{ + 204 => no_content_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp report_state do + %Schema{type: :string, enum: ["open", "closed", "resolved"]} + end + + defp id_param do + Operation.parameter(:id, :path, FlakeID, "Report ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end + + defp report do + %Schema{ + type: :object, + properties: %{ + id: FlakeID, + state: report_state(), + account: account_admin(), + actor: account_admin(), + content: %Schema{type: :string}, + created_at: %Schema{type: :string, format: :"date-time"}, + statuses: %Schema{type: :array, items: Status}, + notes: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :integer}, + user_id: FlakeID, + content: %Schema{type: :string}, + inserted_at: %Schema{type: :string, format: :"date-time"} + } + } + } + } + } + end + + defp account_admin do + %Schema{ + title: "Account", + description: "Account view for admins", + type: :object, + properties: + Map.merge(Account.schema().properties, %{ + nickname: %Schema{type: :string}, + deactivated: %Schema{type: :boolean}, + local: %Schema{type: :boolean}, + roles: %Schema{ + type: :object, + properties: %{ + admin: %Schema{type: :boolean}, + moderator: %Schema{type: :boolean} + } + }, + confirmation_pending: %Schema{type: :boolean} + }) + } + end + + defp update_request do + %Schema{ + type: :object, + required: [:reports], + properties: %{ + reports: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{allOf: [FlakeID], description: "Required, report ID"}, + state: %Schema{ + type: :string, + description: + "Required, the new state. Valid values are `open`, `closed` and `resolved`" + } + } + }, + example: %{ + "reports" => [ + %{"id" => "123", "state" => "closed"}, + %{"id" => "1337", "state" => "resolved"} + ] + } + } + } + } + end + + defp update_400_response do + %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{allOf: [FlakeID], description: "Report ID"}, + error: %Schema{type: :string, description: "Error message"} + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index 2947e6b34..745399b4b 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -123,7 +123,7 @@ defp status do } end - defp admin_account do + def admin_account do %Schema{ type: :object, properties: %{ diff --git a/test/web/admin_api/controllers/report_controller_test.exs b/test/web/admin_api/controllers/report_controller_test.exs index 0eddb369c..940bce340 100644 --- a/test/web/admin_api/controllers/report_controller_test.exs +++ b/test/web/admin_api/controllers/report_controller_test.exs @@ -41,7 +41,7 @@ test "returns report by its id", %{conn: conn} do response = conn |> get("/api/pleroma/admin/reports/#{report_id}") - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) assert response["id"] == report_id end @@ -49,7 +49,7 @@ test "returns report by its id", %{conn: conn} do test "returns 404 when report id is invalid", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/reports/test") - assert json_response(conn, :not_found) == %{"error" => "Not found"} + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} end end @@ -85,10 +85,11 @@ test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} d response = conn |> assign(:token, read_token) + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [%{"state" => "resolved", "id" => id}] }) - |> json_response(403) + |> json_response_and_validate_schema(403) assert response == %{ "error" => "Insufficient permissions: admin:write:reports." @@ -96,20 +97,22 @@ test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} d conn |> assign(:token, write_token) + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [%{"state" => "resolved", "id" => id}] }) - |> json_response(:no_content) + |> json_response_and_validate_schema(:no_content) end test "mark report as resolved", %{conn: conn, id: id, admin: admin} do conn + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [ %{"state" => "resolved", "id" => id} ] }) - |> json_response(:no_content) + |> json_response_and_validate_schema(:no_content) activity = Activity.get_by_id(id) assert activity.data["state"] == "resolved" @@ -122,12 +125,13 @@ test "mark report as resolved", %{conn: conn, id: id, admin: admin} do test "closes report", %{conn: conn, id: id, admin: admin} do conn + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [ %{"state" => "closed", "id" => id} ] }) - |> json_response(:no_content) + |> json_response_and_validate_schema(:no_content) activity = Activity.get_by_id(id) assert activity.data["state"] == "closed" @@ -141,25 +145,28 @@ test "closes report", %{conn: conn, id: id, admin: admin} do test "returns 400 when state is unknown", %{conn: conn, id: id} do conn = conn + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [ %{"state" => "test", "id" => id} ] }) - assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state" + assert "Unsupported state" = + hd(json_response_and_validate_schema(conn, :bad_request))["error"] end test "returns 404 when report is not exist", %{conn: conn} do conn = conn + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [ %{"state" => "closed", "id" => "test"} ] }) - assert hd(json_response(conn, :bad_request))["error"] == "not_found" + assert hd(json_response_and_validate_schema(conn, :bad_request))["error"] == "not_found" end test "updates state of multiple reports", %{ @@ -169,13 +176,14 @@ test "updates state of multiple reports", %{ second_report_id: second_report_id } do conn + |> put_req_header("content-type", "application/json") |> patch("/api/pleroma/admin/reports", %{ "reports" => [ %{"state" => "resolved", "id" => id}, %{"state" => "closed", "id" => second_report_id} ] }) - |> json_response(:no_content) + |> json_response_and_validate_schema(:no_content) activity = Activity.get_by_id(id) second_activity = Activity.get_by_id(second_report_id) @@ -197,7 +205,7 @@ test "returns empty response when no reports created", %{conn: conn} do response = conn |> get("/api/pleroma/admin/reports") - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) assert Enum.empty?(response["reports"]) assert response["total"] == 0 @@ -217,7 +225,7 @@ test "returns reports", %{conn: conn} do response = conn |> get("/api/pleroma/admin/reports") - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) [report] = response["reports"] @@ -248,12 +256,10 @@ test "returns reports with specified state", %{conn: conn} do response = conn - |> get("/api/pleroma/admin/reports", %{ - "state" => "open" - }) - |> json_response(:ok) + |> get("/api/pleroma/admin/reports?state=open") + |> json_response_and_validate_schema(:ok) - [open_report] = response["reports"] + assert [open_report] = response["reports"] assert length(response["reports"]) == 1 assert open_report["id"] == first_report_id @@ -262,27 +268,22 @@ test "returns reports with specified state", %{conn: conn} do response = conn - |> get("/api/pleroma/admin/reports", %{ - "state" => "closed" - }) - |> json_response(:ok) + |> get("/api/pleroma/admin/reports?state=closed") + |> json_response_and_validate_schema(:ok) - [closed_report] = response["reports"] + assert [closed_report] = response["reports"] assert length(response["reports"]) == 1 assert closed_report["id"] == second_report_id assert response["total"] == 1 - response = - conn - |> get("/api/pleroma/admin/reports", %{ - "state" => "resolved" - }) - |> json_response(:ok) - - assert Enum.empty?(response["reports"]) - assert response["total"] == 0 + assert %{"total" => 0, "reports" => []} == + conn + |> get("/api/pleroma/admin/reports?state=resolved", %{ + "" => "" + }) + |> json_response_and_validate_schema(:ok) end test "returns 403 when requested by a non-admin" do @@ -302,7 +303,9 @@ test "returns 403 when requested by a non-admin" do test "returns 403 when requested by anonymous" do conn = get(build_conn(), "/api/pleroma/admin/reports") - assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."} + assert json_response(conn, :forbidden) == %{ + "error" => "Invalid credentials." + } end end @@ -318,11 +321,15 @@ test "returns 403 when requested by anonymous" do status_ids: [activity.id] }) - post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ content: "this is disgusting!" }) - post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ content: "this is disgusting2!" }) @@ -333,7 +340,7 @@ test "returns 403 when requested by anonymous" do end test "it creates report note", %{admin_id: admin_id, report_id: report_id} do - [note, _] = Repo.all(ReportNote) + assert [note, _] = Repo.all(ReportNote) assert %{ activity_id: ^report_id, @@ -345,7 +352,7 @@ test "it creates report note", %{admin_id: admin_id, report_id: report_id} do test "it returns reports with notes", %{conn: conn, admin: admin} do conn = get(conn, "/api/pleroma/admin/reports") - response = json_response(conn, 200) + response = json_response_and_validate_schema(conn, 200) notes = hd(response["reports"])["notes"] [note, _] = notes @@ -357,8 +364,7 @@ test "it returns reports with notes", %{conn: conn, admin: admin} do test "it deletes the note", %{conn: conn, report_id: report_id} do assert ReportNote |> Repo.all() |> length() == 2 - - [note, _] = Repo.all(ReportNote) + assert [note, _] = Repo.all(ReportNote) delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") From c020fd435216012f08812efdb9ee0c05352cec10 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 18:58:58 +0200 Subject: [PATCH 202/375] ChatMessageReferenceView: Return read status as `unread`. --- .../web/pleroma_api/views/chat_message_reference_view.ex | 2 +- .../pleroma_api/views/chat_message_reference_view_test.exs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex index ff170e162..f9405aec5 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex @@ -30,7 +30,7 @@ def render( attachment: chat_message["attachment"] && StatusView.render("attachment.json", attachment: chat_message["attachment"]), - seen: seen + unread: !seen } end diff --git a/test/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/web/pleroma_api/views/chat_message_reference_view_test.exs index 00024d52c..b53bd3490 100644 --- a/test/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -40,7 +40,7 @@ test "it displays a chat message" do assert chat_message[:account_id] == user.id assert chat_message[:chat_id] assert chat_message[:created_at] - assert chat_message[:seen] == true + assert chat_message[:unread] == false assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id) @@ -57,6 +57,6 @@ test "it displays a chat message" do assert chat_message_two[:account_id] == recipient.id assert chat_message_two[:chat_id] == chat_message[:chat_id] assert chat_message_two[:attachment] - assert chat_message_two[:seen] == false + assert chat_message_two[:unread] == true end end From b3407344d3acafa4a1271289d985632c058e7a6e Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 19:21:23 +0200 Subject: [PATCH 203/375] ChatController: Add function to mark single message as read. --- lib/pleroma/chat_message_reference.ex | 6 ++++ .../web/api_spec/operations/chat_operation.ex | 31 +++++++++++++++++-- .../controllers/chat_controller.ex | 23 +++++++++++++- lib/pleroma/web/router.ex | 1 + .../controllers/chat_controller_test.exs | 28 +++++++++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat_message_reference.ex index ad174b294..9b00443f5 100644 --- a/lib/pleroma/chat_message_reference.ex +++ b/lib/pleroma/chat_message_reference.ex @@ -92,6 +92,12 @@ def unread_count_for_chat(chat) do |> Repo.aggregate(:count) end + def mark_as_read(cm_ref) do + cm_ref + |> changeset(%{seen: true}) + |> Repo.update() + end + def set_all_seen_for_chat(chat) do chat |> for_chat_query() diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index a1c5db5dc..6ad325113 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -39,6 +39,31 @@ def mark_as_read_operation do } end + def mark_message_as_read_operation do + %Operation{ + tags: ["chat"], + summary: "Mark one message in the chat as read", + operationId: "ChatController.mark_message_as_read", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:message_id, :path, :string, "The ID of the message") + ], + responses: %{ + 200 => + Operation.response( + "The read ChatMessage", + "application/json", + ChatMessage + ) + }, + security: [ + %{ + "oAuth" => ["write"] + } + ] + } + end + def show_operation do %Operation{ tags: ["chat"], @@ -274,7 +299,8 @@ def chat_messages_response do "content" => "Check this out :firefox:", "id" => "13", "chat_id" => "1", - "actor_id" => "someflakeid" + "actor_id" => "someflakeid", + "unread" => false }, %{ "actor_id" => "someflakeid", @@ -282,7 +308,8 @@ def chat_messages_response do "id" => "12", "chat_id" => "1", "emojis" => [], - "created_at" => "2020-04-21T15:06:45.000Z" + "created_at" => "2020-04-21T15:06:45.000Z", + "unread" => false } ] } diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 29922da99..01d47045d 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -24,7 +24,13 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do plug( OAuthScopesPlug, %{scopes: ["write:statuses"]} - when action in [:post_chat_message, :create, :mark_as_read, :delete_message] + when action in [ + :post_chat_message, + :create, + :mark_as_read, + :mark_message_as_read, + :delete_message + ] ) plug( @@ -88,6 +94,21 @@ def post_chat_message( end end + def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ + id: chat_id, + message_id: message_id + }) do + with %ChatMessageReference{} = cm_ref <- + ChatMessageReference.get_by_id(message_id), + ^chat_id <- cm_ref.chat_id |> to_string(), + %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), + {:ok, cm_ref} <- ChatMessageReference.mark_as_read(cm_ref) do + conn + |> put_view(ChatMessageReferenceView) + |> render("show.json", for: user, chat_message_reference: cm_ref) + end + end + def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), {_n, _} <- ChatMessageReference.set_all_seen_for_chat(chat) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index fef277ac6..fd2dc82ca 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -313,6 +313,7 @@ defmodule Pleroma.Web.Router do post("/chats/:id/messages", ChatController, :post_chat_message) delete("/chats/:id/messages/:message_id", ChatController, :delete_message) post("/chats/:id/read", ChatController, :mark_as_read) + post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read) get("/conversations/:id/statuses", ConversationController, :statuses) get("/conversations/:id", ConversationController, :show) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index e62b71799..e7892142a 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -13,6 +13,33 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do import Pleroma.Factory + describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do + setup do: oauth_access(["write:statuses"]) + + test "it marks one message as read", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + + assert cm_ref.seen == false + + result = + conn + |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read") + |> json_response_and_validate_schema(200) + + assert result["unread"] == false + + cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + + assert cm_ref.seen == true + end + end + describe "POST /api/v1/pleroma/chats/:id/read" do setup do: oauth_access(["write:statuses"]) @@ -20,6 +47,7 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do other_user = insert(:user) {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) object = Object.normalize(create, false) cm_ref = ChatMessageReference.for_chat_and_object(chat, object) From 286bd8eb83e3fd9a2546e27c5e5d98f5316934a0 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 19:24:37 +0200 Subject: [PATCH 204/375] Docs: Add `mark_message_as_read` to docs --- docs/API/chats.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index d1d39f495..c0ef75664 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -94,6 +94,15 @@ Returned data: } ``` +### Marking a single chat message as read + +To set the `unread` property of a message to `false` + +`POST /api/v1/pleroma/chats/:id/messages/:message_id/read` + +Returned data: + +The modified chat message ### Getting a list of Chats @@ -149,7 +158,8 @@ Returned data: "visible_in_picker": false } ], - "id": "13" + "id": "13", + "unread": true }, { "account_id": "someflakeid", @@ -157,7 +167,8 @@ Returned data: "content": "Whats' up?", "created_at": "2020-04-21T15:06:45.000Z", "emojis": [], - "id": "12" + "id": "12", + "unread": false } ] ``` @@ -190,7 +201,8 @@ Returned data: "visible_in_picker": false } ], - "id": "13" + "id": "13", + "unread": false } ``` @@ -215,7 +227,8 @@ There's a new `pleroma:chat_mention` notification, which has this form. It is no "chat_id": "1", "id": "10", "content": "Hello", - "account_id": "someflakeid" + "account_id": "someflakeid", + "unread": false }, "created_at": "somedate" } From e213e3157737f87513999ef2aa00dffa735a8ada Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 19:25:57 +0200 Subject: [PATCH 205/375] Changelog: Add chats to changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839bf90ab..1cf2210f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** removed `with_move` parameter from notifications timeline. ### Added +- Chats: Added support for federated chats. For details, see the docs. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - Instance: Add `background_image` to configuration and `/api/v1/instance` - Instance: Extend `/api/v1/instance` with Pleroma-specific information. From e46aecda55b20c0d48463fb2a5c0040d4fc34e97 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 3 Jun 2020 20:51:59 +0200 Subject: [PATCH 206/375] Notification: Fix notifications backfill for compacted activities --- lib/pleroma/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 0f33d282d..455d214bf 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -412,7 +412,7 @@ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do object = Object.get_by_ap_id(activity.data["object"]) - case object.data["type"] do + case object && object.data["type"] do "ChatMessage" -> "pleroma:chat_mention" _ -> "mention" end From a8132690bd80b83fc0057566d78a49eceefe0349 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 4 Jun 2020 13:46:13 +0400 Subject: [PATCH 207/375] Fix credo --- test/web/admin_api/controllers/admin_api_controller_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index f4c37ae6e..2aaec510d 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1744,7 +1744,6 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do end end - describe "/api/pleroma/admin/stats" do test "status visibility count", %{conn: conn} do admin = insert(:user, is_admin: true) From 5d7dda883e76041025384e453da74110c550aa3b Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 14:46:41 +0200 Subject: [PATCH 208/375] SideEffectsTest: More tests. --- test/web/activity_pub/side_effects_test.exs | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 92c266d84..82d72119e 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -321,7 +321,27 @@ test "it streams the created ChatMessage" do {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - with_mock Pleroma.Web.Streamer, [], stream: fn _, _ -> nil end do + with_mock Pleroma.Web.Streamer, [], + stream: fn _, payload -> + case payload do + {^author, cm_ref} -> + assert cm_ref.seen == true + + {^recipient, cm_ref} -> + assert cm_ref.seen == false + + view = + Pleroma.Web.PleromaAPI.ChatView.render("show.json", + last_message: cm_ref, + chat: cm_ref.chat + ) + + assert view.unread == 1 + + _ -> + nil + end + end do {:ok, _create_activity, _meta} = SideEffects.handle(create_activity, local: false, object_data: chat_message_data) From b952f3f37907c735e3426ba43d01027f6f49c5b5 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 14:49:10 +0200 Subject: [PATCH 209/375] WebPush: Push out chat message notications. --- lib/pleroma/web/push/impl.ex | 3 ++- lib/pleroma/web/push/subscription.ex | 2 +- test/web/push/impl_test.exs | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 125f33755..006a242af 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -32,7 +32,7 @@ def perform( mastodon_type = notification.type gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) avatar_url = User.avatar_url(actor) - object = Object.normalize(activity) + object = Object.normalize(activity, false) user = User.get_cached_by_id(user_id) direct_conversation_id = Activity.direct_conversation_id(activity, user) @@ -171,6 +171,7 @@ def format_title(%{type: type}, mastodon_type) do "follow_request" -> "New Follow Request" "reblog" -> "New Repeat" "favourite" -> "New Favorite" + "pleroma:chat_mention" -> "New Chat Message" type -> "New #{String.capitalize(type || "event")}" end end diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 3e401a490..5b5aa0d59 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -25,7 +25,7 @@ defmodule Pleroma.Web.Push.Subscription do timestamps() end - @supported_alert_types ~w[follow favourite mention reblog]a + @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a defp alerts(%{data: %{alerts: alerts}}) do alerts = Map.take(alerts, @supported_alert_types) diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 26c65bc82..8fb7faaa5 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -196,6 +197,22 @@ test "renders title for create activity with direct visibility" do end describe "build_content/3" do + test "builds content for chat messages" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: hey", + title: "New Chat Message" + } + end + test "hides details for notifications when privacy option enabled" do user = insert(:user, nickname: "Bob") user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) From 6e103a18af6cfd7f454a911e2f0e1ae35cd45aa4 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 14:49:36 +0200 Subject: [PATCH 210/375] Docs: Document WebPush changes. --- docs/API/chats.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/API/chats.md b/docs/API/chats.md index c0ef75664..abeee698f 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -237,3 +237,7 @@ There's a new `pleroma:chat_mention` notification, which has this form. It is no ### Streaming There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. + +### Web Push + +If you want to receive push messages for this type, you'll need to add the `pleroma:chat_mention` type to your alerts in the push subscription. From 00748e9650e911d828dfe6f769ac20a6b31c8b69 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 17:14:42 +0200 Subject: [PATCH 211/375] ChatMessageReferences: Change seen -> unread --- lib/pleroma/chat_message_reference.ex | 18 +++++------ lib/pleroma/web/activity_pub/side_effects.ex | 2 +- .../views/chat_message_reference_view.ex | 4 +-- ...n_to_unread_in_chat_message_references.exs | 30 +++++++++++++++++++ test/web/activity_pub/side_effects_test.exs | 8 ++--- .../controllers/chat_controller_test.exs | 8 ++--- 6 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat_message_reference.ex index 9b00443f5..fc2aaae7a 100644 --- a/lib/pleroma/chat_message_reference.ex +++ b/lib/pleroma/chat_message_reference.ex @@ -23,15 +23,15 @@ defmodule Pleroma.ChatMessageReference do belongs_to(:object, Object) belongs_to(:chat, Chat) - field(:seen, :boolean, default: false) + field(:unread, :boolean, default: true) timestamps() end def changeset(struct, params) do struct - |> cast(params, [:object_id, :chat_id, :seen]) - |> validate_required([:object_id, :chat_id, :seen]) + |> cast(params, [:object_id, :chat_id, :unread]) + |> validate_required([:object_id, :chat_id, :unread]) end def get_by_id(id) do @@ -73,11 +73,11 @@ def last_message_for_chat(chat) do |> Repo.one() end - def create(chat, object, seen) do + def create(chat, object, unread) do params = %{ chat_id: chat.id, object_id: object.id, - seen: seen + unread: unread } %__MODULE__{} @@ -88,13 +88,13 @@ def create(chat, object, seen) do def unread_count_for_chat(chat) do chat |> for_chat_query() - |> where([cmr], cmr.seen == false) + |> where([cmr], cmr.unread == true) |> Repo.aggregate(:count) end def mark_as_read(cm_ref) do cm_ref - |> changeset(%{seen: true}) + |> changeset(%{unread: false}) |> Repo.update() end @@ -103,7 +103,7 @@ def set_all_seen_for_chat(chat) do |> for_chat_query() |> exclude(:order_by) |> exclude(:preload) - |> where([cmr], cmr.seen == false) - |> Repo.update_all(set: [seen: true]) + |> where([cmr], cmr.unread == true) + |> Repo.update_all(set: [unread: false]) end end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 0c5709356..e9f109d80 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -140,7 +140,7 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do |> Enum.each(fn [user, other_user] -> if user.local do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id == actor.ap_id) + {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id != actor.ap_id) Streamer.stream( ["user", "user:pleroma_chat"], diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex index f9405aec5..592bb17f0 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex @@ -16,7 +16,7 @@ def render( id: id, object: %{data: chat_message}, chat_id: chat_id, - seen: seen + unread: unread } } ) do @@ -30,7 +30,7 @@ def render( attachment: chat_message["attachment"] && StatusView.render("attachment.json", attachment: chat_message["attachment"]), - unread: !seen + unread: unread } end diff --git a/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs new file mode 100644 index 000000000..fd6bc7bc7 --- /dev/null +++ b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs @@ -0,0 +1,30 @@ +defmodule Pleroma.Repo.Migrations.MigrateSeenToUnreadInChatMessageReferences do + use Ecto.Migration + + def change do + drop( + index(:chat_message_references, [:chat_id], + where: "seen = false", + name: "unseen_messages_count_index" + ) + ) + + alter table(:chat_message_references) do + add(:unread, :boolean, default: true) + end + + execute("update chat_message_references set unread = not seen") + + alter table(:chat_message_references) do + modify(:unread, :boolean, default: true, null: false) + remove(:seen, :boolean, default: false, null: false) + end + + create( + index(:chat_message_references, [:chat_id], + where: "unread = true", + name: "unread_messages_count_index" + ) + ) + end +end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 82d72119e..40df664eb 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -325,10 +325,10 @@ test "it streams the created ChatMessage" do stream: fn _, payload -> case payload do {^author, cm_ref} -> - assert cm_ref.seen == true + assert cm_ref.unread == false {^recipient, cm_ref} -> - assert cm_ref.seen == false + assert cm_ref.unread == true view = Pleroma.Web.PleromaAPI.ChatView.render("show.json", @@ -369,14 +369,14 @@ test "it creates a Chat and ChatMessageReferences for the local users and bumps [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() assert cm_ref.object.data["content"] == "hey" - assert cm_ref.seen == true + assert cm_ref.unread == false chat = Chat.get(recipient.id, author.ap_id) [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() assert cm_ref.object.data["content"] == "hey" - assert cm_ref.seen == false + assert cm_ref.unread == true end test "it creates a Chat for the local users and bumps the unread count" do diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index e7892142a..7af6dec1c 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -25,7 +25,7 @@ test "it marks one message as read", %{conn: conn, user: user} do object = Object.normalize(create, false) cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert cm_ref.seen == false + assert cm_ref.unread == true result = conn @@ -36,7 +36,7 @@ test "it marks one message as read", %{conn: conn, user: user} do cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert cm_ref.seen == true + assert cm_ref.unread == false end end @@ -52,7 +52,7 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do object = Object.normalize(create, false) cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert cm_ref.seen == false + assert cm_ref.unread == true result = conn @@ -63,7 +63,7 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do cm_ref = ChatMessageReference.for_chat_and_object(chat, object) - assert cm_ref.seen == true + assert cm_ref.unread == false end end From 41503b167335a5f54eb122ecfd945018c9b50f90 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 15:16:10 +0000 Subject: [PATCH 212/375] Apply suggestion to test/web/activity_pub/transmogrifier/chat_message_test.exs --- test/web/activity_pub/transmogrifier/chat_message_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs index 820090de3..d6736dc3e 100644 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do alias Pleroma.Web.ActivityPub.Transmogrifier describe "handle_incoming" do - test "handles this" do + test "handles chonks with attachment" do data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "actor" => "https://honk.tedunangst.com/u/tedu", From 9a53f619e03cd515460a7b1570d339e4554e1740 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 15:16:15 +0000 Subject: [PATCH 213/375] Apply suggestion to test/chat_message_reference_test.exs --- test/chat_message_reference_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/chat_message_reference_test.exs b/test/chat_message_reference_test.exs index 963a0e225..66bf493b4 100644 --- a/test/chat_message_reference_test.exs +++ b/test/chat_message_reference_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.ChatMessageReferencTest do +defmodule Pleroma.ChatMessageReferenceTest do use Pleroma.DataCase, async: true alias Pleroma.Chat From 56dfa0e0fb0ca34054930e64e092f4a3a1b87fd1 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 19:22:49 +0200 Subject: [PATCH 214/375] Transmogrifier: Update notification after accepting. --- lib/pleroma/web/activity_pub/transmogrifier.ex | 1 + .../activity_pub/transmogrifier/follow_handling_test.exs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 886403fcd..b2461de2b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -538,6 +538,7 @@ def handle_incoming( {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, {_, false} <- {:user_locked, User.locked?(followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, + _ <- Notification.update_notification_type(followed, activity), {_, {:ok, _}} <- {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, {:ok, _relationship} <- diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 967389fae..6b003b51c 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Notification alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier @@ -57,9 +58,12 @@ test "it works for incoming follow requests" do activity = Repo.get(Activity, activity.id) assert activity.data["state"] == "accept" assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + + [notification] = Notification.for_user(user) + assert notification.type == "follow" end - test "with locked accounts, it does not create a follow or an accept" do + test "with locked accounts, it does create a Follow, but not an Accept" do user = insert(:user, locked: true) data = @@ -81,6 +85,9 @@ test "with locked accounts, it does not create a follow or an accept" do |> Repo.all() assert Enum.empty?(accepts) + + [notification] = Notification.for_user(user) + assert notification.type == "follow_request" end test "it works for follow requests when you are already followed, creating a new accept activity" do From 317e2b8d6126d86eafb493fe6c3b7a29af65ee21 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 4 Jun 2020 21:33:16 +0400 Subject: [PATCH 215/375] Use atoms as keys in `ActivityPub.fetch_*` functions options --- benchmarks/load_testing/fetcher.ex | 194 ++++---- .../mix/tasks/pleroma/benchmarks/tags.ex | 16 +- lib/pleroma/bbs/handler.ex | 8 +- lib/pleroma/conversation/participation.ex | 4 +- lib/pleroma/pagination.ex | 17 +- lib/pleroma/web/activity_pub/activity_pub.ex | 448 ++++++++---------- .../activity_pub/activity_pub_controller.ex | 16 +- lib/pleroma/web/activity_pub/utils.ex | 15 +- .../controllers/admin_api_controller.ex | 14 +- .../controllers/status_controller.ex | 10 +- .../web/admin_api/views/report_view.ex | 2 +- lib/pleroma/web/controller_helper.ex | 3 +- lib/pleroma/web/feed/tag_controller.ex | 4 +- lib/pleroma/web/feed/user_controller.ex | 6 +- .../controllers/account_controller.ex | 4 +- .../controllers/conversation_controller.ex | 17 - .../controllers/status_controller.ex | 11 +- .../controllers/timeline_controller.ex | 65 ++- .../mastodon_api/views/conversation_view.ex | 4 +- .../controllers/account_controller.ex | 7 +- .../controllers/conversation_controller.ex | 9 +- .../controllers/scrobble_controller.ex | 5 +- .../web/static_fe/static_fe_controller.ex | 8 +- test/pagination_test.exs | 12 +- test/tasks/relay_test.exs | 10 +- test/user_test.exs | 4 +- test/web/activity_pub/activity_pub_test.exs | 261 +++++----- 27 files changed, 538 insertions(+), 636 deletions(-) diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index 22a06e472..15fd06c3d 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -52,12 +52,12 @@ defp render_views(user) do defp opts_for_home_timeline(user) do %{ - "blocking_user" => user, - "count" => "20", - "muting_user" => user, - "type" => ["Create", "Announce"], - "user" => user, - "with_muted" => "true" + blocking_user: user, + count: "20", + muting_user: user, + type: ["Create", "Announce"], + user: user, + with_muted: true } end @@ -70,17 +70,17 @@ defp fetch_home_timeline(user) do ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last() second_page_last = - ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", first_page_last.id)) + ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, first_page_last.id)) |> Enum.reverse() |> List.last() third_page_last = - ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", second_page_last.id)) + ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, second_page_last.id)) |> Enum.reverse() |> List.last() forth_page_last = - ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", third_page_last.id)) + ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, third_page_last.id)) |> Enum.reverse() |> List.last() @@ -90,19 +90,19 @@ defp fetch_home_timeline(user) do }, inputs: %{ "1 page" => opts, - "2 page" => Map.put(opts, "max_id", first_page_last.id), - "3 page" => Map.put(opts, "max_id", second_page_last.id), - "4 page" => Map.put(opts, "max_id", third_page_last.id), - "5 page" => Map.put(opts, "max_id", forth_page_last.id), - "1 page only media" => Map.put(opts, "only_media", "true"), + "2 page" => Map.put(opts, :max_id, first_page_last.id), + "3 page" => Map.put(opts, :max_id, second_page_last.id), + "4 page" => Map.put(opts, :max_id, third_page_last.id), + "5 page" => Map.put(opts, :max_id, forth_page_last.id), + "1 page only media" => Map.put(opts, :only_media, true), "2 page only media" => - Map.put(opts, "max_id", first_page_last.id) |> Map.put("only_media", "true"), + Map.put(opts, :max_id, first_page_last.id) |> Map.put(:only_media, true), "3 page only media" => - Map.put(opts, "max_id", second_page_last.id) |> Map.put("only_media", "true"), + Map.put(opts, :max_id, second_page_last.id) |> Map.put(:only_media, true), "4 page only media" => - Map.put(opts, "max_id", third_page_last.id) |> Map.put("only_media", "true"), + Map.put(opts, :max_id, third_page_last.id) |> Map.put(:only_media, true), "5 page only media" => - Map.put(opts, "max_id", forth_page_last.id) |> Map.put("only_media", "true") + Map.put(opts, :max_id, forth_page_last.id) |> Map.put(:only_media, true) }, formatters: formatters() ) @@ -110,12 +110,12 @@ defp fetch_home_timeline(user) do defp opts_for_direct_timeline(user) do %{ - :visibility => "direct", - "blocking_user" => user, - "count" => "20", - "type" => "Create", - "user" => user, - "with_muted" => "true" + visibility: "direct", + blocking_user: user, + count: "20", + type: "Create", + user: user, + with_muted: true } end @@ -130,7 +130,7 @@ defp fetch_direct_timeline(user) do |> Pagination.fetch_paginated(opts) |> List.last() - opts2 = Map.put(opts, "max_id", first_page_last.id) + opts2 = Map.put(opts, :max_id, first_page_last.id) second_page_last = recipients @@ -138,7 +138,7 @@ defp fetch_direct_timeline(user) do |> Pagination.fetch_paginated(opts2) |> List.last() - opts3 = Map.put(opts, "max_id", second_page_last.id) + opts3 = Map.put(opts, :max_id, second_page_last.id) third_page_last = recipients @@ -146,7 +146,7 @@ defp fetch_direct_timeline(user) do |> Pagination.fetch_paginated(opts3) |> List.last() - opts4 = Map.put(opts, "max_id", third_page_last.id) + opts4 = Map.put(opts, :max_id, third_page_last.id) forth_page_last = recipients @@ -165,7 +165,7 @@ defp fetch_direct_timeline(user) do "2 page" => opts2, "3 page" => opts3, "4 page" => opts4, - "5 page" => Map.put(opts4, "max_id", forth_page_last.id) + "5 page" => Map.put(opts4, :max_id, forth_page_last.id) }, formatters: formatters() ) @@ -173,34 +173,34 @@ defp fetch_direct_timeline(user) do defp opts_for_public_timeline(user) do %{ - "type" => ["Create", "Announce"], - "local_only" => false, - "blocking_user" => user, - "muting_user" => user + type: ["Create", "Announce"], + local_only: false, + blocking_user: user, + muting_user: user } end defp opts_for_public_timeline(user, :local) do %{ - "type" => ["Create", "Announce"], - "local_only" => true, - "blocking_user" => user, - "muting_user" => user + type: ["Create", "Announce"], + local_only: true, + blocking_user: user, + muting_user: user } end defp opts_for_public_timeline(user, :tag) do %{ - "blocking_user" => user, - "count" => "20", - "local_only" => nil, - "muting_user" => user, - "tag" => ["tag"], - "tag_all" => [], - "tag_reject" => [], - "type" => "Create", - "user" => user, - "with_muted" => "true" + blocking_user: user, + count: "20", + local_only: nil, + muting_user: user, + tag: ["tag"], + tag_all: [], + tag_reject: [], + type: "Create", + user: user, + with_muted: true } end @@ -223,7 +223,7 @@ defp fetch_public_timeline(user, :tag) do end defp fetch_public_timeline(user, :only_media) do - opts = opts_for_public_timeline(user) |> Map.put("only_media", "true") + opts = opts_for_public_timeline(user) |> Map.put(:only_media, true) fetch_public_timeline(opts, "public timeline only media") end @@ -245,15 +245,13 @@ defp fetch_public_timeline(user, :with_blocks) do user = User.get_by_id(user.id) - opts = Map.put(opts, "blocking_user", user) + opts = Map.put(opts, :blocking_user, user) - Benchee.run( - %{ - "public timeline with user block" => fn -> - ActivityPub.fetch_public_activities(opts) - end - }, - ) + Benchee.run(%{ + "public timeline with user block" => fn -> + ActivityPub.fetch_public_activities(opts) + end + }) domains = Enum.reduce(remote_non_friends, [], fn non_friend, domains -> @@ -269,30 +267,28 @@ defp fetch_public_timeline(user, :with_blocks) do end) user = User.get_by_id(user.id) - opts = Map.put(opts, "blocking_user", user) + opts = Map.put(opts, :blocking_user, user) - Benchee.run( - %{ - "public timeline with domain block" => fn opts -> - ActivityPub.fetch_public_activities(opts) - end - } - ) + Benchee.run(%{ + "public timeline with domain block" => fn -> + ActivityPub.fetch_public_activities(opts) + end + }) end defp fetch_public_timeline(opts, title) when is_binary(title) do first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() second_page_last = - ActivityPub.fetch_public_activities(Map.put(opts, "max_id", first_page_last.id)) + ActivityPub.fetch_public_activities(Map.put(opts, :max_id, first_page_last.id)) |> List.last() third_page_last = - ActivityPub.fetch_public_activities(Map.put(opts, "max_id", second_page_last.id)) + ActivityPub.fetch_public_activities(Map.put(opts, :max_id, second_page_last.id)) |> List.last() forth_page_last = - ActivityPub.fetch_public_activities(Map.put(opts, "max_id", third_page_last.id)) + ActivityPub.fetch_public_activities(Map.put(opts, :max_id, third_page_last.id)) |> List.last() Benchee.run( @@ -303,17 +299,17 @@ defp fetch_public_timeline(opts, title) when is_binary(title) do }, inputs: %{ "1 page" => opts, - "2 page" => Map.put(opts, "max_id", first_page_last.id), - "3 page" => Map.put(opts, "max_id", second_page_last.id), - "4 page" => Map.put(opts, "max_id", third_page_last.id), - "5 page" => Map.put(opts, "max_id", forth_page_last.id) + "2 page" => Map.put(opts, :max_id, first_page_last.id), + "3 page" => Map.put(opts, :max_id, second_page_last.id), + "4 page" => Map.put(opts, :max_id, third_page_last.id), + "5 page" => Map.put(opts, :max_id, forth_page_last.id) }, formatters: formatters() ) end defp opts_for_notifications do - %{"count" => "20", "with_muted" => "true"} + %{count: "20", with_muted: true} end defp fetch_notifications(user) do @@ -322,15 +318,15 @@ defp fetch_notifications(user) do first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last() second_page_last = - MastodonAPI.get_notifications(user, Map.put(opts, "max_id", first_page_last.id)) + MastodonAPI.get_notifications(user, Map.put(opts, :max_id, first_page_last.id)) |> List.last() third_page_last = - MastodonAPI.get_notifications(user, Map.put(opts, "max_id", second_page_last.id)) + MastodonAPI.get_notifications(user, Map.put(opts, :max_id, second_page_last.id)) |> List.last() forth_page_last = - MastodonAPI.get_notifications(user, Map.put(opts, "max_id", third_page_last.id)) + MastodonAPI.get_notifications(user, Map.put(opts, :max_id, third_page_last.id)) |> List.last() Benchee.run( @@ -341,10 +337,10 @@ defp fetch_notifications(user) do }, inputs: %{ "1 page" => opts, - "2 page" => Map.put(opts, "max_id", first_page_last.id), - "3 page" => Map.put(opts, "max_id", second_page_last.id), - "4 page" => Map.put(opts, "max_id", third_page_last.id), - "5 page" => Map.put(opts, "max_id", forth_page_last.id) + "2 page" => Map.put(opts, :max_id, first_page_last.id), + "3 page" => Map.put(opts, :max_id, second_page_last.id), + "4 page" => Map.put(opts, :max_id, third_page_last.id), + "5 page" => Map.put(opts, :max_id, forth_page_last.id) }, formatters: formatters() ) @@ -354,13 +350,13 @@ defp fetch_favourites(user) do first_page_last = ActivityPub.fetch_favourites(user) |> List.last() second_page_last = - ActivityPub.fetch_favourites(user, %{"max_id" => first_page_last.id}) |> List.last() + ActivityPub.fetch_favourites(user, %{:max_id => first_page_last.id}) |> List.last() third_page_last = - ActivityPub.fetch_favourites(user, %{"max_id" => second_page_last.id}) |> List.last() + ActivityPub.fetch_favourites(user, %{:max_id => second_page_last.id}) |> List.last() forth_page_last = - ActivityPub.fetch_favourites(user, %{"max_id" => third_page_last.id}) |> List.last() + ActivityPub.fetch_favourites(user, %{:max_id => third_page_last.id}) |> List.last() Benchee.run( %{ @@ -370,10 +366,10 @@ defp fetch_favourites(user) do }, inputs: %{ "1 page" => %{}, - "2 page" => %{"max_id" => first_page_last.id}, - "3 page" => %{"max_id" => second_page_last.id}, - "4 page" => %{"max_id" => third_page_last.id}, - "5 page" => %{"max_id" => forth_page_last.id} + "2 page" => %{:max_id => first_page_last.id}, + "3 page" => %{:max_id => second_page_last.id}, + "4 page" => %{:max_id => third_page_last.id}, + "5 page" => %{:max_id => forth_page_last.id} }, formatters: formatters() ) @@ -381,8 +377,8 @@ defp fetch_favourites(user) do defp opts_for_long_thread(user) do %{ - "blocking_user" => user, - "user" => user + blocking_user: user, + user: user } end @@ -392,9 +388,9 @@ defp fetch_long_thread(user) do opts = opts_for_long_thread(user) - private_input = {private.data["context"], Map.put(opts, "exclude_id", private.id)} + private_input = {private.data["context"], Map.put(opts, :exclude_id, private.id)} - public_input = {public.data["context"], Map.put(opts, "exclude_id", public.id)} + public_input = {public.data["context"], Map.put(opts, :exclude_id, public.id)} Benchee.run( %{ @@ -514,13 +510,13 @@ defp render_long_thread(user) do public_context = ActivityPub.fetch_activities_for_context( public.data["context"], - Map.put(fetch_opts, "exclude_id", public.id) + Map.put(fetch_opts, :exclude_id, public.id) ) private_context = ActivityPub.fetch_activities_for_context( private.data["context"], - Map.put(fetch_opts, "exclude_id", private.id) + Map.put(fetch_opts, :exclude_id, private.id) ) Benchee.run( @@ -551,14 +547,14 @@ defp fetch_timelines_with_reply_filtering(user) do end, "Public timeline with reply filtering - following" => fn -> public_params - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() end, "Public timeline with reply filtering - self" => fn -> public_params - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() end }, @@ -577,16 +573,16 @@ defp fetch_timelines_with_reply_filtering(user) do "Home timeline with reply filtering - following" => fn -> private_params = private_params - |> Map.put("reply_filtering_user", user) - |> Map.put("reply_visibility", "following") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") ActivityPub.fetch_activities(recipients, private_params) end, "Home timeline with reply filtering - self" => fn -> private_params = private_params - |> Map.put("reply_filtering_user", user) - |> Map.put("reply_visibility", "self") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "self") ActivityPub.fetch_activities(recipients, private_params) end diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex index 1162b2e06..c051335a5 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex @@ -100,14 +100,14 @@ defp hashtag_fetching(params, user, local_only) do _activities = params - |> Map.put("type", "Create") - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("tag", tags) - |> Map.put("tag_all", tag_all) - |> Map.put("tag_reject", tag_reject) + |> Map.put(:type, "Create") + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:tag, tags) + |> Map.put(:tag_all, tag_all) + |> Map.put(:tag_reject, tag_reject) |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() end end diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index 12d64c2fe..cd523cf7d 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -92,10 +92,10 @@ def handle_command(state, "home") do params = %{} - |> Map.put("type", ["Create"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities = [user.ap_id | Pleroma.User.following(user)] diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 51bb1bda9..ce7bd2396 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -163,8 +163,8 @@ def for_user_with_last_activity_id(user, params \\ %{}) do |> Enum.map(fn participation -> activity_id = ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - "user" => user, - "blocking_user" => user + user: user, + blocking_user: user }) %{ diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index d43a96cd2..0ccc7b1f2 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -16,19 +16,16 @@ defmodule Pleroma.Pagination do @default_limit 20 @max_limit 40 - @page_keys ["max_id", "min_id", "limit", "since_id", "order"] - - def page_keys, do: @page_keys @spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil) - def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do + def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do total = Repo.aggregate(query, :count, :id) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding) + items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding) } end @@ -41,7 +38,7 @@ def fetch_paginated(query, params, :keyset, table_binding) do |> enforce_order(options) end - def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do + def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do total = query |> Ecto.Query.exclude(:left_join) @@ -49,7 +46,7 @@ def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding) + items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding) } end @@ -90,12 +87,6 @@ defp cast_params(params) do skip_order: :boolean } - params = - Enum.reduce(params, %{}, fn - {key, _value}, acc when is_atom(key) -> Map.drop(acc, [key]) - {key, value}, acc -> Map.put(acc, key, value) - end) - changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset.changes end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 958f3e5af..ef21f180b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -67,16 +67,12 @@ defp get_recipients(data) do {recipients, to, cc} end - defp check_actor_is_active(actor) do - if not is_nil(actor) do - with user <- User.get_cached_by_ap_id(actor), - false <- user.deactivated do - true - else - _e -> false - end - else - true + defp check_actor_is_active(nil), do: true + + defp check_actor_is_active(actor) when is_binary(actor) do + case User.get_cached_by_ap_id(actor) do + %User{deactivated: deactivated} -> not deactivated + _ -> false end end @@ -87,7 +83,7 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil( defp check_remote_limit(_), do: true - def increase_note_count_if_public(actor, object) do + defp increase_note_count_if_public(actor, object) do if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor} end @@ -95,36 +91,26 @@ def decrease_note_count_if_public(actor, object) do if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor} end - def increase_replies_count_if_reply(%{ - "object" => %{"inReplyTo" => reply_ap_id} = object, - "type" => "Create" - }) do + defp increase_replies_count_if_reply(%{ + "object" => %{"inReplyTo" => reply_ap_id} = object, + "type" => "Create" + }) do if is_public?(object) do Object.increase_replies_count(reply_ap_id) end end - def increase_replies_count_if_reply(_create_data), do: :noop + defp increase_replies_count_if_reply(_create_data), do: :noop - def decrease_replies_count_if_reply(%Object{ - data: %{"inReplyTo" => reply_ap_id} = object - }) do - if is_public?(object) do - Object.decrease_replies_count(reply_ap_id) - end - end - - def decrease_replies_count_if_reply(_object), do: :noop - - def increase_poll_votes_if_vote(%{ - "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, - "type" => "Create", - "actor" => actor - }) do + defp increase_poll_votes_if_vote(%{ + "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, + "type" => "Create", + "actor" => actor + }) do Object.increase_vote_count(reply_ap_id, name, actor) end - def increase_poll_votes_if_vote(_create_data), do: :noop + defp increase_poll_votes_if_vote(_create_data), do: :noop @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} def persist(object, meta) do @@ -203,8 +189,8 @@ def notify_and_stream(activity) do defp create_or_bump_conversation(activity, actor) do with {:ok, conversation} <- Conversation.create_or_bump_for(activity), - %User{} = user <- User.get_cached_by_ap_id(actor), - Participation.mark_as_read(user, conversation) do + %User{} = user <- User.get_cached_by_ap_id(actor) do + Participation.mark_as_read(user, conversation) {:ok, conversation} end end @@ -226,13 +212,15 @@ def stream_out_participations(participations) do end def stream_out_participations(%Object{data: %{"context" => context}}, user) do - with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), - conversation = Repo.preload(conversation, :participations), - last_activity_id = - fetch_latest_activity_id_for_context(conversation.ap_id, %{ - "user" => user, - "blocking_user" => user - }) do + with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do + conversation = Repo.preload(conversation, :participations) + + last_activity_id = + fetch_latest_activity_id_for_context(conversation.ap_id, %{ + user: user, + blocking_user: user + }) + if last_activity_id do stream_out_participations(conversation.participations) end @@ -266,12 +254,13 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param published = params[:published] quick_insert? = Config.get([:env]) == :benchmark - with create_data <- - make_create_data( - %{to: to, actor: actor, published: published, context: context, object: object}, - additional - ), - {:ok, activity} <- insert(create_data, local, fake), + create_data = + make_create_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ) + + with {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), _ <- increase_poll_votes_if_vote(create_data), @@ -299,12 +288,13 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d local = !(params[:local] == false) published = params[:published] - with listen_data <- - make_listen_data( - %{to: to, actor: actor, published: published, context: context, object: object}, - additional - ), - {:ok, activity} <- insert(listen_data, local), + listen_data = + make_listen_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ) + + with {:ok, activity} <- insert(listen_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -322,14 +312,15 @@ def reject(params) do end @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()} - def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do + defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do local = Map.get(params, :local, true) activity_id = Map.get(params, :activity_id, nil) - with data <- - %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} - |> Utils.maybe_put("id", activity_id), - {:ok, activity} <- insert(data, local), + data = + %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} + |> Utils.maybe_put("id", activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -341,15 +332,17 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do local = !(params[:local] == false) activity_id = params[:activity_id] - with data <- %{ - "to" => to, - "cc" => cc, - "type" => "Update", - "actor" => actor, - "object" => object - }, - data <- Utils.maybe_put(data, "id", activity_id), - {:ok, activity} <- insert(data, local), + data = + %{ + "to" => to, + "cc" => cc, + "type" => "Update", + "actor" => actor, + "object" => object + } + |> Utils.maybe_put("id", activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -366,8 +359,9 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do end defp do_follow(follower, followed, activity_id, local) do - with data <- make_follow_data(follower, followed, activity_id), - {:ok, activity} <- insert(data, local), + data = make_follow_data(follower, followed, activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -411,13 +405,13 @@ def block(blocker, blocked, activity_id \\ nil, local \\ true) do defp do_block(blocker, blocked, activity_id, local) do unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) - if unfollow_blocked do - follow_activity = fetch_latest_follow(blocker, blocked) - if follow_activity, do: unfollow(blocker, blocked, nil, local) + if unfollow_blocked and fetch_latest_follow(blocker, blocked) do + unfollow(blocker, blocked, nil, local) end - with block_data <- make_block_data(blocker, blocked, activity_id), - {:ok, activity} <- insert(block_data, local), + block_data = make_block_data(blocker, blocked, activity_id) + + with {:ok, activity} <- insert(block_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -496,8 +490,8 @@ def fetch_activities_for_context_query(context, opts) do public = [Constants.as_public()] recipients = - if opts["user"], - do: [opts["user"].ap_id | User.following(opts["user"])] ++ public, + if opts[:user], + do: [opts[:user].ap_id | User.following(opts[:user])] ++ public, else: public from(activity in Activity) @@ -505,7 +499,7 @@ def fetch_activities_for_context_query(context, opts) do |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) - |> restrict_recipients(recipients, opts["user"]) + |> restrict_recipients(recipients, opts[:user]) |> where( [activity], fragment( @@ -532,7 +526,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do FlakeId.Ecto.CompatType.t() | nil def fetch_latest_activity_id_for_context(context, opts \\ %{}) do context - |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) + |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts)) |> limit(1) |> select([a], a.id) |> Repo.one() @@ -540,24 +534,18 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do - opts = Map.drop(opts, ["user"]) + opts = Map.delete(opts, :user) - query = fetch_activities_query([Constants.as_public()], opts) - - query = - if opts["restrict_unlisted"] do - restrict_unlisted(query) - else - query - end - - Pagination.fetch_paginated(query, opts, pagination) + [Constants.as_public()] + |> fetch_activities_query(opts) + |> restrict_unlisted(opts) + |> Pagination.fetch_paginated(opts, pagination) end @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do opts - |> Map.put("restrict_unlisted", true) + |> Map.put(:restrict_unlisted, true) |> fetch_public_or_unlisted_activities(pagination) end @@ -566,20 +554,17 @@ def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do defp restrict_visibility(query, %{visibility: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do - query = - from( - a in query, - where: - fragment( - "activity_visibility(?, ?, ?) = ANY (?)", - a.actor, - a.recipients, - a.data, - ^visibility - ) - ) - - query + from( + a in query, + where: + fragment( + "activity_visibility(?, ?, ?) = ANY (?)", + a.actor, + a.recipients, + a.data, + ^visibility + ) + ) else Logger.error("Could not restrict visibility to #{visibility}") end @@ -601,7 +586,7 @@ defp restrict_visibility(_query, %{visibility: visibility}) defp restrict_visibility(query, _visibility), do: query - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do from( @@ -621,7 +606,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) end end - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility in @valid_visibilities do from( a in query, @@ -636,7 +621,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) ) end - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility not in [nil | @valid_visibilities] do Logger.error("Could not exclude visibility to #{visibility}") query @@ -647,14 +632,10 @@ defp exclude_visibility(query, _visibility), do: query defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _), do: query - defp restrict_thread_visibility( - query, - %{"user" => %User{skip_thread_containment: true}}, - _ - ), - do: query + defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _), + do: query - defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do + defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do from( a in query, where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) @@ -666,87 +647,79 @@ defp restrict_thread_visibility(query, _, _), do: query def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do params = params - |> Map.put("user", reading_user) - |> Map.put("actor_id", user.ap_id) + |> Map.put(:user, reading_user) + |> Map.put(:actor_id, user.ap_id) - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params) |> Enum.reverse() end def fetch_user_activities(user, reading_user, params \\ %{}) do params = params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("user", reading_user) - |> Map.put("actor_id", user.ap_id) - |> Map.put("pinned_activity_ids", user.pinned_activities) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:user, reading_user) + |> Map.put(:actor_id, user.ap_id) + |> Map.put(:pinned_activity_ids, user.pinned_activities) params = if User.blocks?(reading_user, user) do params else params - |> Map.put("blocking_user", reading_user) - |> Map.put("muting_user", reading_user) + |> Map.put(:blocking_user, reading_user) + |> Map.put(:muting_user, reading_user) end - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params) |> Enum.reverse() end def fetch_statuses(reading_user, params) do - params = - params - |> Map.put("type", ["Create", "Announce"]) + params = Map.put(params, :type, ["Create", "Announce"]) - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params, :offset) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params, :offset) |> Enum.reverse() end - defp user_activities_recipients(%{"godmode" => true}) do - [] - end + defp user_activities_recipients(%{godmode: true}), do: [] - defp user_activities_recipients(%{"reading_user" => reading_user}) do + defp user_activities_recipients(%{reading_user: reading_user}) do if reading_user do - [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)] + [Constants.as_public(), reading_user.ap_id | User.following(reading_user)] else [Constants.as_public()] end end - defp restrict_since(query, %{"since_id" => ""}), do: query + defp restrict_since(query, %{since_id: ""}), do: query - defp restrict_since(query, %{"since_id" => since_id}) do + defp restrict_since(query, %{since_id: since_id}) do from(activity in query, where: activity.id > ^since_id) end defp restrict_since(query, _), do: query - defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do + defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) - when is_list(tag_reject) and tag_reject != [] do + defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do from( [_activity, object] in query, where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject) @@ -755,12 +728,11 @@ defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) defp restrict_tag_reject(query, _), do: query - defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do + defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag_all(query, %{"tag_all" => tag_all}) - when is_list(tag_all) and tag_all != [] do + defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all) @@ -769,18 +741,18 @@ defp restrict_tag_all(query, %{"tag_all" => tag_all}) defp restrict_tag_all(query, _), do: query - defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do + defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do + defp restrict_tag(query, %{tag: tag}) when is_list(tag) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag) ) end - defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do + defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\? (?)", object.data, ^tag) @@ -803,35 +775,35 @@ defp restrict_recipients(query, recipients, user) do ) end - defp restrict_local(query, %{"local_only" => true}) do + defp restrict_local(query, %{local_only: true}) do from(activity in query, where: activity.local == true) end defp restrict_local(query, _), do: query - defp restrict_actor(query, %{"actor_id" => actor_id}) do + defp restrict_actor(query, %{actor_id: actor_id}) do from(activity in query, where: activity.actor == ^actor_id) end defp restrict_actor(query, _), do: query - defp restrict_type(query, %{"type" => type}) when is_binary(type) do + defp restrict_type(query, %{type: type}) when is_binary(type) do from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type)) end - defp restrict_type(query, %{"type" => type}) do + defp restrict_type(query, %{type: type}) do from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type)) end defp restrict_type(query, _), do: query - defp restrict_state(query, %{"state" => state}) do + defp restrict_state(query, %{state: state}) do from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state)) end defp restrict_state(query, _), do: query - defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do + defp restrict_favorited_by(query, %{favorited_by: ap_id}) do from( [_activity, object] in query, where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id) @@ -840,11 +812,11 @@ defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, _), do: query - defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do + defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do + defp restrict_media(query, %{only_media: true}) do from( [_activity, object] in query, where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) @@ -853,7 +825,7 @@ defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1 defp restrict_media(query, _), do: query - defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do + defp restrict_replies(query, %{exclude_replies: true}) do from( [_activity, object] in query, where: fragment("?->>'inReplyTo' is null", object.data) @@ -861,8 +833,8 @@ defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "tr end defp restrict_replies(query, %{ - "reply_filtering_user" => user, - "reply_visibility" => "self" + reply_filtering_user: user, + reply_visibility: "self" }) do from( [activity, object] in query, @@ -877,8 +849,8 @@ defp restrict_replies(query, %{ end defp restrict_replies(query, %{ - "reply_filtering_user" => user, - "reply_visibility" => "following" + reply_filtering_user: user, + reply_visibility: "following" }) do from( [activity, object] in query, @@ -897,16 +869,16 @@ defp restrict_replies(query, %{ defp restrict_replies(query, _), do: query - defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do + defp restrict_reblogs(query, %{exclude_reblogs: true}) do from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data)) end defp restrict_reblogs(query, _), do: query - defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query + defp restrict_muted(query, %{with_muted: true}), do: query - defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user) + defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do + mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user) query = from([activity] in query, @@ -914,7 +886,7 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes) ) - unless opts["skip_preload"] do + unless opts[:skip_preload] do from([thread_mute: tm] in query, where: is_nil(tm.user_id)) else query @@ -923,8 +895,8 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query - defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do - blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) + defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do + blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) domain_blocks = user.domain_blocks || [] following_ap_ids = User.get_friends_ap_ids(user) @@ -970,7 +942,7 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do defp restrict_blocked(query, _), do: query - defp restrict_unlisted(query) do + defp restrict_unlisted(query, %{restrict_unlisted: true}) do from( activity in query, where: @@ -982,19 +954,16 @@ defp restrict_unlisted(query) do ) end - # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only, - # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2' - # and `restrict_muted/2` + defp restrict_unlisted(query, _), do: query - defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids}) - when pinned in [true, "true", "1"] do + defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do from(activity in query, where: activity.id in ^ids) end defp restrict_pinned(query, _), do: query - defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do - muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user) + defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do + muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user) from( activity in query, @@ -1010,7 +979,7 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted_reblogs(query, _), do: query - defp restrict_instance(query, %{"instance" => instance}) do + defp restrict_instance(query, %{instance: instance}) do users = from( u in User, @@ -1024,7 +993,7 @@ defp restrict_instance(query, %{"instance" => instance}) do defp restrict_instance(query, _), do: query - defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do if has_named_binding?(query, :object) do @@ -1036,7 +1005,7 @@ defp exclude_poll_votes(query, _) do end end - defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query + defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query defp exclude_invisible_actors(query, _opts) do invisible_ap_ids = @@ -1047,38 +1016,38 @@ defp exclude_invisible_actors(query, _opts) do from([activity] in query, where: activity.actor not in ^invisible_ap_ids) end - defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do + defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do from(activity in query, where: activity.id != ^id) end defp exclude_id(query, _), do: query - defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query + defp maybe_preload_objects(query, %{skip_preload: true}), do: query defp maybe_preload_objects(query, _) do query |> Activity.with_preloaded_object() end - defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query + defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query defp maybe_preload_bookmarks(query, opts) do query - |> Activity.with_preloaded_bookmark(opts["user"]) + |> Activity.with_preloaded_bookmark(opts[:user]) end - defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do + defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do query |> Activity.with_preloaded_report_notes() end defp maybe_preload_report_notes(query, _), do: query - defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query + defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query defp maybe_set_thread_muted_field(query, opts) do query - |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"]) + |> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user]) end defp maybe_order(query, %{order: :desc}) do @@ -1094,24 +1063,23 @@ defp maybe_order(query, %{order: :asc}) do defp maybe_order(query, _), do: query defp fetch_activities_query_ap_ids_ops(opts) do - source_user = opts["muting_user"] + source_user = opts[:muting_user] ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: [] ap_id_relationships = - ap_id_relationships ++ - if opts["blocking_user"] && opts["blocking_user"] == source_user do - [:block] - else - [] - end + if opts[:blocking_user] && opts[:blocking_user] == source_user do + [:block | ap_id_relationships] + else + ap_id_relationships + end 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) + 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) restrict_muted_reblogs_opts = - Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts) + Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts) {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} end @@ -1130,7 +1098,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) - |> restrict_recipients(recipients, opts["user"]) + |> restrict_recipients(recipients, opts[:user]) |> restrict_replies(opts) |> restrict_tag(opts) |> restrict_tag_reject(opts) @@ -1157,12 +1125,12 @@ def fetch_activities_query(recipients, opts \\ %{}) do end def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do - list_memberships = Pleroma.List.memberships(opts["user"]) + list_memberships = Pleroma.List.memberships(opts[:user]) fetch_activities_query(recipients ++ list_memberships, opts) |> Pagination.fetch_paginated(opts, pagination) |> Enum.reverse() - |> maybe_update_cc(list_memberships, opts["user"]) + |> maybe_update_cc(list_memberships, opts[:user]) end @doc """ @@ -1178,16 +1146,15 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do |> select([_like, object, activity], %{activity | object: object}) |> order_by([like, _, _], desc_nulls_last: like.id) |> Pagination.fetch_paginated( - Map.merge(params, %{"skip_order" => true}), + Map.merge(params, %{skip_order: true}), pagination, :object_activity ) end - defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) - when is_list(list_memberships) and length(list_memberships) > 0 do + defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do Enum.map(activities, fn - %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> + %{data: %{"bcc" => [_ | _] = bcc}} = activity -> if Enum.any?(bcc, &(&1 in list_memberships)) do update_in(activity.data["cc"], &[user_ap_id | &1]) else @@ -1201,7 +1168,7 @@ defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) defp maybe_update_cc(activities, _, _), do: activities - def fetch_activities_bounded_query(query, recipients, recipients_with_public) do + defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do from(activity in query, where: fragment("? && ?", activity.recipients, ^recipients) or @@ -1276,8 +1243,8 @@ defp object_to_user_data(data) do %{"type" => "Emoji"} -> true _ -> false end) - |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc -> - Map.put(acc, String.trim(name, ":"), url) + |> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} end) locked = data["manuallyApprovesFollowers"] || false @@ -1323,18 +1290,15 @@ defp object_to_user_data(data) do } # nickname can be nil because of virtual actors - user_data = - if data["preferredUsername"] do - Map.put( - user_data, - :nickname, - "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" - ) - else - Map.put(user_data, :nickname, nil) - end - - {:ok, user_data} + if data["preferredUsername"] do + Map.put( + user_data, + :nickname, + "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" + ) + else + Map.put(user_data, :nickname, nil) + end end def fetch_follow_information_for_user(user) do @@ -1409,9 +1373,8 @@ defp collection_private(%{"first" => first}) do defp collection_private(_data), do: {:ok, true} def user_data_from_user_object(data) do - with {:ok, data} <- MRF.filter(data), - {:ok, data} <- object_to_user_data(data) do - {:ok, data} + with {:ok, data} <- MRF.filter(data) do + {:ok, object_to_user_data(data)} else e -> {:error, e} end @@ -1419,15 +1382,14 @@ def user_data_from_user_object(data) do def fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), - {:ok, data} <- user_data_from_user_object(data), - data <- maybe_update_follow_information(data) do - {:ok, data} + {:ok, data} <- user_data_from_user_object(data) do + {:ok, maybe_update_follow_information(data)} else - {:error, "Object has been deleted"} = e -> + {:error, "Object has been deleted" = e} -> Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} - e -> + {:error, e} -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} end @@ -1450,8 +1412,6 @@ def make_user_from_ap_id(ap_id) do |> Repo.insert() |> User.set_cache() end - else - e -> {:error, e} end end end @@ -1465,7 +1425,7 @@ def make_user_from_nickname(nickname) do end # filter out broken threads - def contain_broken_threads(%Activity{} = activity, %User{} = user) do + defp contain_broken_threads(%Activity{} = activity, %User{} = user) do entire_thread_visible_for_user?(activity, user) end @@ -1476,7 +1436,7 @@ def contain_activity(%Activity{} = activity, %User{} = user) do def fetch_direct_messages_query do Activity - |> restrict_type(%{"type" => "Create"}) + |> restrict_type(%{type: "Create"}) |> restrict_visibility(%{visibility: "direct"}) |> order_by([activity], asc: activity.id) end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 28727d619..55947925e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -233,16 +233,16 @@ def outbox( activities = if params["max_id"] do ActivityPub.fetch_user_activities(user, for_user, %{ - "max_id" => params["max_id"], + 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 - "include_poll_votes" => true, - "limit" => 10 + include_poll_votes: true, + limit: 10 }) else ActivityPub.fetch_user_activities(user, for_user, %{ - "limit" => 10, - "include_poll_votes" => true + limit: 10, + include_poll_votes: true }) end @@ -356,11 +356,11 @@ def read_inbox( activities = if params["max_id"] do ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{ - "max_id" => params["max_id"], - "limit" => 10 + max_id: params["max_id"], + limit: 10 }) else - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10}) + ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{limit: 10}) end conn diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index a76a699ee..1c40afdb2 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -244,7 +244,7 @@ defp lazy_put_object_defaults(activity, _), do: activity Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) - when is_map(object_data) and type in @supported_object_types do + when type in @supported_object_types do with {:ok, object} <- Object.create(object_data) do map = Map.put(map, "object", object.data["id"]) @@ -740,13 +740,12 @@ defp build_flag_object(_), do: [] def get_reports(params, page, page_size) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Flag") - |> Map.put("skip_preload", true) - |> Map.put("preload_report_notes", true) - |> Map.put("total", true) - |> Map.put("limit", page_size) - |> Map.put("offset", (page - 1) * page_size) + |> Map.put(:type, "Flag") + |> Map.put(:skip_preload, true) + |> Map.put(:preload_report_notes, true) + |> Map.put(:total, true) + |> Map.put(:limit, page_size) + |> Map.put(:offset, (page - 1) * page_size) ActivityPub.fetch_activities([], params, :offset) end diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index bf24581cc..edd3abc63 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -228,10 +228,10 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do activities = ActivityPub.fetch_statuses(nil, %{ - "instance" => instance, - "limit" => page_size, - "offset" => (page - 1) * page_size, - "exclude_reblogs" => !with_reblogs && "true" + instance: instance, + limit: page_size, + offset: (page - 1) * page_size, + exclude_reblogs: not with_reblogs }) conn @@ -248,9 +248,9 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do activities = ActivityPub.fetch_user_activities(user, nil, %{ - "limit" => page_size, - "godmode" => godmode, - "exclude_reblogs" => !with_reblogs && "true" + limit: page_size, + godmode: godmode, + exclude_reblogs: not with_reblogs }) conn diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index 574196be8..bc48cc527 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -29,11 +29,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do def index(%{assigns: %{user: _admin}} = conn, params) do activities = ActivityPub.fetch_statuses(nil, %{ - "godmode" => params.godmode, - "local_only" => params.local_only, - "limit" => params.page_size, - "offset" => (params.page - 1) * params.page_size, - "exclude_reblogs" => not params.with_reblogs + godmode: params.godmode, + local_only: params.local_only, + limit: params.page_size, + offset: (params.page - 1) * params.page_size, + exclude_reblogs: not params.with_reblogs }) render(conn, "index.json", activities: activities, as: :activity) diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index f432b8c2c..773f798fe 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -18,7 +18,7 @@ def render("index.json", %{reports: reports}) do %{ reports: reports[:items] - |> Enum.map(&Report.extract_report_info(&1)) + |> Enum.map(&Report.extract_report_info/1) |> Enum.map(&render(__MODULE__, "show.json", &1)) |> Enum.reverse(), total: reports[:total] diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5a1316a5f..9f0ca5b69 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -81,8 +81,7 @@ def add_link_headers(conn, activities, extra_params) do end def assign_account_by_id(conn, _) do - # TODO: use `conn.params[:id]` only after moving to OpenAPI - case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do + case Pleroma.User.get_cached_by_id(conn.params.id) do %Pleroma.User{} = account -> assign(conn, :account, account) nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() end diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 8133f8480..3404d2856 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -15,8 +15,8 @@ def feed(conn, %{"tag" => raw_tag} = params) do {format, tag} = parse_tag(raw_tag) activities = - %{"type" => ["Create"], "tag" => tag} - |> put_if_exist("max_id", params["max_id"]) + %{type: ["Create"], tag: tag} + |> put_if_exist(:max_id, params["max_id"]) |> ActivityPub.fetch_public_activities() conn diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 5a6fc9de0..7bf9bd3e3 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -52,10 +52,10 @@ def feed(conn, %{"nickname" => nickname} = params) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do activities = %{ - "type" => ["Create"], - "actor_id" => user.ap_id + type: ["Create"], + actor_id: user.ap_id } - |> put_if_exist("max_id", params["max_id"]) + |> put_if_exist(:max_id, params["max_id"]) |> ActivityPub.fetch_public_or_unlisted_activities() conn diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 97295a52f..edecbf418 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -254,9 +254,7 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do params = params |> Map.delete(:tagged) - |> Enum.filter(&(not is_nil(&1))) - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("tag", params[:tagged]) + |> Map.put(:tag, params[:tagged]) activities = ActivityPub.fetch_user_activities(user, reading_user, params) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index 69f0e3846..f35ec3596 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do @doc "GET /api/v1/conversations" def index(%{assigns: %{user: user}} = conn, params) do - params = stringify_pagination_params(params) participations = Participation.for_user_with_last_activity_id(user, params) conn @@ -37,20 +36,4 @@ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do render(conn, "participation.json", participation: participation, for: user) end end - - defp stringify_pagination_params(params) do - atom_keys = - Pleroma.Pagination.page_keys() - |> Enum.map(&String.to_atom(&1)) - - str_keys = - params - |> Map.take(atom_keys) - |> Enum.map(fn {key, value} -> {to_string(key), value} end) - |> Enum.into(%{}) - - params - |> Map.delete(atom_keys) - |> Map.merge(str_keys) - 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 f20157a5f..468b44b67 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -359,9 +359,9 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id) do activities = ActivityPub.fetch_activities_for_context(activity.data["context"], %{ - "blocking_user" => user, - "user" => user, - "exclude_id" => activity.id + blocking_user: user, + user: user, + exclude_id: activity.id }) render(conn, "context.json", activity: activity, activities: activities, user: user) @@ -370,11 +370,6 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do @doc "GET /api/v1/favourites" def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do - params = - params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.take(Pleroma.Pagination.page_keys()) - activities = ActivityPub.fetch_favourites(user, params) conn diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index f67f75430..ed74a771a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -44,12 +44,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do def home(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) recipients = [user.ap_id | User.following(user)] @@ -71,10 +70,9 @@ def home(%{assigns: %{user: user}} = conn, params) do def direct(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Create") - |> Map.put("blocking_user", user) - |> Map.put("user", user) + |> Map.put(:type, "Create") + |> Map.put(:blocking_user, user) + |> Map.put(:user, user) |> Map.put(:visibility, "direct") activities = @@ -93,9 +91,7 @@ def direct(%{assigns: %{user: user}} = conn, params) do # GET /api/v1/timelines/public def public(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {key, value} -> {to_string(key), value} end) - - local_only = params["local"] + local_only = params[:local] cfg_key = if local_only do @@ -111,11 +107,11 @@ def public(%{assigns: %{user: user}} = conn, params) do else activities = params - |> Map.put("type", ["Create"]) - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create"]) + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() conn @@ -130,39 +126,38 @@ def public(%{assigns: %{user: user}} = conn, params) do defp hashtag_fetching(params, user, local_only) do tags = - [params["tag"], params["any"]] + [params[:tag], params[:any]] |> List.flatten() |> Enum.uniq() - |> Enum.filter(& &1) - |> Enum.map(&String.downcase(&1)) + |> Enum.reject(&is_nil/1) + |> Enum.map(&String.downcase/1) tag_all = params - |> Map.get("all", []) - |> Enum.map(&String.downcase(&1)) + |> Map.get(:all, []) + |> Enum.map(&String.downcase/1) tag_reject = params - |> Map.get("none", []) - |> Enum.map(&String.downcase(&1)) + |> Map.get(:none, []) + |> Enum.map(&String.downcase/1) _activities = params - |> Map.put("type", "Create") - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("tag", tags) - |> Map.put("tag_all", tag_all) - |> Map.put("tag_reject", tag_reject) + |> Map.put(:type, "Create") + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:tag, tags) + |> Map.put(:tag_all, tag_all) + |> Map.put(:tag_reject, tag_reject) |> ActivityPub.fetch_public_activities() end # GET /api/v1/timelines/tag/:tag def hashtag(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {key, value} -> {to_string(key), value} end) - local_only = params["local"] + local_only = params[:local] activities = hashtag_fetching(params, user, local_only) conn diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 2b6f84c72..fbe618377 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -24,8 +24,8 @@ def render("participation.json", %{participation: participation, for: user}) do last_activity_id = with nil <- participation.last_activity_id do ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - "user" => user, - "blocking_user" => user + user: user, + blocking_user: user }) end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 0a3f45620..f3554d919 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -126,10 +126,9 @@ def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Create") - |> Map.put("favorited_by", user.ap_id) - |> Map.put("blocking_user", for_user) + |> Map.put(:type, "Create") + |> Map.put(:favorited_by, user.ap_id) + |> Map.put(:blocking_user, for_user) recipients = if for_user do diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex index 21d5eb8d5..3d007f324 100644 --- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -42,15 +42,14 @@ def statuses( Participation.get(participation_id, preload: [:conversation]) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities = participation.conversation.ap_id |> ActivityPub.fetch_activities_for_context_query(params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false)) |> Enum.reverse() conn diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 8665ca56c..e9a4fba92 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -36,10 +36,7 @@ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do - params = - params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", ["Listen"]) + params = Map.put(params, :type, ["Listen"]) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index c3efb6651..a7a891b13 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -111,8 +111,14 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do %User{} = user -> meta = Metadata.build_tags(%{user: user}) + params = + params + |> Map.take(@page_keys) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + timeline = - ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys)) + user + |> ActivityPub.fetch_user_activities(nil, params) |> Enum.map(&represent/1) prev_page_id = diff --git a/test/pagination_test.exs b/test/pagination_test.exs index d5b1b782d..9165427ae 100644 --- a/test/pagination_test.exs +++ b/test/pagination_test.exs @@ -21,7 +21,7 @@ test "paginates by min_id", %{notes: notes} do id = Enum.at(notes, 2).id |> Integer.to_string() %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{"min_id" => id, "total" => true}) + Pagination.fetch_paginated(Object, %{min_id: id, total: true}) assert length(paginated) == 2 assert total == 5 @@ -31,7 +31,7 @@ test "paginates by since_id", %{notes: notes} do id = Enum.at(notes, 2).id |> Integer.to_string() %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{"since_id" => id, "total" => true}) + Pagination.fetch_paginated(Object, %{since_id: id, total: true}) assert length(paginated) == 2 assert total == 5 @@ -41,7 +41,7 @@ test "paginates by max_id", %{notes: notes} do id = Enum.at(notes, 1).id |> Integer.to_string() %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{"max_id" => id, "total" => true}) + Pagination.fetch_paginated(Object, %{max_id: id, total: true}) assert length(paginated) == 1 assert total == 5 @@ -50,7 +50,7 @@ test "paginates by max_id", %{notes: notes} do test "paginates by min_id & limit", %{notes: notes} do id = Enum.at(notes, 2).id |> Integer.to_string() - paginated = Pagination.fetch_paginated(Object, %{"min_id" => id, "limit" => 1}) + paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1}) assert length(paginated) == 1 end @@ -64,13 +64,13 @@ test "paginates by min_id & limit", %{notes: notes} do end test "paginates by limit" do - paginated = Pagination.fetch_paginated(Object, %{"limit" => 2}, :offset) + paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset) assert length(paginated) == 2 end test "paginates by limit & offset" do - paginated = Pagination.fetch_paginated(Object, %{"limit" => 2, "offset" => 4}, :offset) + paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset) assert length(paginated) == 1 end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 678288854..a8ba0658d 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -62,11 +62,11 @@ test "relay is unfollowed" do [undo_activity] = ActivityPub.fetch_activities([], %{ - "type" => "Undo", - "actor_id" => follower_id, - "limit" => 1, - "skip_preload" => true, - "invisible_actors" => true + type: "Undo", + actor_id: follower_id, + limit: 1, + skip_preload: true, + invisible_actors: true }) assert undo_activity.data["type"] == "Undo" diff --git a/test/user_test.exs b/test/user_test.exs index 6b344158d..48c7605f5 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1122,7 +1122,7 @@ test "hide a user's statuses from timelines and notifications" do assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - "user" => user2 + user: user2 }) {:ok, _user} = User.deactivate(user) @@ -1132,7 +1132,7 @@ test "hide a user's statuses from timelines and notifications" do assert [] == ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - "user" => user2 + user: user2 }) end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 3dcb62873..2f65dfc8e 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -82,30 +82,28 @@ test "it restricts by the appropriate visibility" do {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - activities = - ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id}) + activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id}) assert activities == [direct_activity] activities = - ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id}) + ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id}) assert activities == [unlisted_activity] activities = - ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id}) + ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id}) assert activities == [private_activity] - activities = - ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id}) + activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id}) assert activities == [public_activity] activities = ActivityPub.fetch_activities([], %{ - :visibility => ~w[private public], - "actor_id" => user.ap_id + visibility: ~w[private public], + actor_id: user.ap_id }) assert activities == [public_activity, private_activity] @@ -126,8 +124,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "direct", - "actor_id" => user.ap_id + exclude_visibilities: "direct", + actor_id: user.ap_id }) assert public_activity in activities @@ -137,8 +135,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "unlisted", - "actor_id" => user.ap_id + exclude_visibilities: "unlisted", + actor_id: user.ap_id }) assert public_activity in activities @@ -148,8 +146,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "private", - "actor_id" => user.ap_id + exclude_visibilities: "private", + actor_id: user.ap_id }) assert public_activity in activities @@ -159,8 +157,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "public", - "actor_id" => user.ap_id + exclude_visibilities: "public", + actor_id: user.ap_id }) refute public_activity in activities @@ -193,23 +191,22 @@ test "it fetches the appropriate tag-restricted posts" do {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) - fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"}) + fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) - fetch_two = - ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]}) + fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) fetch_three = ActivityPub.fetch_activities([], %{ - "type" => "Create", - "tag" => ["test", "essais"], - "tag_reject" => ["reject"] + type: "Create", + tag: ["test", "essais"], + tag_reject: ["reject"] }) fetch_four = ActivityPub.fetch_activities([], %{ - "type" => "Create", - "tag" => ["test"], - "tag_all" => ["test", "reject"] + type: "Create", + tag: ["test"], + tag_all: ["test", "reject"] }) assert fetch_one == [status_one, status_three] @@ -375,7 +372,7 @@ test "can be fetched into a timeline" do _listen_activity_2 = insert(:listen) _listen_activity_3 = insert(:listen) - timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]}) + timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]}) assert length(timeline) == 3 end @@ -507,7 +504,7 @@ test "retrieves activities that have a given context" do {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) - activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user}) + activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) assert activities == [activity_two, activity] end end @@ -520,8 +517,7 @@ test "doesn't return blocked activities" do booster = insert(:user) {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -529,8 +525,7 @@ test "doesn't return blocked activities" do {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -541,16 +536,14 @@ test "doesn't return blocked activities" do %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) refute Enum.member?(activities, activity_three) refute Enum.member?(activities, boost_activity) assert Enum.member?(activities, activity_one) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -573,7 +566,7 @@ test "doesn't return transitive interactions concerning blocked users" do {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker}) + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker}) assert Enum.member?(activities, activity_one) refute Enum.member?(activities, activity_two) @@ -595,7 +588,7 @@ test "doesn't return announce activities concerning blocked users" do {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend) activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker}) + ActivityPub.fetch_activities([], %{blocking_user: blocker}) |> Enum.map(fn act -> act.id end) assert Enum.member?(activities, activity_one.id) @@ -611,8 +604,7 @@ test "doesn't return activities from blocked domains" do user = insert(:user) {:ok, user} = User.block_domain(user, domain) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) refute activity in activities @@ -620,8 +612,7 @@ test "doesn't return activities from blocked domains" do ActivityPub.follow(user, followed_user) {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) refute repeat_activity in activities end @@ -641,8 +632,7 @@ test "does return activities from followed users on blocked domains" do note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) activity = insert(:note_activity, %{note: note}) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) assert activity in activities @@ -653,8 +643,7 @@ test "does return activities from followed users on blocked domains" do bad_activity = insert(:note_activity, %{note: bad_note}) {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) refute repeat_activity in activities end @@ -669,8 +658,7 @@ test "doesn't return muted activities" do activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) {:ok, _user_relationships} = User.mute(user, activity_one_actor) - activities = - ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -679,9 +667,9 @@ test "doesn't return muted activities" do # Calling with 'with_muted' will deliver muted activities, too. activities = ActivityPub.fetch_activities([], %{ - "muting_user" => user, - "with_muted" => true, - "skip_preload" => true + muting_user: user, + with_muted: true, + skip_preload: true }) assert Enum.member?(activities, activity_two) @@ -690,8 +678,7 @@ test "doesn't return muted activities" do {:ok, _user_mute} = User.unmute(user, activity_one_actor) - activities = - ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -703,15 +690,14 @@ test "doesn't return muted activities" do %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) - activities = - ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) refute Enum.member?(activities, activity_three) refute Enum.member?(activities, boost_activity) assert Enum.member?(activities, activity_one) - activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -727,7 +713,7 @@ test "doesn't return thread muted activities" do {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) - assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user}) + assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user}) end test "returns thread muted activities when with_muted is set" do @@ -739,7 +725,7 @@ test "returns thread muted activities when with_muted is set" do {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) assert [_activity_two, _activity_one] = - ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true}) + ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true}) end test "does include announces on request" do @@ -761,7 +747,7 @@ test "excludes reblogs on request" do {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) - [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"}) + [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true}) assert activity == expected_activity end @@ -804,7 +790,7 @@ test "retrieves ids starting from a since_id" do expected_activities = ActivityBuilder.insert_list(10) since_id = List.last(activities).id - activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id}) + activities = ActivityPub.fetch_public_activities(%{since_id: since_id}) assert collect_ids(activities) == collect_ids(expected_activities) assert length(activities) == 10 @@ -819,7 +805,7 @@ test "retrieves ids up to max_id" do |> ActivityBuilder.insert_list() |> List.first() - activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id}) + activities = ActivityPub.fetch_public_activities(%{max_id: max_id}) assert length(activities) == 20 assert collect_ids(activities) == collect_ids(expected_activities) @@ -831,8 +817,7 @@ test "paginates via offset/limit" do later_activities = ActivityBuilder.insert_list(10) - activities = - ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset) + activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset) assert length(activities) == 20 @@ -848,7 +833,7 @@ test "doesn't return reblogs for users for whom reblogs have been muted" do {:ok, activity} = CommonAPI.repeat(activity.id, booster) - activities = ActivityPub.fetch_activities([], %{"muting_user" => user}) + activities = ActivityPub.fetch_activities([], %{muting_user: user}) refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) end @@ -862,7 +847,7 @@ test "returns reblogs for users for whom reblogs have not been muted" do {:ok, activity} = CommonAPI.repeat(activity.id, booster) - activities = ActivityPub.fetch_activities([], %{"muting_user" => user}) + activities = ActivityPub.fetch_activities([], %{muting_user: user}) assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) end @@ -1066,7 +1051,7 @@ test "it filters broken threads" do assert length(activities) == 3 activities = - ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1}) + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1}) |> Enum.map(fn a -> a.id end) assert [public_activity.id, private_activity_1.id] == activities @@ -1115,7 +1100,7 @@ test "returned pinned statuses" do CommonAPI.pin(activity_three.id, user) user = refresh_record(user) - activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"}) + activities = ActivityPub.fetch_user_activities(user, nil, %{pinned: true}) assert 3 = length(activities) end @@ -1226,7 +1211,7 @@ test "fetch_activities/2 returns activities addressed to a list " do activity = Repo.preload(activity, :bookmark) activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} - assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity] + assert ActivityPub.fetch_activities([], %{user: user}) == [activity] end def data_uri do @@ -1400,7 +1385,7 @@ test "returns a favourite activities sorted by adds to favorite" do assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] - result = ActivityPub.fetch_favourites(user, %{"limit" => 2}) + result = ActivityPub.fetch_favourites(user, %{limit: 2}) assert Enum.map(result, & &1.id) == [a1.id, a5.id] end end @@ -1470,7 +1455,7 @@ test "doesn't retrieve replies activities with exclude_replies" do {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id}) - [result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"}) + [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true}) assert result.id == activity.id @@ -1483,11 +1468,11 @@ test "doesn't retrieve replies activities with exclude_replies" do test "public timeline", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1504,12 +1489,12 @@ test "public timeline with reply_visibility `following`", %{ } do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1531,12 +1516,12 @@ test "public timeline with reply_visibility `self`", %{ } do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1555,11 +1540,11 @@ test "home timeline", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1593,12 +1578,12 @@ test "home timeline with reply_visibility `following`", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1632,12 +1617,12 @@ test "home timeline with reply_visibility `self`", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1666,11 +1651,11 @@ test "home timeline with reply_visibility `self`", %{ test "public timeline", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1680,13 +1665,13 @@ test "public timeline", %{users: %{u1: user}} do test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1696,13 +1681,13 @@ test "public timeline with default reply_visibility `following`", %{users: %{u1: test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1712,10 +1697,10 @@ test "public timeline with default reply_visibility `self`", %{users: %{u1: user test "home timeline", %{users: %{u1: user}} do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1727,12 +1712,12 @@ test "home timeline", %{users: %{u1: user}} do test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1751,12 +1736,12 @@ test "home timeline with default reply_visibility `self`", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) From d44da91bbf50ae91e8246ebe3669cfaf1fabda1b Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 20:28:33 +0200 Subject: [PATCH 216/375] SubscriptionOperation: Let chat mentions through. --- .../web/api_spec/operations/subscription_operation.ex | 5 +++++ .../controllers/subscription_controller_test.exs | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex index c575a87e6..775dd795d 100644 --- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex +++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex @@ -141,6 +141,11 @@ defp create_request do allOf: [BooleanLike], nullable: true, description: "Receive poll notifications?" + }, + "pleroma:chat_mention": %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive chat notifications?" } } } diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs index 4aa260663..d36bb1ae8 100644 --- a/test/web/mastodon_api/controllers/subscription_controller_test.exs +++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs @@ -58,7 +58,9 @@ test "successful creation", %{conn: conn} do result = conn |> post("/api/v1/push/subscription", %{ - "data" => %{"alerts" => %{"mention" => true, "test" => true}}, + "data" => %{ + "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true} + }, "subscription" => @sub }) |> json_response_and_validate_schema(200) @@ -66,7 +68,7 @@ test "successful creation", %{conn: conn} do [subscription] = Pleroma.Repo.all(Subscription) assert %{ - "alerts" => %{"mention" => true}, + "alerts" => %{"mention" => true, "pleroma:chat_mention" => true}, "endpoint" => subscription.endpoint, "id" => to_string(subscription.id), "server_key" => @server_key From aa2ac76510d95f2412e23f3739e8e1ae4402643f Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 4 Jun 2020 20:40:46 +0200 Subject: [PATCH 217/375] Notification: Don't break on figuring out the type of old EmojiReactions --- lib/pleroma/notification.ex | 4 ++++ test/notification_test.exs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 455d214bf..e5b880b10 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -398,6 +398,10 @@ defp type_from_activity(%{data: %{"type" => type}} = activity, opts \\ []) do "EmojiReact" -> "pleroma:emoji_reaction" + # Compatibility with old reactions + "EmojiReaction" -> + "pleroma:emoji_reaction" + "Create" -> activity |> type_from_activity_object() diff --git a/test/notification_test.exs b/test/notification_test.exs index 6bc2b6904..f2115a29e 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -8,8 +8,10 @@ defmodule Pleroma.NotificationTest do import Pleroma.Factory import Mock + alias Pleroma.Activity alias Pleroma.FollowingRelationship alias Pleroma.Notification + alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -29,8 +31,18 @@ test "it fills in missing notification types" do {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") {:ok, like} = CommonAPI.favorite(other_user, post.id) + {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - assert {4, nil} = Repo.update_all(Notification, set: [type: nil]) + data = + react_2.data + |> Map.put("type", "EmojiReaction") + + {:ok, react_2} = + react_2 + |> Activity.change(%{data: data}) + |> Repo.update() + + assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) Notification.fill_in_notification_types() @@ -43,6 +55,9 @@ test "it fills in missing notification types" do assert %{type: "pleroma:emoji_reaction"} = Repo.get_by(Notification, user_id: user.id, activity_id: react.id) + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) + assert %{type: "pleroma:chat_mention"} = Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) end From cc8a7dc205a4516452c48659e6bf081f3f730496 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 5 Jun 2020 12:01:33 +0200 Subject: [PATCH 218/375] SideEffects / ChatView: Add an unread cache. This is to prevent wrong values in the stream. --- lib/pleroma/web/activity_pub/side_effects.ex | 5 ++++ .../web/pleroma_api/views/chat_view.ex | 2 +- lib/pleroma/web/streamer/streamer.ex | 28 +++++-------------- lib/pleroma/web/views/streamer_view.ex | 2 ++ test/web/pleroma_api/views/chat_view_test.exs | 15 ++++++++++ 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index e9f109d80..992c04ac1 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -142,6 +142,11 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id != actor.ap_id) + # We add a cache of the unread value here so that it doesn't change when being streamed out + chat = + chat + |> Map.put(:unread, ChatMessageReference.unread_count_for_chat(chat)) + Streamer.stream( ["user", "user:pleroma_chat"], {user, %{cm_ref | chat: chat, object: object}} diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index c903a71fd..91d50dd1e 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -20,7 +20,7 @@ def render("show.json", %{chat: %Chat{} = chat} = opts) do %{ id: chat.id |> to_string(), account: AccountView.render("show.json", Map.put(opts, :user, recipient)), - unread: ChatMessageReference.unread_count_for_chat(chat), + unread: Map.get(chat, :unread) || ChatMessageReference.unread_count_for_chat(chat), last_message: last_message && ChatMessageReferenceView.render("show.json", chat_message_reference: last_message), diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 5e37e2cf2..b22297955 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -90,34 +90,20 @@ def remove_socket(topic) do if should_env_send?(), do: Registry.unregister(@registry, topic) end - def stream(topics, item) when is_list(topics) do + def stream(topics, items) do if should_env_send?() do - Enum.each(topics, fn t -> - spawn(fn -> do_stream(t, item) end) + List.wrap(topics) + |> Enum.each(fn topic -> + List.wrap(items) + |> Enum.each(fn item -> + spawn(fn -> do_stream(topic, item) end) + end) end) end :ok end - def stream(topic, items) when is_list(items) do - if should_env_send?() do - Enum.each(items, fn i -> - spawn(fn -> do_stream(topic, i) end) - end) - - :ok - end - end - - def stream(topic, item) do - if should_env_send?() do - spawn(fn -> do_stream(topic, item) end) - end - - :ok - end - def filtered_by_user?(%User{} = user, %Activity{} = item) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index a6efd0109..b000e7ce0 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -55,6 +55,8 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do # Explicitly giving the cmr for the object here, so we don't accidentally # send a later 'last_message' that was inserted between inserting this and # streaming it out + # + # It also contains the chat with a cache of the correct unread count Logger.debug("Trying to stream out #{inspect(cm_ref)}") representation = diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index f3bd12616..f77584dd1 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -16,6 +16,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do import Pleroma.Factory + test "giving a chat with an 'unread' field, it uses that" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + chat = + chat + |> Map.put(:unread, 5) + + represented_chat = ChatView.render("show.json", chat: chat) + + assert represented_chat[:unread] == 5 + end + test "it represents a chat" do user = insert(:user) recipient = insert(:user) From 0efa8aa0b9567f42b1af63e2b93a9c51e9a0fb11 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 5 Jun 2020 12:26:07 +0200 Subject: [PATCH 219/375] Transmogrifier: For follows, create notifications last. As the notification type changes depending on the follow state, the notification should not be created and streamed out before the state settles. For this reason, the notification creation has been delayed until it's clear if the user has been followed or not. This is a bit hacky but it will be properly rewritten using the pipeline soon. --- lib/pleroma/web/activity_pub/activity_pub.ex | 12 +++++++----- lib/pleroma/web/activity_pub/transmogrifier.ex | 5 +++-- .../transmogrifier/follow_handling_test.exs | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 568db2348..4f7043c92 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -363,19 +363,21 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do end end - @spec follow(User.t(), User.t(), String.t() | nil, boolean()) :: + @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: {:ok, Activity.t()} | {:error, any()} - def follow(follower, followed, activity_id \\ nil, local \\ true) do + def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do with {:ok, result} <- - Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do + Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do result end end - defp do_follow(follower, followed, activity_id, local) do + defp do_follow(follower, followed, activity_id, local, opts) do + skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false) + with data <- make_follow_data(follower, followed, activity_id), {:ok, activity} <- insert(data, local), - _ <- notify_and_stream(activity), + _ <- skip_notify_and_stream || notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index b2461de2b..50f3216f3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -533,12 +533,12 @@ def handle_incoming( User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), - {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do + {:ok, activity} <- + ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, {_, false} <- {:user_locked, User.locked?(followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, - _ <- Notification.update_notification_type(followed, activity), {_, {:ok, _}} <- {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, {:ok, _relationship} <- @@ -577,6 +577,7 @@ def handle_incoming( :noop end + ActivityPub.notify_and_stream(activity) {:ok, activity} else _e -> diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 6b003b51c..06c39eed6 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do import Pleroma.Factory import Ecto.Query + import Mock setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -151,6 +152,23 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl assert activity.data["state"] == "reject" end + test "it rejects incoming follow requests if the following errors for some reason" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do + {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) + + %Activity{} = activity = Activity.get_by_ap_id(id) + + assert activity.data["state"] == "reject" + end + end + test "it works for incoming follow requests from hubzilla" do user = insert(:user) From f3ea6ee2c82b2d63991d3e658566298c722ac0af Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 5 Jun 2020 12:45:25 +0200 Subject: [PATCH 220/375] Credo fixes. --- lib/pleroma/web/activity_pub/side_effects.ex | 3 ++- lib/pleroma/web/views/streamer_view.ex | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 992c04ac1..e7d050e81 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -142,7 +142,8 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id != actor.ap_id) - # We add a cache of the unread value here so that it doesn't change when being streamed out + # We add a cache of the unread value here so that it + # doesn't change when being streamed out chat = chat |> Map.put(:unread, ChatMessageReference.unread_count_for_chat(chat)) diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index b000e7ce0..476a33245 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -55,7 +55,7 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do # Explicitly giving the cmr for the object here, so we don't accidentally # send a later 'last_message' that was inserted between inserting this and # streaming it out - # + # # It also contains the chat with a cache of the correct unread count Logger.debug("Trying to stream out #{inspect(cm_ref)}") From 65689ba9bd44e291fc626cce2bd5136b93a5da90 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 5 Jun 2020 13:10:48 +0200 Subject: [PATCH 221/375] If Credo fixes is so good, why is there no Credo fixes 2? --- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index e7d050e81..b3aacff40 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -142,7 +142,7 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id != actor.ap_id) - # We add a cache of the unread value here so that it + # We add a cache of the unread value here so that it # doesn't change when being streamed out chat = chat From 115d08a7542b92c5e1d889da41c0ee6837a1235e Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 5 Jun 2020 16:47:02 +0200 Subject: [PATCH 222/375] Pipeline: Add a side effects step after the transaction finishes This is to run things like streaming notifications out, which will sometimes need data that is created by the transaction, but is streamed out asynchronously. --- lib/pleroma/notification.ex | 26 ++++-- lib/pleroma/web/activity_pub/pipeline.ex | 4 + lib/pleroma/web/activity_pub/side_effects.ex | 30 ++++++- test/web/activity_pub/pipeline_test.exs | 9 +- test/web/activity_pub/side_effects_test.exs | 86 +++++++++++++++++--- test/web/common_api/common_api_test.exs | 43 ++++++++-- 6 files changed, 169 insertions(+), 29 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index e5b880b10..49e27c05a 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -334,30 +334,34 @@ def dismiss(%{id: user_id} = _user, id) do end end - def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do + def create_notifications(activity, options \\ []) + + def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do object = Object.normalize(activity, false) if object && object.data["type"] == "Answer" do {:ok, []} else - do_create_notifications(activity) + do_create_notifications(activity, options) end end - def create_notifications(%Activity{data: %{"type" => type}} = activity) + def create_notifications(%Activity{data: %{"type" => type}} = activity, options) when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do - do_create_notifications(activity) + do_create_notifications(activity, options) end - def create_notifications(_), do: {:ok, []} + def create_notifications(_, _), do: {:ok, []} + + defp do_create_notifications(%Activity{} = activity, options) do + do_send = Keyword.get(options, :do_send, true) - defp do_create_notifications(%Activity{} = activity) do {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity) potential_receivers = enabled_receivers ++ disabled_receivers notifications = Enum.map(potential_receivers, fn user -> - do_send = user in enabled_receivers + do_send = do_send && user in enabled_receivers create_notification(activity, user, do_send) end) @@ -623,4 +627,12 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, end def skip?(_, _, _), do: false + + def for_user_and_activity(user, activity) do + from(n in __MODULE__, + where: n.user_id == ^user.id, + where: n.activity_id == ^activity.id + ) + |> Repo.one() + end end diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 0c54c4b23..6875c47f6 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} def common_pipeline(object, meta) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do + {:ok, {:ok, activity, meta}} -> + SideEffects.handle_after_transaction(meta) + {:ok, activity, meta} + {:ok, value} -> value diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b3aacff40..10136789a 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Streamer + alias Pleroma.Web.Push def handle(object, meta \\ []) @@ -37,7 +38,12 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do # - Set up notifications def handle(%{data: %{"type" => "Create"}} = activity, meta) do with {:ok, _object, _meta} <- handle_object_creation(meta[:object_data], meta) do - Notification.create_notifications(activity) + {:ok, notifications} = Notification.create_notifications(activity, do_send: false) + + meta = + meta + |> add_notifications(notifications) + {:ok, activity, meta} else e -> Repo.rollback(e) @@ -200,4 +206,26 @@ def handle_undoing( end def handle_undoing(object), do: {:error, ["don't know how to handle", object]} + + defp send_notifications(meta) do + Keyword.get(meta, :created_notifications, []) + |> Enum.each(fn notification -> + Streamer.stream(["user", "user:notification"], notification) + Push.send(notification) + end) + + meta + end + + defp add_notifications(meta, notifications) do + existing = Keyword.get(meta, :created_notifications, []) + + meta + |> Keyword.put(:created_notifications, notifications ++ existing) + end + + def handle_after_transaction(meta) do + meta + |> send_notifications() + end end diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs index 26557720b..8deb64501 100644 --- a/test/web/activity_pub/pipeline_test.exs +++ b/test/web/activity_pub/pipeline_test.exs @@ -33,7 +33,10 @@ test "it goes through validation, filtering, persisting, side effects and federa { Pleroma.Web.ActivityPub.SideEffects, [], - [handle: fn o, m -> {:ok, o, m} end] + [ + handle: fn o, m -> {:ok, o, m} end, + handle_after_transaction: fn m -> m end + ] }, { Pleroma.Web.Federator, @@ -71,7 +74,7 @@ test "it goes through validation, filtering, persisting, side effects without fe { Pleroma.Web.ActivityPub.SideEffects, [], - [handle: fn o, m -> {:ok, o, m} end] + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] }, { Pleroma.Web.Federator, @@ -110,7 +113,7 @@ test "it goes through validation, filtering, persisting, side effects without fe { Pleroma.Web.ActivityPub.SideEffects, [], - [handle: fn o, m -> {:ok, o, m} end] + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] }, { Pleroma.Web.Federator, diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 40df664eb..43ffe1337 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -22,6 +22,47 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do import Pleroma.Factory import Mock + describe "handle_after_transaction" do + test "it streams out notifications" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [notification] = meta[:created_notifications] + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + SideEffects.handle_after_transaction(meta) + + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Push.send(notification)) + end + end + end + describe "delete objects" do setup do user = insert(:user) @@ -361,22 +402,47 @@ test "it creates a Chat and ChatMessageReferences for the local users and bumps {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - chat = Chat.get(author.id, recipient.ap_id) + # The notification gets created + assert [notification] = meta[:created_notifications] + assert notification.activity_id == create_activity.id - [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + # But it is not sent out + refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + refute called(Pleroma.Web.Push.send(notification)) - assert cm_ref.object.data["content"] == "hey" - assert cm_ref.unread == false + chat = Chat.get(author.id, recipient.ap_id) - chat = Chat.get(recipient.id, author.ap_id) + [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() - [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == false - assert cm_ref.object.data["content"] == "hey" - assert cm_ref.unread == true + chat = Chat.get(recipient.id, author.ap_id) + + [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == true + end end test "it creates a Chat for the local users and bumps the unread count" do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 611a9ae66..63b59820e 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.Activity alias Pleroma.Chat alias Pleroma.Conversation.Participation + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -39,15 +40,41 @@ test "it posts a chat message without content but with an attachment" do {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - nil, - media_id: upload.id - ) + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> + nil + end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + nil, + media_id: upload.id + ) - assert activity + notification = + Notification.for_user_and_activity(recipient, activity) + |> Repo.preload(:activity) + + assert called(Pleroma.Web.Push.send(notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + + assert activity + end end test "it adds html newlines" do From 54bae06b4fa960eadb9918414f50b9ececc1faa4 Mon Sep 17 00:00:00 2001 From: Haelwenn Date: Fri, 5 Jun 2020 14:48:02 +0000 Subject: [PATCH 223/375] Create Pleroma.Maps.put_if_present(map, key, value, value_fun // &{:ok, &1}) Unifies all the similar functions to one and simplify some blocks with it. --- lib/pleroma/helpers/uri_helper.ex | 8 ----- lib/pleroma/maps.ex | 15 ++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 20 +++-------- .../web/activity_pub/transmogrifier.ex | 17 ++++----- lib/pleroma/web/activity_pub/utils.ex | 18 +++++----- .../controllers/config_controller.ex | 5 ++- .../controllers/oauth_app_controller.ex | 14 ++------ lib/pleroma/web/controller_helper.ex | 5 --- lib/pleroma/web/feed/tag_controller.ex | 4 +-- lib/pleroma/web/feed/user_controller.ex | 4 +-- .../controllers/account_controller.ex | 36 +++++++------------ .../web/mastodon_api/views/app_view.ex | 6 +--- .../views/scheduled_activity_view.ex | 8 ++--- lib/pleroma/web/oauth/oauth_controller.ex | 5 +-- 14 files changed, 59 insertions(+), 106 deletions(-) create mode 100644 lib/pleroma/maps.ex diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex index 69d8c8fe0..6d205a636 100644 --- a/lib/pleroma/helpers/uri_helper.ex +++ b/lib/pleroma/helpers/uri_helper.ex @@ -17,14 +17,6 @@ def append_uri_params(uri, appended_params) do |> URI.to_string() end - def append_param_if_present(%{} = params, param_name, param_value) do - if param_value do - Map.put(params, param_name, param_value) - else - params - end - end - def maybe_add_base("/" <> uri, base), do: Path.join([base, uri]) def maybe_add_base(uri, _base), do: uri end diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex new file mode 100644 index 000000000..ab2e32e2f --- /dev/null +++ b/lib/pleroma/maps.ex @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Maps do + def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do + with false <- is_nil(key), + false <- is_nil(value), + {:ok, new_value} <- value_function.(value) do + Map.put(map, key, new_value) + else + _ -> map + end + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 958f3e5af..75468f415 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Constants alias Pleroma.Conversation alias Pleroma.Conversation.Participation + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment @@ -19,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Streamer alias Pleroma.Web.WebFinger alias Pleroma.Workers.BackgroundWorker @@ -161,12 +161,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when }) # Splice in the child object if we have one. - activity = - if not is_nil(object) do - Map.put(activity, :object, object) - else - activity - end + activity = Maps.put_if_present(activity, :object, object) BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) @@ -328,7 +323,7 @@ def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do with data <- %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} - |> Utils.maybe_put("id", activity_id), + |> Maps.put_if_present("id", activity_id), {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do @@ -348,7 +343,7 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do "actor" => actor, "object" => object }, - data <- Utils.maybe_put(data, "id", activity_id), + data <- Maps.put_if_present(data, "id", activity_id), {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do @@ -1225,12 +1220,7 @@ def fetch_activities_bounded( @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} def upload(file, opts \\ []) do with {:ok, data} <- Upload.store(file, opts) do - obj_data = - if opts[:actor] do - Map.put(data, "actor", opts[:actor]) - else - data - end + obj_data = Maps.put_if_present(data, "actor", opts[:actor]) Repo.insert(%Object{data: obj_data}) end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 8443c284c..fda1c71df 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Activity alias Pleroma.EarmarkRenderer alias Pleroma.FollowingRelationship + alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -208,12 +209,6 @@ def fix_context(object) do |> Map.put("conversation", context) end - defp add_if_present(map, _key, nil), do: map - - defp add_if_present(map, key, value) do - Map.put(map, key, value) - end - def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do attachments = Enum.map(attachment, fn data -> @@ -241,13 +236,13 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm attachment_url = %{"href" => href} - |> add_if_present("mediaType", media_type) - |> add_if_present("type", Map.get(url || %{}, "type")) + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", Map.get(url || %{}, "type")) %{"url" => [attachment_url]} - |> add_if_present("mediaType", media_type) - |> add_if_present("type", data["type"]) - |> add_if_present("name", data["name"]) + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", data["type"]) + |> Maps.put_if_present("name", data["name"]) end) Map.put(object, "attachment", attachments) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index a76a699ee..5fce0ba63 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Ecto.UUID alias Pleroma.Activity alias Pleroma.Config + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -307,7 +308,7 @@ def make_like_data( "cc" => cc, "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_emoji_reaction_data(user, object, emoji, activity_id) do @@ -477,7 +478,7 @@ def make_follow_data( "object" => followed_id, "state" => "pending" } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do @@ -546,7 +547,7 @@ def make_announce_data( "cc" => [], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_announce_data( @@ -563,7 +564,7 @@ def make_announce_data( "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_undo_data( @@ -582,7 +583,7 @@ def make_undo_data( "cc" => [Pleroma.Constants.as_public()], "context" => context } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end @spec add_announce_to_object(Activity.t(), Object.t()) :: @@ -627,7 +628,7 @@ def make_unfollow_data(follower, followed, follow_activity, activity_id) do "to" => [followed.ap_id], "object" => follow_activity.data } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Block-related helpers @@ -650,7 +651,7 @@ def make_block_data(blocker, blocked, activity_id) do "to" => [blocked.ap_id], "object" => blocked.ap_id } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Create-related helpers @@ -871,7 +872,4 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> Repo.all() end - - def maybe_put(map, _key, nil), do: map - def maybe_put(map, key, value), do: Map.put(map, key, value) end diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index e221d9418..d6e2019bc 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -61,13 +61,12 @@ def show(conn, _params) do value end - setting = %{ + %{ group: ConfigDB.convert(group), key: ConfigDB.convert(key), value: ConfigDB.convert(merged_value) } - - if db, do: Map.put(setting, :db, db), else: setting + |> Pleroma.Maps.put_if_present(:db, db) end) end) |> List.flatten() diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex index 04e629fc1..dca23ea73 100644 --- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex @@ -42,12 +42,7 @@ def index(conn, params) do end def create(%{body_params: params} = conn, _) do - params = - if params[:name] do - Map.put(params, :client_name, params[:name]) - else - params - end + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) case App.create(params) do {:ok, app} -> @@ -59,12 +54,7 @@ def create(%{body_params: params} = conn, _) do end def update(%{body_params: params} = conn, %{id: id}) do - params = - if params[:name] do - Map.put(params, :client_name, params.name) - else - params - end + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) with {:ok, app} <- App.update(id, params) do render(conn, "show.json", app: app, admin: true) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5a1316a5f..bf832fe94 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -99,11 +99,6 @@ def try_render(conn, _, _) do render_error(conn, :not_implemented, "Can't display this activity") end - @spec put_if_exist(map(), atom() | String.t(), any) :: map() - def put_if_exist(map, _key, nil), do: map - - def put_if_exist(map, key, value), do: Map.put(map, key, value) - @doc """ Returns true if request specifies to include embedded relationships in account objects. May only be used in selected account-related endpoints; has no effect for status- or diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 8133f8480..4e86cfeb5 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Feed.FeedView - import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] - def feed(conn, %{"tag" => raw_tag} = params) do {format, tag} = parse_tag(raw_tag) activities = %{"type" => ["Create"], "tag" => tag} - |> put_if_exist("max_id", params["max_id"]) + |> Pleroma.Maps.put_if_present("max_id", params["max_id"]) |> ActivityPub.fetch_public_activities() conn diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 5a6fc9de0..7c2e0d522 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.Feed.FeedView - import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] - plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) action_fallback(:errors) @@ -55,7 +53,7 @@ def feed(conn, %{"nickname" => nickname} = params) do "type" => ["Create"], "actor_id" => user.ap_id } - |> put_if_exist("max_id", params["max_id"]) + |> Pleroma.Maps.put_if_present("max_id", params["max_id"]) |> ActivityPub.fetch_public_or_unlisted_activities() conn diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 97295a52f..5734bb854 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do json_response: 3 ] + alias Pleroma.Maps alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter @@ -160,23 +161,22 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p :discoverable ] |> Enum.reduce(%{}, fn key, acc -> - add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)}) + Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) end) - |> add_if_present(params, :display_name, :name) - |> 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, + |> Maps.put_if_present(:name, params[:display_name]) + |> Maps.put_if_present(:bio, params[:note]) + |> Maps.put_if_present(:avatar, params[:avatar]) + |> Maps.put_if_present(:banner, params[:header]) + |> Maps.put_if_present(:background, params[:pleroma_background_image]) + |> Maps.put_if_present( :raw_fields, + params[:fields_attributes], &{: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["source"], "privacy", :default_scope) - |> add_if_present(params, :actor_type, :actor_type) + |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store]) + |> Maps.put_if_present(:default_scope, params[:default_scope]) + |> Maps.put_if_present(:default_scope, params["source"]["privacy"]) + |> Maps.put_if_present(:actor_type, params[:actor_type]) changeset = User.update_changeset(user, user_params) @@ -206,16 +206,6 @@ defp build_update_activity_params(user) do } end - defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do - with true <- is_map(params), - true <- Map.has_key?(params, params_field), - {:ok, new_value} <- value_function.(Map.get(params, params_field)) do - Map.put(map, map_field, new_value) - else - _ -> map - end - end - defp normalize_fields_attributes(fields) do if Enum.all?(fields, &is_tuple/1) do Enum.map(fields, fn {_, v} -> v end) diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex index 36071cd25..e44272c6f 100644 --- a/lib/pleroma/web/mastodon_api/views/app_view.ex +++ b/lib/pleroma/web/mastodon_api/views/app_view.ex @@ -45,10 +45,6 @@ def render("short.json", %{app: %App{website: webiste, client_name: name}}) do defp with_vapid_key(data) do vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] - if vapid_key do - Map.put(data, "vapid_key", vapid_key) - else - data - end + Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key) end end diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex index 458f6bc78..5b896bf3b 100644 --- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex +++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex @@ -30,7 +30,7 @@ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attac defp with_media_attachments(data, _), do: data defp status_params(params) do - data = %{ + %{ text: params["status"], sensitive: params["sensitive"], spoiler_text: params["spoiler_text"], @@ -39,10 +39,6 @@ defp status_params(params) do poll: params["poll"], in_reply_to_id: params["in_reply_to_id"] } - - case params["media_ids"] do - nil -> data - media_ids -> Map.put(data, :media_ids, media_ids) - end + |> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"]) end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 7c804233c..c557778ca 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps alias Pleroma.MFA alias Pleroma.Plugs.RateLimiter alias Pleroma.Registration @@ -108,7 +109,7 @@ defp handle_existing_authorization( if redirect_uri in String.split(app.redirect_uris) do redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{access_token: token.token} - url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) + url_params = Maps.put_if_present(url_params, :state, params["state"]) url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) else @@ -147,7 +148,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ if redirect_uri in String.split(app.redirect_uris) do redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{code: auth.token} - url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) else From f24d2f714f44175cae9fcd878de1629ee32be73c Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 5 Jun 2020 17:18:48 +0200 Subject: [PATCH 224/375] Credo fixes --- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 10136789a..5258212ec 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -15,8 +15,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.Streamer alias Pleroma.Web.Push + alias Pleroma.Web.Streamer def handle(object, meta \\ []) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index f97ab510e..d2347cdc9 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -9,8 +9,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Activity alias Pleroma.EarmarkRenderer alias Pleroma.FollowingRelationship - alias Pleroma.Notification alias Pleroma.Maps + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo From 167812a3f2c1470012cb161f3c5ba4c021fbad97 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 5 Jun 2020 23:18:29 +0400 Subject: [PATCH 225/375] Fix pagination --- lib/pleroma/pagination.ex | 3 +++ lib/pleroma/web/activity_pub/activity_pub_controller.ex | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index 0ccc7b1f2..1b99e44f9 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -16,6 +16,9 @@ defmodule Pleroma.Pagination do @default_limit 20 @max_limit 40 + @page_keys ["max_id", "min_id", "limit", "since_id", "order"] + + def page_keys, do: @page_keys @spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5b8441384..f0b5c6e93 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -238,6 +238,7 @@ def outbox( params |> Map.drop(["nickname", "page"]) |> Map.put("include_poll_votes", true) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) activities = ActivityPub.fetch_user_activities(user, for_user, params) @@ -354,6 +355,7 @@ def read_inbox( |> Map.drop(["nickname", "page"]) |> Map.put("blocking_user", user) |> Map.put("user", user) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) activities = [user.ap_id | User.following(user)] From 4e8c0eecd5179428a47795380a9da1ab0419e024 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 09:46:07 +0200 Subject: [PATCH 226/375] WebPush: Don't break on contentless chat messages. --- lib/pleroma/web/push/impl.ex | 7 +++++++ test/web/push/impl_test.exs | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 006a242af..cdb827e76 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -124,6 +124,13 @@ def build_content(notification, actor, object, mastodon_type) do def format_body(activity, actor, object, mastodon_type \\ nil) + def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do + case content do + nil -> "@#{actor.nickname}: (Attachment)" + content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" + end + end + def format_body( %{activity: %{data: %{"type" => "Create"}}}, actor, diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 8fb7faaa5..b48952b29 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.Push.ImplTest do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription @@ -213,6 +214,30 @@ test "builds content for chat messages" do } end + test "builds content for chat messages with no content" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: (Attachment)", + title: "New Chat Message" + } + end + test "hides details for notifications when privacy option enabled" do user = insert(:user, nickname: "Bob") user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) From c5e3f2454c736e09de5c433a2bf578e8eb0e70c3 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 10:35:38 +0200 Subject: [PATCH 227/375] Docs: Unify parameters in examples. --- docs/API/chats.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index abeee698f..761047336 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -41,7 +41,7 @@ This is the overview of using the API. The API is also documented via OpenAPI, s To create or get an existing Chat for a certain recipient (identified by Account ID) you can call: -`POST /api/v1/pleroma/chats/by-account-id/{account_id}` +`POST /api/v1/pleroma/chats/by-account-id/:account_id` The account id is the normal FlakeId of the user ``` @@ -136,7 +136,7 @@ The usual pagination options are implemented. For a given Chat id, you can get the associated messages with -`GET /api/v1/pleroma/chats/{id}/messages` +`GET /api/v1/pleroma/chats/:id/messages` This will return all messages, sorted by most recent to least recent. The usual pagination options are implemented. @@ -177,7 +177,7 @@ Returned data: Posting a chat message for given Chat id works like this: -`POST /api/v1/pleroma/chats/{id}/messages` +`POST /api/v1/pleroma/chats/:id/messages` Parameters: - content: The text content of the message. Optional if media is attached. @@ -210,7 +210,7 @@ Returned data: Deleting a chat message for given Chat id works like this: -`DELETE /api/v1/pleroma/chats/{chat_id}/messages/{message_id}` +`DELETE /api/v1/pleroma/chats/:chat_id/messages/:message_id` Returned data is the deleted message. From 239d03499ebe9196c099b6c8ded05f1f6634c09d Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 10:38:45 +0200 Subject: [PATCH 228/375] Chat: creation_cng -> changeset Make our usage of this more uniform. --- lib/pleroma/chat.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 5aefddc5e..4fe31de94 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Chat do timestamps() end - def creation_cng(struct, params) do + def changeset(struct, params) do struct |> cast(params, [:user_id, :recipient]) |> validate_change(:recipient, fn @@ -49,7 +49,7 @@ def get(user_id, recipient) do def get_or_create(user_id, recipient) do %__MODULE__{} - |> creation_cng(%{user_id: user_id, recipient: recipient}) + |> changeset(%{user_id: user_id, recipient: recipient}) |> Repo.insert( # Need to set something, otherwise we get nothing back at all on_conflict: [set: [recipient: recipient]], @@ -60,7 +60,7 @@ def get_or_create(user_id, recipient) do def bump_or_create(user_id, recipient) do %__MODULE__{} - |> creation_cng(%{user_id: user_id, recipient: recipient}) + |> changeset(%{user_id: user_id, recipient: recipient}) |> Repo.insert( on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], conflict_target: [:user_id, :recipient] From 137adef6e061a1d7d7fc704feac27ebf5319a768 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 10:42:24 +0200 Subject: [PATCH 229/375] ChatMessageReference: Use FlakeId.Ecto.Type No need for compat because this is brand new. --- lib/pleroma/chat_message_reference.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat_message_reference.ex index fc2aaae7a..6e836cad9 100644 --- a/lib/pleroma/chat_message_reference.ex +++ b/lib/pleroma/chat_message_reference.ex @@ -17,7 +17,7 @@ defmodule Pleroma.ChatMessageReference do import Ecto.Changeset import Ecto.Query - @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} schema "chat_message_references" do belongs_to(:object, Object) From ca0e6e702be3714bb40ff0fb48e9c08aaf322fff Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 11:51:10 +0200 Subject: [PATCH 230/375] ChatMessageReference -> Chat.MessageReference --- .../message_reference.ex} | 2 +- lib/pleroma/web/activity_pub/side_effects.ex | 8 ++--- .../mastodon_api/views/notification_view.ex | 8 ++--- .../controllers/chat_controller.ex | 30 +++++++++---------- .../message_reference_view.ex} | 2 +- .../web/pleroma_api/views/chat_view.ex | 10 +++---- lib/pleroma/web/streamer/streamer.ex | 4 +-- .../message_reference_test.exs} | 6 ++-- test/web/activity_pub/side_effects_test.exs | 8 ++--- .../views/notification_view_test.exs | 9 +++--- .../controllers/chat_controller_test.exs | 18 +++++------ .../message_reference_view_test.exs} | 15 +++++----- test/web/pleroma_api/views/chat_view_test.exs | 8 ++--- test/web/streamer/streamer_test.exs | 6 ++-- 14 files changed, 66 insertions(+), 68 deletions(-) rename lib/pleroma/{chat_message_reference.ex => chat/message_reference.ex} (98%) rename lib/pleroma/web/pleroma_api/views/{chat_message_reference_view.ex => chat/message_reference_view.ex} (95%) rename test/{chat_message_reference_test.exs => chat/message_reference_test.exs} (82%) rename test/web/pleroma_api/views/{chat_message_reference_view_test.exs => chat/message_reference_view_test.exs} (77%) diff --git a/lib/pleroma/chat_message_reference.ex b/lib/pleroma/chat/message_reference.ex similarity index 98% rename from lib/pleroma/chat_message_reference.ex rename to lib/pleroma/chat/message_reference.ex index 6e836cad9..4b201db2e 100644 --- a/lib/pleroma/chat_message_reference.ex +++ b/lib/pleroma/chat/message_reference.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.ChatMessageReference do +defmodule Pleroma.Chat.MessageReference do @moduledoc """ A reference that builds a relation between an AP chat message that a user can see and whether it has been seen by them, or should be displayed to them. Used to build the chat view that is presented to the user. diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 5258212ec..1e9d6c2fc 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do """ alias Pleroma.Activity alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -111,7 +111,7 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, Object.decrease_replies_count(in_reply_to) end - ChatMessageReference.delete_for_object(deleted_object) + MessageReference.delete_for_object(deleted_object) ActivityPub.stream_out(object) ActivityPub.stream_out_participations(deleted_object, user) @@ -146,13 +146,13 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do |> Enum.each(fn [user, other_user] -> if user.local do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - {:ok, cm_ref} = ChatMessageReference.create(chat, object, user.ap_id != actor.ap_id) + {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id) # We add a cache of the unread value here so that it # doesn't change when being streamed out chat = chat - |> Map.put(:unread, ChatMessageReference.unread_count_for_chat(chat)) + |> Map.put(:unread, MessageReference.unread_count_for_chat(chat)) Streamer.stream( ["user", "user:pleroma_chat"], diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 2ae82eb2d..b11578623 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do use Pleroma.Web, :view alias Pleroma.Activity - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User @@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView @parent_types ~w{Like Announce EmojiReact} @@ -139,9 +139,9 @@ defp put_chat_message(response, activity, reading_user, opts) do object = Object.normalize(activity) author = User.get_cached_by_ap_id(object.data["actor"]) chat = Pleroma.Chat.get(reading_user.id, author.ap_id) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref}) - chat_message_render = ChatMessageReferenceView.render("show.json", render_opts) + chat_message_render = MessageReferenceView.render("show.json", render_opts) Map.put(response, :chat_message, chat_message_render) end diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 01d47045d..d6b3415d1 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -6,14 +6,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Activity alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.Pagination alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.ChatView import Ecto.Query @@ -46,13 +46,13 @@ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ message_id: message_id, id: chat_id }) do - with %ChatMessageReference{} = cm_ref <- - ChatMessageReference.get_by_id(message_id), + with %MessageReference{} = cm_ref <- + MessageReference.get_by_id(message_id), ^chat_id <- cm_ref.chat_id |> to_string(), %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), {:ok, _} <- remove_or_delete(cm_ref, user) do conn - |> put_view(ChatMessageReferenceView) + |> put_view(MessageReferenceView) |> render("show.json", chat_message_reference: cm_ref) else _e -> @@ -71,7 +71,7 @@ defp remove_or_delete( defp remove_or_delete(cm_ref, _) do cm_ref - |> ChatMessageReference.delete() + |> MessageReference.delete() end def post_chat_message( @@ -87,9 +87,9 @@ def post_chat_message( media_id: params[:media_id] ), message <- Object.normalize(activity, false), - cm_ref <- ChatMessageReference.for_chat_and_object(chat, message) do + cm_ref <- MessageReference.for_chat_and_object(chat, message) do conn - |> put_view(ChatMessageReferenceView) + |> put_view(MessageReferenceView) |> render("show.json", for: user, chat_message_reference: cm_ref) end end @@ -98,20 +98,20 @@ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ id: chat_id, message_id: message_id }) do - with %ChatMessageReference{} = cm_ref <- - ChatMessageReference.get_by_id(message_id), + with %MessageReference{} = cm_ref <- + MessageReference.get_by_id(message_id), ^chat_id <- cm_ref.chat_id |> to_string(), %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), - {:ok, cm_ref} <- ChatMessageReference.mark_as_read(cm_ref) do + {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do conn - |> put_view(ChatMessageReferenceView) + |> put_view(MessageReferenceView) |> render("show.json", for: user, chat_message_reference: cm_ref) end end def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), - {_n, _} <- ChatMessageReference.set_all_seen_for_chat(chat) do + {_n, _} <- MessageReference.set_all_seen_for_chat(chat) do conn |> put_view(ChatView) |> render("show.json", chat: chat) @@ -122,11 +122,11 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = para with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do cm_refs = chat - |> ChatMessageReference.for_chat_query() + |> MessageReference.for_chat_query() |> Pagination.fetch_paginated(params |> stringify_keys()) conn - |> put_view(ChatMessageReferenceView) + |> put_view(MessageReferenceView) |> render("index.json", for: user, chat_message_references: cm_refs) else _ -> diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex similarity index 95% rename from lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex rename to lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex index 592bb17f0..f2112a86e 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_reference_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceView do +defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do use Pleroma.Web, :view alias Pleroma.User diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index 91d50dd1e..d4c10977f 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -6,24 +6,24 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do use Pleroma.Web, :view alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.User alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView def render("show.json", %{chat: %Chat{} = chat} = opts) do recipient = User.get_cached_by_ap_id(chat.recipient) - last_message = opts[:last_message] || ChatMessageReference.last_message_for_chat(chat) + last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat) %{ id: chat.id |> to_string(), account: AccountView.render("show.json", Map.put(opts, :user, recipient)), - unread: Map.get(chat, :unread) || ChatMessageReference.unread_count_for_chat(chat), + unread: Map.get(chat, :unread) || MessageReference.unread_count_for_chat(chat), last_message: last_message && - ChatMessageReferenceView.render("show.json", chat_message_reference: last_message), + MessageReferenceView.render("show.json", chat_message_reference: last_message), updated_at: Utils.to_masto_date(chat.updated_at) } end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index b22297955..d1d2c9b9c 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Streamer do require Logger alias Pleroma.Activity - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Config alias Pleroma.Conversation.Participation alias Pleroma.Notification @@ -187,7 +187,7 @@ defp do_stream(topic, %Notification{} = item) end) end - defp do_stream(topic, {user, %ChatMessageReference{} = cm_ref}) + defp do_stream(topic, {user, %MessageReference{} = cm_ref}) when topic in ["user", "user:pleroma_chat"] do topic = "#{topic}:#{user.id}" diff --git a/test/chat_message_reference_test.exs b/test/chat/message_reference_test.exs similarity index 82% rename from test/chat_message_reference_test.exs rename to test/chat/message_reference_test.exs index 66bf493b4..aaa7c1ad4 100644 --- a/test/chat_message_reference_test.exs +++ b/test/chat/message_reference_test.exs @@ -2,11 +2,11 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.ChatMessageReferenceTest do +defmodule Pleroma.Chat.MessageReferenceTest do use Pleroma.DataCase, async: true alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -21,7 +21,7 @@ test "it returns the last message in a chat" do {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - message = ChatMessageReference.last_message_for_chat(chat) + message = MessageReference.last_message_for_chat(chat) assert message.object.data["content"] == "ho" end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 43ffe1337..b1afa6a2e 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do alias Pleroma.Activity alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -391,7 +391,7 @@ test "it streams the created ChatMessage" do end end - test "it creates a Chat and ChatMessageReferences for the local users and bumps the unread count, except for the author" do + test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do author = insert(:user, local: true) recipient = insert(:user, local: true) @@ -431,14 +431,14 @@ test "it creates a Chat and ChatMessageReferences for the local users and bumps chat = Chat.get(author.id, recipient.ap_id) - [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() assert cm_ref.object.data["content"] == "hey" assert cm_ref.unread == false chat = Chat.get(recipient.id, author.ap_id) - [cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all() + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() assert cm_ref.object.data["content"] == "hey" assert cm_ref.unread == true diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index c5691341a..b2fa5b302 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Activity alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView import Pleroma.Factory defp test_notifications_rendering(notifications, user, expected_result) do @@ -45,15 +45,14 @@ test "ChatMessage notification" do object = Object.normalize(activity) chat = Chat.get(recipient.id, user.ap_id) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) expected = %{ id: to_string(notification.id), pleroma: %{is_seen: false}, type: "pleroma:chat_mention", account: AccountView.render("show.json", %{user: user, for: recipient}), - chat_message: - ChatMessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), + chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), created_at: Utils.to_masto_date(notification.inserted_at) } diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 7af6dec1c..e73e4a32e 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -23,7 +23,7 @@ test "it marks one message as read", %{conn: conn, user: user} do {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) object = Object.normalize(create, false) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) assert cm_ref.unread == true @@ -34,7 +34,7 @@ test "it marks one message as read", %{conn: conn, user: user} do assert result["unread"] == false - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) assert cm_ref.unread == false end @@ -50,7 +50,7 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) object = Object.normalize(create, false) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) assert cm_ref.unread == true @@ -61,7 +61,7 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do assert result["unread"] == 0 - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) assert cm_ref.unread == false end @@ -139,7 +139,7 @@ test "it deletes a message from the chat", %{conn: conn, user: user} do chat = Chat.get(user.id, recipient.ap_id) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) # Deleting your own message removes the message and the reference result = @@ -149,12 +149,12 @@ test "it deletes a message from the chat", %{conn: conn, user: user} do |> json_response_and_validate_schema(200) assert result["id"] == cm_ref.id - refute ChatMessageReference.get_by_id(cm_ref.id) + refute MessageReference.get_by_id(cm_ref.id) assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) # Deleting other people's messages just removes the reference object = Object.normalize(other_message, false) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) result = conn @@ -163,7 +163,7 @@ test "it deletes a message from the chat", %{conn: conn, user: user} do |> json_response_and_validate_schema(200) assert result["id"] == cm_ref.id - refute ChatMessageReference.get_by_id(cm_ref.id) + refute MessageReference.get_by_id(cm_ref.id) assert Object.get_by_id(object.id) end end diff --git a/test/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/web/pleroma_api/views/chat/message_reference_view_test.exs similarity index 77% rename from test/web/pleroma_api/views/chat_message_reference_view_test.exs rename to test/web/pleroma_api/views/chat/message_reference_view_test.exs index b53bd3490..e5b165255 100644 --- a/test/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/web/pleroma_api/views/chat/message_reference_view_test.exs @@ -2,15 +2,15 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do +defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceViewTest do use Pleroma.DataCase alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView import Pleroma.Factory @@ -31,9 +31,9 @@ test "it displays a chat message" do object = Object.normalize(activity) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) - chat_message = ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref) + chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) assert chat_message[:id] == cm_ref.id assert chat_message[:content] == "kippis :firefox:" @@ -47,10 +47,9 @@ test "it displays a chat message" do object = Object.normalize(activity) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) - chat_message_two = - ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref) + chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) assert chat_message_two[:id] == cm_ref.id assert chat_message_two[:content] == "gkgkgk" diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index f77584dd1..f7af5d4e0 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -6,12 +6,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do use Pleroma.DataCase alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.ChatView import Pleroma.Factory @@ -55,9 +55,9 @@ test "it represents a chat" do represented_chat = ChatView.render("show.json", chat: chat) - cm_ref = ChatMessageReference.for_chat_and_object(chat, chat_message) + cm_ref = MessageReference.for_chat_and_object(chat, chat_message) assert represented_chat[:last_message] == - ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref) + MessageReferenceView.render("show.json", chat_message_reference: cm_ref) end end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 893ae5449..245f6e63f 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.StreamerTest do import Pleroma.Factory alias Pleroma.Chat - alias Pleroma.ChatMessageReference + alias Pleroma.Chat.MessageReference alias Pleroma.Conversation.Participation alias Pleroma.List alias Pleroma.Object @@ -155,7 +155,7 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} d {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") object = Object.normalize(create_activity, false) chat = Chat.get(user.id, other_user.ap_id) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) cm_ref = %{cm_ref | chat: chat, object: object} Streamer.get_topic_and_add_socket("user:pleroma_chat", user) @@ -173,7 +173,7 @@ test "it sends chat messages to the 'user' stream", %{user: user} do {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") object = Object.normalize(create_activity, false) chat = Chat.get(user.id, other_user.ap_id) - cm_ref = ChatMessageReference.for_chat_and_object(chat, object) + cm_ref = MessageReference.for_chat_and_object(chat, object) cm_ref = %{cm_ref | chat: chat, object: object} Streamer.get_topic_and_add_socket("user", user) From 9fa3f0b156f92ba575b58b191685fa068a83f4d2 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 13:08:45 +0200 Subject: [PATCH 231/375] Notification: Change type of `type` to an enum. --- lib/pleroma/notification.ex | 3 ++ ..._change_type_to_enum_for_notifications.exs | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 49e27c05a..5c8994e35 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -30,6 +30,9 @@ defmodule Pleroma.Notification do schema "notifications" do field(:seen, :boolean, default: false) + # This is an enum type in the database. If you add a new notification type, + # remembert to add a migration to add it to the `notifications_type` enum + # as well. field(:type, :string) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) diff --git a/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs new file mode 100644 index 000000000..9ea34436b --- /dev/null +++ b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs @@ -0,0 +1,36 @@ +defmodule Pleroma.Repo.Migrations.ChangeTypeToEnumForNotifications do + use Ecto.Migration + + def up do + """ + create type notification_type as enum ( + 'follow', + 'follow_request', + 'mention', + 'move', + 'pleroma:emoji_reaction', + 'pleroma:chat_mention', + 'reblog', + 'favourite' + ) + """ + |> execute() + + """ + alter table notifications + alter column type type notification_type using (type::notification_type) + """ + |> execute() + end + + def down do + alter table(:notifications) do + modify(:type, :string) + end + + """ + drop type notification_type + """ + |> execute() + end +end From 9189b489eef29be723389e1b3642a843bc0d01bc Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 15:33:02 +0200 Subject: [PATCH 232/375] Migrations: Move Notification migration code to helper --- lib/pleroma/migration_helper.ex | 85 +++++++++++++++++++ lib/pleroma/notification.ex | 37 +------- ...0602125218_backfill_notification_types.exs | 2 +- test/migration_helper_test.exs | 56 ++++++++++++ test/notification_test.exs | 42 --------- 5 files changed, 144 insertions(+), 78 deletions(-) create mode 100644 lib/pleroma/migration_helper.ex create mode 100644 test/migration_helper_test.exs diff --git a/lib/pleroma/migration_helper.ex b/lib/pleroma/migration_helper.ex new file mode 100644 index 000000000..e6346aff1 --- /dev/null +++ b/lib/pleroma/migration_helper.ex @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MigrationHelper do + alias Pleroma.User + alias Pleroma.Object + alias Pleroma.Notification + alias Pleroma.Repo + + import Ecto.Query + + def fill_in_notification_types do + query = + from(n in Pleroma.Notification, + where: is_nil(n.type), + preload: :activity + ) + + query + |> Repo.all() + |> Enum.each(fn notification -> + type = + notification.activity + |> type_from_activity() + + notification + |> Notification.changeset(%{type: type}) + |> Repo.update() + end) + end + + # This is copied over from Notifications to keep this stable. + defp type_from_activity(%{data: %{"type" => type}} = activity) do + case type do + "Follow" -> + accepted_function = fn activity -> + with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), + %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do + Pleroma.FollowingRelationship.following?(follower, followed) + end + end + + if accepted_function.(activity) do + "follow" + else + "follow_request" + end + + "Announce" -> + "reblog" + + "Like" -> + "favourite" + + "Move" -> + "move" + + "EmojiReact" -> + "pleroma:emoji_reaction" + + # Compatibility with old reactions + "EmojiReaction" -> + "pleroma:emoji_reaction" + + "Create" -> + activity + |> type_from_activity_object() + + t -> + raise "No notification type for activity type #{t}" + end + end + + defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention" + + defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do + object = Object.get_by_ap_id(activity.data["object"]) + + case object && object.data["type"] do + "ChatMessage" -> "pleroma:chat_mention" + _ -> "mention" + end + end +end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 5c8994e35..682a26912 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -40,26 +40,6 @@ defmodule Pleroma.Notification do timestamps() end - def fill_in_notification_types do - query = - from(n in __MODULE__, - where: is_nil(n.type), - preload: :activity - ) - - query - |> Repo.all() - |> Enum.each(fn notification -> - type = - notification.activity - |> type_from_activity(no_cachex: true) - - notification - |> changeset(%{type: type}) - |> Repo.update() - end) - end - def update_notification_type(user, activity) do with %__MODULE__{} = notification <- Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do @@ -371,23 +351,10 @@ defp do_create_notifications(%Activity{} = activity, options) do {:ok, notifications} end - defp type_from_activity(%{data: %{"type" => type}} = activity, opts \\ []) do + defp type_from_activity(%{data: %{"type" => type}} = activity) do case type do "Follow" -> - accepted_function = - if Keyword.get(opts, :no_cachex, false) do - # A special function to make this usable in a migration. - fn activity -> - with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), - %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do - Pleroma.FollowingRelationship.following?(follower, followed) - end - end - else - &Activity.follow_accepted?/1 - end - - if accepted_function.(activity) do + if Activity.follow_accepted?(activity) do "follow" else "follow_request" diff --git a/priv/repo/migrations/20200602125218_backfill_notification_types.exs b/priv/repo/migrations/20200602125218_backfill_notification_types.exs index 493c0280c..58943fad0 100644 --- a/priv/repo/migrations/20200602125218_backfill_notification_types.exs +++ b/priv/repo/migrations/20200602125218_backfill_notification_types.exs @@ -2,7 +2,7 @@ defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do use Ecto.Migration def up do - Pleroma.Notification.fill_in_notification_types() + Pleroma.MigrationHelper.fill_in_notification_types() end def down do diff --git a/test/migration_helper_test.exs b/test/migration_helper_test.exs new file mode 100644 index 000000000..1c8173987 --- /dev/null +++ b/test/migration_helper_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MigrationHelperTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.MigrationHelper + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "fill_in_notification_types" do + test "it fills in missing notification types" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) + {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") + {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + {:ok, like} = CommonAPI.favorite(other_user, post.id) + {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + + data = + react_2.data + |> Map.put("type", "EmojiReaction") + + {:ok, react_2} = + react_2 + |> Activity.change(%{data: data}) + |> Repo.update() + + assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) + + MigrationHelper.fill_in_notification_types() + + assert %{type: "mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) + + assert %{type: "favourite"} = + Repo.get_by(Notification, user_id: user.id, activity_id: like.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) + + assert %{type: "pleroma:chat_mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) + end + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs index f2115a29e..b9bbdceca 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.NotificationTest do import Pleroma.Factory import Mock - alias Pleroma.Activity alias Pleroma.FollowingRelationship alias Pleroma.Notification alias Pleroma.Repo @@ -22,47 +21,6 @@ defmodule Pleroma.NotificationTest do alias Pleroma.Web.Push alias Pleroma.Web.Streamer - describe "fill_in_notification_types" do - test "it fills in missing notification types" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) - {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") - {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - {:ok, like} = CommonAPI.favorite(other_user, post.id) - {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - - data = - react_2.data - |> Map.put("type", "EmojiReaction") - - {:ok, react_2} = - react_2 - |> Activity.change(%{data: data}) - |> Repo.update() - - assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) - - Notification.fill_in_notification_types() - - assert %{type: "mention"} = - Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) - - assert %{type: "favourite"} = - Repo.get_by(Notification, user_id: user.id, activity_id: like.id) - - assert %{type: "pleroma:emoji_reaction"} = - Repo.get_by(Notification, user_id: user.id, activity_id: react.id) - - assert %{type: "pleroma:emoji_reaction"} = - Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) - - assert %{type: "pleroma:chat_mention"} = - Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) - end - end - describe "create_notifications" do test "creates a notification for an emoji reaction" do user = insert(:user) From f77d4a302d8ee5999979136290caac556cac8873 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 15:51:08 +0200 Subject: [PATCH 233/375] Credo fixes. --- lib/pleroma/migration_helper.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/migration_helper.ex b/lib/pleroma/migration_helper.ex index e6346aff1..a20d27a01 100644 --- a/lib/pleroma/migration_helper.ex +++ b/lib/pleroma/migration_helper.ex @@ -3,10 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MigrationHelper do - alias Pleroma.User - alias Pleroma.Object alias Pleroma.Notification + alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.User import Ecto.Query From e1b07402ab077899dd5b9c0023fbe1c48af259e9 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 23 Mar 2020 22:52:25 +0100 Subject: [PATCH 234/375] User: Add raw_bio, storing unformatted bio Related: https://git.pleroma.social/pleroma/pleroma/issues/1643 --- lib/pleroma/user.ex | 13 +++++++++- .../controllers/account_controller.ex | 1 + .../web/mastodon_api/views/account_view.ex | 13 +--------- .../20200322174133_user_raw_bio.exs | 9 +++++++ .../20200328193433_populate_user_raw_bio.exs | 25 +++++++++++++++++++ test/support/factory.ex | 3 ++- .../update_credentials_test.exs | 13 +++++++--- .../mastodon_api/views/account_view_test.exs | 3 ++- 8 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 priv/repo/migrations/20200322174133_user_raw_bio.exs create mode 100644 priv/repo/migrations/20200328193433_populate_user_raw_bio.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 72ee2d58e..23ca8c9f3 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -79,6 +79,7 @@ defmodule Pleroma.User do schema "users" do field(:bio, :string) + field(:raw_bio, :string) field(:email, :string) field(:name, :string) field(:nickname, :string) @@ -432,6 +433,7 @@ def update_changeset(struct, params \\ %{}) do params, [ :bio, + :raw_bio, :name, :emoji, :avatar, @@ -607,7 +609,16 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do struct |> confirmation_changeset(need_confirmation: need_confirmation?) - |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji]) + |> cast(params, [ + :bio, + :raw_bio, + :email, + :name, + :nickname, + :password, + :password_confirmation, + :emoji + ]) |> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_confirmation(:password) |> unique_constraint(:email) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 5734bb854..ebfa533dd 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -165,6 +165,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p end) |> Maps.put_if_present(:name, params[:display_name]) |> Maps.put_if_present(:bio, params[:note]) + |> Maps.put_if_present(:raw_bio, params[:note]) |> Maps.put_if_present(:avatar, params[:avatar]) |> Maps.put_if_present(:banner, params[:header]) |> Maps.put_if_present(:background, params[:pleroma_background_image]) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 04c419d2f..5326b02c6 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -224,7 +224,7 @@ defp do_render("show.json", %{user: user} = opts) do fields: user.fields, bot: bot, source: %{ - note: prepare_user_bio(user), + note: user.raw_bio || "", sensitive: false, fields: user.raw_fields, pleroma: %{ @@ -259,17 +259,6 @@ defp do_render("show.json", %{user: user} = opts) do |> maybe_put_unread_notification_count(user, opts[:for]) end - defp prepare_user_bio(%User{bio: ""}), do: "" - - defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do - bio - |> String.replace(~r(
), "\n") - |> Pleroma.HTML.strip_tags() - |> HtmlEntities.decode() - end - - defp prepare_user_bio(_), do: "" - defp username_from_nickname(string) when is_binary(string) do hd(String.split(string, "@")) end diff --git a/priv/repo/migrations/20200322174133_user_raw_bio.exs b/priv/repo/migrations/20200322174133_user_raw_bio.exs new file mode 100644 index 000000000..ddf9be4f5 --- /dev/null +++ b/priv/repo/migrations/20200322174133_user_raw_bio.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.UserRawBio do + use Ecto.Migration + + def change do + alter table(:users) do + add_if_not_exists(:raw_bio, :text) + end + end +end diff --git a/priv/repo/migrations/20200328193433_populate_user_raw_bio.exs b/priv/repo/migrations/20200328193433_populate_user_raw_bio.exs new file mode 100644 index 000000000..cb35db3f5 --- /dev/null +++ b/priv/repo/migrations/20200328193433_populate_user_raw_bio.exs @@ -0,0 +1,25 @@ +defmodule Pleroma.Repo.Migrations.PopulateUserRawBio do + use Ecto.Migration + import Ecto.Query + alias Pleroma.User + alias Pleroma.Repo + + def change do + {:ok, _} = Application.ensure_all_started(:fast_sanitize) + + User.Query.build(%{local: true}) + |> select([u], struct(u, [:id, :ap_id, :bio])) + |> Repo.stream() + |> Enum.each(fn %{bio: bio} = user -> + if bio do + raw_bio = + bio + |> String.replace(~r(
), "\n") + |> Pleroma.HTML.strip_tags() + + Ecto.Changeset.cast(user, %{raw_bio: raw_bio}, [:raw_bio]) + |> Repo.update() + end + end) + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 6e3676aca..1a9b96180 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -42,7 +42,8 @@ def user_factory do user | ap_id: User.ap_id(user), follower_address: User.ap_followers(user), - following_address: User.ap_following(user) + following_address: User.ap_following(user), + raw_bio: user.bio } end 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 7c420985d..76e6d603a 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 @@ -83,10 +83,9 @@ test "sets user settings in a generic way", %{conn: conn} do test "updates the user's bio", %{conn: conn} do user2 = insert(:user) - conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "note" => "I drink #cofe with @#{user2.nickname}\n\nsuya.." - }) + raw_bio = "I drink #cofe with @#{user2.nickname}\n\nsuya.." + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) assert user_data = json_response_and_validate_schema(conn, 200) @@ -94,6 +93,12 @@ test "updates the user's bio", %{conn: conn} do ~s(I drink #cofe with @#{user2.nickname}

suya..) + + assert user_data["source"]["note"] == raw_bio + + user = Repo.get(User, user_data["id"]) + + assert user.raw_bio == raw_bio 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 f91333e5c..7ac70dc58 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -33,7 +33,8 @@ test "Represent a user account" do bio: "valid html. a
b
c
d
f '&<>\"", inserted_at: ~N[2017-08-15 15:47:06.597036], - emoji: %{"karjalanpiirakka" => "/file.png"} + emoji: %{"karjalanpiirakka" => "/file.png"}, + raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"" }) expected = %{ From f4cf4ae16ee84655bf6630cf7e98e9eef2f410cc Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 16:48:02 +0200 Subject: [PATCH 235/375] ChatController: Use new oauth scope *:chats. --- .../web/api_spec/operations/chat_operation.ex | 14 +++++++------- .../pleroma_api/controllers/chat_controller.ex | 4 ++-- .../controllers/chat_controller_test.exs | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 6ad325113..74c3ad0bd 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -33,7 +33,7 @@ def mark_as_read_operation do }, security: [ %{ - "oAuth" => ["write"] + "oAuth" => ["write:chats"] } ] } @@ -58,7 +58,7 @@ def mark_message_as_read_operation do }, security: [ %{ - "oAuth" => ["write"] + "oAuth" => ["write:chats"] } ] } @@ -120,7 +120,7 @@ def create_operation do }, security: [ %{ - "oAuth" => ["write"] + "oAuth" => ["write:chats"] } ] } @@ -137,7 +137,7 @@ def index_operation do }, security: [ %{ - "oAuth" => ["read"] + "oAuth" => ["read:chats"] } ] } @@ -161,7 +161,7 @@ def messages_operation do }, security: [ %{ - "oAuth" => ["read"] + "oAuth" => ["read:chats"] } ] } @@ -187,7 +187,7 @@ def post_chat_message_operation do }, security: [ %{ - "oAuth" => ["write"] + "oAuth" => ["write:chats"] } ] } @@ -212,7 +212,7 @@ def delete_message_operation do }, security: [ %{ - "oAuth" => ["write"] + "oAuth" => ["write:chats"] } ] } diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index d6b3415d1..983550b13 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} + %{scopes: ["write:chats"]} when action in [ :post_chat_message, :create, @@ -35,7 +35,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do plug( OAuthScopesPlug, - %{scopes: ["read:statuses"]} when action in [:messages, :index, :show] + %{scopes: ["read:chats"]} when action in [:messages, :index, :show] ) plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index e73e4a32e..2128fd9dd 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -14,7 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do import Pleroma.Factory describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do - setup do: oauth_access(["write:statuses"]) + setup do: oauth_access(["write:chats"]) test "it marks one message as read", %{conn: conn, user: user} do other_user = insert(:user) @@ -41,7 +41,7 @@ test "it marks one message as read", %{conn: conn, user: user} do end describe "POST /api/v1/pleroma/chats/:id/read" do - setup do: oauth_access(["write:statuses"]) + setup do: oauth_access(["write:chats"]) test "it marks all messages in a chat as read", %{conn: conn, user: user} do other_user = insert(:user) @@ -68,7 +68,7 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do end describe "POST /api/v1/pleroma/chats/:id/messages" do - setup do: oauth_access(["write:statuses"]) + setup do: oauth_access(["write:chats"]) test "it posts a message to the chat", %{conn: conn, user: user} do other_user = insert(:user) @@ -125,7 +125,7 @@ test "it works with an attachment", %{conn: conn, user: user} do end describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do - setup do: oauth_access(["write:statuses"]) + setup do: oauth_access(["write:chats"]) test "it deletes a message from the chat", %{conn: conn, user: user} do recipient = insert(:user) @@ -169,7 +169,7 @@ test "it deletes a message from the chat", %{conn: conn, user: user} do end describe "GET /api/v1/pleroma/chats/:id/messages" do - setup do: oauth_access(["read:statuses"]) + setup do: oauth_access(["read:chats"]) test "it paginates", %{conn: conn, user: user} do recipient = insert(:user) @@ -229,7 +229,7 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do end describe "POST /api/v1/pleroma/chats/by-account-id/:id" do - setup do: oauth_access(["write:statuses"]) + setup do: oauth_access(["write:chats"]) test "it creates or returns a chat", %{conn: conn} do other_user = insert(:user) @@ -244,7 +244,7 @@ test "it creates or returns a chat", %{conn: conn} do end describe "GET /api/v1/pleroma/chats/:id" do - setup do: oauth_access(["read:statuses"]) + setup do: oauth_access(["read:chats"]) test "it returns a chat", %{conn: conn, user: user} do other_user = insert(:user) @@ -261,7 +261,7 @@ test "it returns a chat", %{conn: conn, user: user} do end describe "GET /api/v1/pleroma/chats" do - setup do: oauth_access(["read:statuses"]) + setup do: oauth_access(["read:chats"]) test "it does not return chats with users you blocked", %{conn: conn, user: user} do recipient = insert(:user) From 40fc4e974e5f60c3d61702b17029566774898e84 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 6 Jun 2020 16:59:08 +0200 Subject: [PATCH 236/375] Notfication: Add validation of notification types --- lib/pleroma/notification.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 682a26912..3ac8737e2 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -61,9 +61,21 @@ def unread_notifications_count(%User{id: user_id}) do |> Repo.aggregate(:count, :id) end + @notification_types ~w{ + favourite + follow + follow_request + mention + move + pleroma:chat_mention + pleroma:emoji_reaction + reblog + } + def changeset(%Notification{} = notification, attrs) do notification |> cast(attrs, [:seen, :type]) + |> validate_inclusion(:type, @notification_types) end @spec last_read_query(User.t()) :: Ecto.Queryable.t() From 0365053c8dbbcae4a4883f68b7eaec263c14f656 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 09:19:00 +0200 Subject: [PATCH 237/375] AttachmentValidator: Check if the mime type is valid. --- .../activity_pub/object_validators/attachment_validator.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index c4b502cb9..f53bb02be 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -45,11 +45,11 @@ def fix_media_type(data) do data |> Map.put_new("mediaType", data["mimeType"]) - if data["mediaType"] == "" do + if MIME.valid?(data["mediaType"]) do data - |> Map.put("mediaType", "application/octet-stream") else data + |> Map.put("mediaType", "application/octet-stream") end end From 1a11f0e453527070a8ab5511318045470abc95e2 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 14:25:30 +0200 Subject: [PATCH 238/375] Chats: Change id to flake id. --- lib/pleroma/chat.ex | 3 +++ lib/pleroma/chat/message_reference.ex | 2 +- ...20200607112923_change_chat_id_to_flake.exs | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 4fe31de94..24a86371e 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -16,6 +16,8 @@ defmodule Pleroma.Chat do It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. """ + @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + schema "chats" do belongs_to(:user, User, type: FlakeId.Ecto.CompatType) field(:recipient, :string) @@ -63,6 +65,7 @@ def bump_or_create(user_id, recipient) do |> changeset(%{user_id: user_id, recipient: recipient}) |> Repo.insert( on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], + returning: true, conflict_target: [:user_id, :recipient] ) end diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex index 4b201db2e..7ee7508ca 100644 --- a/lib/pleroma/chat/message_reference.ex +++ b/lib/pleroma/chat/message_reference.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Chat.MessageReference do schema "chat_message_references" do belongs_to(:object, Object) - belongs_to(:chat, Chat) + belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType) field(:unread, :boolean, default: true) diff --git a/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs new file mode 100644 index 000000000..f14e269ca --- /dev/null +++ b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs @@ -0,0 +1,23 @@ +defmodule Pleroma.Repo.Migrations.ChangeChatIdToFlake do + use Ecto.Migration + + def up do + execute(""" + alter table chats + drop constraint chats_pkey cascade, + alter column id drop default, + alter column id set data type uuid using cast( lpad( to_hex(id), 32, '0') as uuid), + add primary key (id) + """) + + execute(""" + alter table chat_message_references + alter column chat_id set data type uuid using cast( lpad( to_hex(chat_id), 32, '0') as uuid), + add constraint chat_message_references_chat_id_fkey foreign key (chat_id) references chats(id) on delete cascade + """) + end + + def down do + :ok + end +end From 2cdaac433035d8df3890eae098b55380b9e1c9fc Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 14:52:56 +0200 Subject: [PATCH 239/375] SideEffects: Move streaming of chats to after the transaction. --- lib/pleroma/web/activity_pub/side_effects.ex | 59 ++++++++++++------- .../web/pleroma_api/views/chat_view.ex | 3 +- test/web/activity_pub/side_effects_test.exs | 41 ++++--------- test/web/common_api/common_api_test.exs | 1 + test/web/pleroma_api/views/chat_view_test.exs | 15 ----- 5 files changed, 52 insertions(+), 67 deletions(-) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 1e9d6c2fc..1a1cc675c 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -37,7 +37,7 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do # - Rollback if we couldn't create it # - Set up notifications def handle(%{data: %{"type" => "Create"}} = activity, meta) do - with {:ok, _object, _meta} <- handle_object_creation(meta[:object_data], meta) do + with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do {:ok, notifications} = Notification.create_notifications(activity, do_send: false) meta = @@ -142,24 +142,24 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do actor = User.get_cached_by_ap_id(object.data["actor"]) recipient = User.get_cached_by_ap_id(hd(object.data["to"])) - [[actor, recipient], [recipient, actor]] - |> Enum.each(fn [user, other_user] -> - if user.local do - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id) + streamables = + [[actor, recipient], [recipient, actor]] + |> Enum.map(fn [user, other_user] -> + if user.local do + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id) - # We add a cache of the unread value here so that it - # doesn't change when being streamed out - chat = - chat - |> Map.put(:unread, MessageReference.unread_count_for_chat(chat)) + { + ["user", "user:pleroma_chat"], + {user, %{cm_ref | chat: chat, object: object}} + } + end + end) + |> Enum.filter(& &1) - Streamer.stream( - ["user", "user:pleroma_chat"], - {user, %{cm_ref | chat: chat, object: object}} - ) - end - end) + meta = + meta + |> add_streamables(streamables) {:ok, object, meta} end @@ -208,7 +208,7 @@ def handle_undoing( def handle_undoing(object), do: {:error, ["don't know how to handle", object]} defp send_notifications(meta) do - Keyword.get(meta, :created_notifications, []) + Keyword.get(meta, :notifications, []) |> Enum.each(fn notification -> Streamer.stream(["user", "user:notification"], notification) Push.send(notification) @@ -217,15 +217,32 @@ defp send_notifications(meta) do meta end - defp add_notifications(meta, notifications) do - existing = Keyword.get(meta, :created_notifications, []) + defp send_streamables(meta) do + Keyword.get(meta, :streamables, []) + |> Enum.each(fn {topics, items} -> + Streamer.stream(topics, items) + end) meta - |> Keyword.put(:created_notifications, notifications ++ existing) + end + + defp add_streamables(meta, streamables) do + existing = Keyword.get(meta, :streamables, []) + + meta + |> Keyword.put(:streamables, streamables ++ existing) + end + + defp add_notifications(meta, notifications) do + existing = Keyword.get(meta, :notifications, []) + + meta + |> Keyword.put(:notifications, notifications ++ existing) end def handle_after_transaction(meta) do meta |> send_notifications() + |> send_streamables() end end diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex index d4c10977f..1c996da11 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -14,13 +14,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do def render("show.json", %{chat: %Chat{} = chat} = opts) do recipient = User.get_cached_by_ap_id(chat.recipient) - last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat) %{ id: chat.id |> to_string(), account: AccountView.render("show.json", Map.put(opts, :user, recipient)), - unread: Map.get(chat, :unread) || MessageReference.unread_count_for_chat(chat), + unread: MessageReference.unread_count_for_chat(chat), last_message: last_message && MessageReferenceView.render("show.json", chat_message_reference: last_message), diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index b1afa6a2e..6bbbaae87 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do import Mock describe "handle_after_transaction" do - test "it streams out notifications" do + test "it streams out notifications and streams" do author = insert(:user, local: true) recipient = insert(:user, local: true) @@ -37,7 +37,7 @@ test "it streams out notifications" do {:ok, _create_activity, meta} = SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - assert [notification] = meta[:created_notifications] + assert [notification] = meta[:notifications] with_mocks([ { @@ -58,6 +58,7 @@ test "it streams out notifications" do SideEffects.handle_after_transaction(meta) assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) assert called(Pleroma.Web.Push.send(notification)) end end @@ -362,33 +363,10 @@ test "it streams the created ChatMessage" do {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - with_mock Pleroma.Web.Streamer, [], - stream: fn _, payload -> - case payload do - {^author, cm_ref} -> - assert cm_ref.unread == false + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - {^recipient, cm_ref} -> - assert cm_ref.unread == true - - view = - Pleroma.Web.PleromaAPI.ChatView.render("show.json", - last_message: cm_ref, - chat: cm_ref.chat - ) - - assert view.unread == 1 - - _ -> - nil - end - end do - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], {author, :_})) - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], {recipient, :_})) - end + assert [_, _] = meta[:streamables] end test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do @@ -422,13 +400,18 @@ test "it creates a Chat and MessageReferences for the local users and bumps the SideEffects.handle(create_activity, local: false, object_data: chat_message_data) # The notification gets created - assert [notification] = meta[:created_notifications] + assert [notification] = meta[:notifications] assert notification.activity_id == create_activity.id # But it is not sent out refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) refute called(Pleroma.Web.Push.send(notification)) + # Same for the user chat stream + assert [{topics, _}, _] = meta[:streamables] + assert topics == ["user", "user:pleroma_chat"] + refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + chat = Chat.get(author.id, recipient.ap_id) [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 63b59820e..6bd26050e 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -72,6 +72,7 @@ test "it posts a chat message without content but with an attachment" do assert called(Pleroma.Web.Push.send(notification)) assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) assert activity end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs index f7af5d4e0..14eecb1bd 100644 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -16,21 +16,6 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do import Pleroma.Factory - test "giving a chat with an 'unread' field, it uses that" do - user = insert(:user) - recipient = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - chat = - chat - |> Map.put(:unread, 5) - - represented_chat = ChatView.render("show.json", chat: chat) - - assert represented_chat[:unread] == 5 - end - test "it represents a chat" do user = insert(:user) recipient = insert(:user) From 801e668a97adff4a33451dd7bb48799562ed8796 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 15:38:33 +0200 Subject: [PATCH 240/375] ChatController: Add `last_read_id` option to mark_as_read. --- lib/pleroma/chat/message_reference.ex | 20 +++++++++++----- .../web/api_spec/operations/chat_operation.ex | 18 ++++++++++++++ .../controllers/chat_controller.ex | 3 ++- .../controllers/chat_controller_test.exs | 24 +++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex index 7ee7508ca..131ae0186 100644 --- a/lib/pleroma/chat/message_reference.ex +++ b/lib/pleroma/chat/message_reference.ex @@ -98,12 +98,20 @@ def mark_as_read(cm_ref) do |> Repo.update() end - def set_all_seen_for_chat(chat) do - chat - |> for_chat_query() - |> exclude(:order_by) - |> exclude(:preload) - |> where([cmr], cmr.unread == true) + def set_all_seen_for_chat(chat, last_read_id \\ nil) do + query = + chat + |> for_chat_query() + |> exclude(:order_by) + |> exclude(:preload) + |> where([cmr], cmr.unread == true) + + if last_read_id do + query + |> where([cmr], cmr.id <= ^last_read_id) + else + query + end |> Repo.update_all(set: [unread: false]) end end diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 74c3ad0bd..45fbad311 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -23,6 +23,7 @@ def mark_as_read_operation do summary: "Mark all messages in the chat as read", operationId: "ChatController.mark_as_read", parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")], + requestBody: request_body("Parameters", mark_as_read()), responses: %{ 200 => Operation.response( @@ -333,4 +334,21 @@ def chat_message_create do } } end + + def mark_as_read do + %Schema{ + title: "MarkAsReadRequest", + description: "POST body for marking a number of chat messages as read", + type: :object, + properties: %{ + last_read_id: %Schema{ + type: :string, + description: "The content of your message. Optional." + } + }, + example: %{ + "last_read_id" => "abcdef12456" + } + } + end end diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 983550b13..002b75082 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -111,7 +111,8 @@ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), - {_n, _} <- MessageReference.set_all_seen_for_chat(chat) do + {_n, _} <- + MessageReference.set_all_seen_for_chat(chat, conn.body_params[:last_read_id]) do conn |> put_view(ChatView) |> render("show.json", chat: chat) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 2128fd9dd..63cd89c73 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -65,6 +65,30 @@ test "it marks all messages in a chat as read", %{conn: conn, user: user} do assert cm_ref.unread == false end + + test "it given a `last_read_id` ", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id}) + |> json_response_and_validate_schema(200) + + assert result["unread"] == 1 + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end end describe "POST /api/v1/pleroma/chats/:id/messages" do From 680fa5fa36d8b30a9a9749edacf1a2c69fded29a Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 15:41:46 +0200 Subject: [PATCH 241/375] Docs: Update docs on mark as read. --- docs/API/chats.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/API/chats.md b/docs/API/chats.md index 761047336..81ff57941 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -79,6 +79,11 @@ To set the `unread` count of a chat to 0, call `POST /api/v1/pleroma/chats/:id/read` + +Parameters: +- last_read_id: Given this id, all chat messages until this one will be marked as read. This should always be used. + + Returned data: ```json From 8d9e58688712ea416109aaee2883cc9ace644e02 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sun, 7 Jun 2020 17:31:37 +0200 Subject: [PATCH 242/375] Delete pending follow requests on user deletion --- lib/pleroma/following_relationship.ex | 6 ++++++ lib/pleroma/user.ex | 8 ++++++++ test/user_test.exs | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 3a3082e72..093b1f405 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -141,6 +141,12 @@ def following_query(%User{} = user) do |> where([r], r.state == ^:follow_accept) end + def outgoing_pending_follow_requests_query(%User{} = follower) do + __MODULE__ + |> where([r], r.follower_id == ^follower.id) + |> where([r], r.state == ^:follow_pending) + end + def following(%User{} = user) do following = following_query(user) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 72ee2d58e..c5c74d132 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1489,6 +1489,8 @@ def perform(:delete, %User{} = user) do delete_user_activities(user) + delete_outgoing_pending_follow_requests(user) + delete_or_deactivate(user) end @@ -1611,6 +1613,12 @@ defp delete_activity(%{data: %{"type" => type}} = activity, user) defp delete_activity(_activity, _user), do: "Doing nothing" + defp delete_outgoing_pending_follow_requests(user) do + user + |> FollowingRelationship.outgoing_pending_follow_requests_query() + |> Repo.delete_all() + end + def html_filter_policy(%User{no_rich_text: true}) do Pleroma.HTML.Scrubber.TwitterText end diff --git a/test/user_test.exs b/test/user_test.exs index 6b344158d..d68b4a58c 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1159,6 +1159,9 @@ test "it deactivates a user, all follow relationships and all activities", %{use follower = insert(:user) {:ok, follower} = User.follow(follower, user) + locked_user = insert(:user, name: "locked", locked: true) + {:ok, _} = User.follow(user, locked_user, :follow_pending) + object = insert(:note, user: user) activity = insert(:note_activity, user: user, note: object) @@ -1177,6 +1180,8 @@ test "it deactivates a user, all follow relationships and all activities", %{use refute User.following?(follower, user) assert %{deactivated: true} = User.get_by_id(user.id) + assert [] == User.get_follow_requests(locked_user) + user_activities = user.ap_id |> Activity.Queries.by_actor() From fe2a5d061463313f447b0557de05572fa3771728 Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 20:22:08 +0200 Subject: [PATCH 243/375] ChatController: Make last_read_id mandatory. --- .../web/api_spec/operations/chat_operation.ex | 3 +- .../controllers/chat_controller.ex | 7 +++-- .../controllers/chat_controller_test.exs | 28 +++---------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 45fbad311..cf299bfc2 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -340,10 +340,11 @@ def mark_as_read do title: "MarkAsReadRequest", description: "POST body for marking a number of chat messages as read", type: :object, + required: [:last_read_id], properties: %{ last_read_id: %Schema{ type: :string, - description: "The content of your message. Optional." + description: "The content of your message." } }, example: %{ diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 002b75082..b9949236c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -109,10 +109,13 @@ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ end end - def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do + def mark_as_read( + %{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn, + %{id: id} + ) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), {_n, _} <- - MessageReference.set_all_seen_for_chat(chat, conn.body_params[:last_read_id]) do + MessageReference.set_all_seen_for_chat(chat, last_read_id) do conn |> put_view(ChatView) |> render("show.json", chat: chat) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 63cd89c73..c2960956d 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -43,30 +43,10 @@ test "it marks one message as read", %{conn: conn, user: user} do describe "POST /api/v1/pleroma/chats/:id/read" do setup do: oauth_access(["write:chats"]) - test "it marks all messages in a chat as read", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") - {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - object = Object.normalize(create, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == true - - result = - conn - |> post("/api/v1/pleroma/chats/#{chat.id}/read") - |> json_response_and_validate_schema(200) - - assert result["unread"] == 0 - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == false - end - - test "it given a `last_read_id` ", %{conn: conn, user: user} do + test "given a `last_read_id`, it marks everything until then as read", %{ + conn: conn, + user: user + } do other_user = insert(:user) {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") From 1a2acce7c5927cd113ebcffd0acc7a5c547bbf0e Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 7 Jun 2020 20:23:17 +0200 Subject: [PATCH 244/375] Docs: Document new mandatory parameter. --- docs/API/chats.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 81ff57941..9eb581943 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -75,13 +75,13 @@ Returned data: ### Marking a chat as read -To set the `unread` count of a chat to 0, call +To mark a number of messages in a chat up to a certain message as read, you can use `POST /api/v1/pleroma/chats/:id/read` Parameters: -- last_read_id: Given this id, all chat messages until this one will be marked as read. This should always be used. +- last_read_id: Given this id, all chat messages until this one will be marked as read. Required. Returned data: From 89b85f65297ef4b8ce92eacb27c90e8f7c874f54 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 8 Jun 2020 11:09:53 +0200 Subject: [PATCH 245/375] ChatController: Remove nonsensical pagination. --- docs/API/chats.md | 2 +- .../web/pleroma_api/controllers/chat_controller.ex | 4 ++-- .../pleroma_api/controllers/chat_controller_test.exs | 11 ++--------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/API/chats.md b/docs/API/chats.md index 9eb581943..aa6119670 100644 --- a/docs/API/chats.md +++ b/docs/API/chats.md @@ -135,7 +135,7 @@ Returned data: ``` The recipient of messages that are sent to this chat is given by their AP ID. -The usual pagination options are implemented. +No pagination is implemented for now. ### Getting the messages for a Chat diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index b9949236c..e4760f53e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -140,7 +140,7 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = para end end - def index(%{assigns: %{user: %{id: user_id} = user}} = conn, params) do + def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do blocked_ap_ids = User.blocked_users_ap_ids(user) chats = @@ -149,7 +149,7 @@ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, params) do where: c.recipient not in ^blocked_ap_ids, order_by: [desc: c.updated_at] ) - |> Pagination.fetch_paginated(params |> stringify_keys) + |> Repo.all() conn |> put_view(ChatView) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index c2960956d..82e16741d 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -289,7 +289,7 @@ test "it does not return chats with users you blocked", %{conn: conn, user: user assert length(result) == 0 end - test "it paginates", %{conn: conn, user: user} do + test "it returns all chats", %{conn: conn, user: user} do Enum.each(1..30, fn _ -> recipient = insert(:user) {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) @@ -300,14 +300,7 @@ test "it paginates", %{conn: conn, user: user} do |> get("/api/v1/pleroma/chats") |> json_response_and_validate_schema(200) - assert length(result) == 20 - - result = - conn - |> get("/api/v1/pleroma/chats?max_id=#{List.last(result)["id"]}") - |> json_response_and_validate_schema(200) - - assert length(result) == 10 + assert length(result) == 30 end test "it return a list of chats the current user is participating in, in descending order of updates", From d44843e6774ed1c60d510a5307e0113e39569416 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 8 Jun 2020 17:56:34 +0400 Subject: [PATCH 246/375] Restrict ActivityExpirationPolicy to Notes only --- .../mrf/activity_expiration_policy.ex | 6 ++++- .../mrf/activity_expiration_policy_test.exs | 26 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index a9bdf3b69..8e47f1e02 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do @impl true def filter(activity) do activity = - if activity["type"] == "Create" && local?(activity) do + if note?(activity) and local?(activity) do maybe_add_expiration(activity) else activity @@ -25,6 +25,10 @@ defp local?(%{"id" => id}) do String.starts_with?(id, Pleroma.Web.Endpoint.url()) end + defp note?(activity) do + match?(%{"type" => "Create", "object" => %{"type" => "Note"}}, activity) + end + defp maybe_add_expiration(activity) do days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days) diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs index 0d3bcc457..8babf49e7 100644 --- a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -10,7 +10,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do test "adds `expires_at` property" do assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = - ActivityExpirationPolicy.filter(%{"id" => @id, "type" => "Create"}) + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "type" => "Create", + "object" => %{"type" => "Note"} + }) assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 end @@ -22,7 +26,8 @@ test "keeps existing `expires_at` if it less than the config setting" do ActivityExpirationPolicy.filter(%{ "id" => @id, "type" => "Create", - "expires_at" => expires_at + "expires_at" => expires_at, + "object" => %{"type" => "Note"} }) end @@ -33,7 +38,8 @@ test "overwrites existing `expires_at` if it greater than the config setting" do ActivityExpirationPolicy.filter(%{ "id" => @id, "type" => "Create", - "expires_at" => too_distant_future + "expires_at" => too_distant_future, + "object" => %{"type" => "Note"} }) assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 @@ -43,13 +49,14 @@ test "ignores remote activities" do assert {:ok, activity} = ActivityExpirationPolicy.filter(%{ "id" => "https://example.com/123", - "type" => "Create" + "type" => "Create", + "object" => %{"type" => "Note"} }) refute Map.has_key?(activity, "expires_at") end - test "ignores non-Create activities" do + test "ignores non-Create/Note activities" do assert {:ok, activity} = ActivityExpirationPolicy.filter(%{ "id" => "https://example.com/123", @@ -57,5 +64,14 @@ test "ignores non-Create activities" do }) refute Map.has_key?(activity, "expires_at") + + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "type" => "Create", + "object" => %{"type" => "Cofe"} + }) + + refute Map.has_key?(activity, "expires_at") end end From fe1cb56fdc52092a8af2895fae4c020679156674 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 8 Jun 2020 21:04:16 +0200 Subject: [PATCH 247/375] transmogrifier: MIME.valid?/1 for mediaType No issues with the rest of the network yet but this makes sure it will work once https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2429 is merged. --- lib/pleroma/web/activity_pub/transmogrifier.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index fda1c71df..543972ae9 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -221,9 +221,9 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm media_type = cond do - is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"] - is_binary(data["mediaType"]) -> data["mediaType"] - is_binary(data["mimeType"]) -> data["mimeType"] + is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"] + MIME.valid?(data["mediaType"]) -> data["mediaType"] + MIME.valid?(data["mimeType"]) -> data["mimeType"] true -> nil end From fc04a138d46c43860a2838d60fc8668112fdc1ec Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 8 Jun 2020 20:01:37 +0000 Subject: [PATCH 248/375] Apply suggestion to lib/pleroma/notification.ex --- lib/pleroma/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 3ac8737e2..3386a1933 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Notification do schema "notifications" do field(:seen, :boolean, default: false) # This is an enum type in the database. If you add a new notification type, - # remembert to add a migration to add it to the `notifications_type` enum + # remember to add a migration to add it to the `notifications_type` enum # as well. field(:type, :string) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) From e1bc37d11852684a5007a9550208944d899800ca Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 9 Jun 2020 09:20:55 +0200 Subject: [PATCH 249/375] MigrationHelper: Move notification backfilling to own module. --- .../notification_backfill.ex} | 2 +- .../20200602125218_backfill_notification_types.exs | 2 +- .../notification_backfill_test.exs} | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename lib/pleroma/{migration_helper.ex => migration_helper/notification_backfill.ex} (97%) rename test/{migration_helper_test.exs => migration_helper/notification_backfill_test.exs} (91%) diff --git a/lib/pleroma/migration_helper.ex b/lib/pleroma/migration_helper/notification_backfill.ex similarity index 97% rename from lib/pleroma/migration_helper.ex rename to lib/pleroma/migration_helper/notification_backfill.ex index a20d27a01..09647d12a 100644 --- a/lib/pleroma/migration_helper.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.MigrationHelper do +defmodule Pleroma.MigrationHelper.NotificationBackfill do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo diff --git a/priv/repo/migrations/20200602125218_backfill_notification_types.exs b/priv/repo/migrations/20200602125218_backfill_notification_types.exs index 58943fad0..996d721ee 100644 --- a/priv/repo/migrations/20200602125218_backfill_notification_types.exs +++ b/priv/repo/migrations/20200602125218_backfill_notification_types.exs @@ -2,7 +2,7 @@ defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do use Ecto.Migration def up do - Pleroma.MigrationHelper.fill_in_notification_types() + Pleroma.MigrationHelper.NotificationBackfill.fill_in_notification_types() end def down do diff --git a/test/migration_helper_test.exs b/test/migration_helper/notification_backfill_test.exs similarity index 91% rename from test/migration_helper_test.exs rename to test/migration_helper/notification_backfill_test.exs index 1c8173987..2a62a2b00 100644 --- a/test/migration_helper_test.exs +++ b/test/migration_helper/notification_backfill_test.exs @@ -2,11 +2,11 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.MigrationHelperTest do +defmodule Pleroma.MigrationHelper.NotificationBackfillTest do use Pleroma.DataCase alias Pleroma.Activity - alias Pleroma.MigrationHelper + alias Pleroma.MigrationHelper.NotificationBackfill alias Pleroma.Notification alias Pleroma.Repo alias Pleroma.Web.CommonAPI @@ -35,7 +35,7 @@ test "it fills in missing notification types" do assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) - MigrationHelper.fill_in_notification_types() + NotificationBackfill.fill_in_notification_types() assert %{type: "mention"} = Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) From 063e6b9841ec72c7e89339c54581d199fa31e675 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 9 Jun 2020 10:53:40 +0200 Subject: [PATCH 250/375] StatusController: Correctly paginate favorites. Favorites were paginating wrongly, because the pagination headers where using the id of the id of the `Create` activity, while the ordering was by the id of the `Like` activity. This isn't easy to notice in most cases, as they usually have a similar order because people tend to favorite posts as they come in. This commit adds a way to give different pagination ids to the pagination helper, so we can paginate correctly in cases like this. --- lib/pleroma/activity.ex | 4 ++ lib/pleroma/web/activity_pub/activity_pub.ex | 5 +- .../api_spec/operations/status_operation.ex | 3 +- lib/pleroma/web/controller_helper.ex | 56 +++++++++++-------- .../controllers/status_controller_test.exs | 43 ++++++++++++-- 5 files changed, 79 insertions(+), 32 deletions(-) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 6213d0eb7..f800447fd 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -41,6 +41,10 @@ defmodule Pleroma.Activity do field(:recipients, {:array, :string}, default: []) field(:thread_muted?, :boolean, virtual: true) + # A field that can be used if you need to join some kind of other + # id to order / paginate this field by + field(:pagination_id, :string, virtual: true) + # This is a fake relation, # do not use outside of with_preloaded_user_actor/with_joined_user_actor has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index eb73c95fe..cc883ccce 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1138,12 +1138,11 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do |> Activity.Queries.by_type("Like") |> Activity.with_joined_object() |> Object.with_joined_activity() - |> select([_like, object, activity], %{activity | object: object}) + |> select([like, object, activity], %{activity | object: object, pagination_id: like.id}) |> order_by([like, _, _], desc_nulls_last: like.id) |> Pagination.fetch_paginated( Map.merge(params, %{skip_order: true}), - pagination, - :object_activity + pagination ) end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index ca9db01e5..0b7fad793 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -333,7 +333,8 @@ def favourites_operation do %Operation{ tags: ["Statuses"], summary: "Favourited statuses", - description: "Statuses the user has favourited", + description: + "Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.", operationId: "StatusController.favourites", parameters: pagination_params(), security: [%{"oAuth" => ["read:favourites"]}], diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5d67d75b5..5e33e0810 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -57,35 +57,45 @@ def add_link_headers(conn, activities, extra_params) do end end + defp build_pagination_fields(conn, min_id, max_id, extra_params) do + params = + conn.params + |> Map.drop(Map.keys(conn.path_params)) + |> Map.merge(extra_params) + |> Map.drop(Pagination.page_keys() -- ["limit", "order"]) + + fields = %{ + "next" => current_url(conn, Map.put(params, :max_id, max_id)), + "prev" => current_url(conn, Map.put(params, :min_id, min_id)) + } + + # Generating an `id` without already present pagination keys would + # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` + # instead of the `q.id > ^min_id` and `q.id < ^max_id`. + # This is because we only have ids present inside of the page, while + # `min_id`, `since_id` and `max_id` requires to know one outside of it. + if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do + Map.put(fields, "id", current_url(conn, conn.params)) + else + fields + end + end + def get_pagination_fields(conn, activities, extra_params \\ %{}) do case List.last(activities) do - %{id: max_id} -> - params = - conn.params - |> Map.drop(Map.keys(conn.path_params)) - |> Map.merge(extra_params) - |> Map.drop(Pagination.page_keys() -- ["limit", "order"]) - - min_id = + %{pagination_id: max_id} when not is_nil(max_id) -> + %{pagination_id: min_id} = activities |> List.first() - |> Map.get(:id) - fields = %{ - "next" => current_url(conn, Map.put(params, :max_id, max_id)), - "prev" => current_url(conn, Map.put(params, :min_id, min_id)) - } + build_pagination_fields(conn, min_id, max_id, extra_params) - # Generating an `id` without already present pagination keys would - # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` - # instead of the `q.id > ^min_id` and `q.id < ^max_id`. - # This is because we only have ids present inside of the page, while - # `min_id`, `since_id` and `max_id` requires to know one outside of it. - if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do - Map.put(fields, "id", current_url(conn, conn.params)) - else - fields - end + %{id: max_id} -> + %{id: min_id} = + activities + |> List.first() + + build_pagination_fields(conn, min_id, max_id, extra_params) _ -> %{} diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 700c82e4f..648e6f2ce 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -1541,14 +1541,49 @@ test "context" do } = response end + test "favorites paginate correctly" do + %{user: user, conn: conn} = oauth_access(["read:favourites"]) + other_user = insert(:user) + {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"}) + + {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id) + {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id) + {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id) + + result = + conn + |> get("/api/v1/favourites?limit=1") + + assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200) + assert post_id == second_post.id + + # Using the header for pagination works correctly + [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") + [_, max_id] = Regex.run(~r/max_id=(.*)>;/, next) + + assert max_id == third_favorite.id + + result = + conn + |> get("/api/v1/favourites?max_id=#{max_id}") + + assert [%{"id" => first_post_id}, %{"id" => third_post_id}] = + json_response_and_validate_schema(result, 200) + + assert first_post_id == first_post.id + assert third_post_id == third_post.id + end + test "returns the favorites of a user" do %{user: user, conn: conn} = oauth_access(["read:favourites"]) other_user = insert(:user) {:ok, _} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, activity} = CommonAPI.post(other_user, %{status: "traps are happy"}) + {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"}) - {:ok, _} = CommonAPI.favorite(user, activity.id) + {:ok, last_like} = CommonAPI.favorite(user, activity.id) first_conn = get(conn, "/api/v1/favourites") @@ -1566,9 +1601,7 @@ test "returns the favorites of a user" do {:ok, _} = CommonAPI.favorite(user, second_activity.id) - last_like = status["id"] - - second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}") + second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}") assert [second_status] = json_response_and_validate_schema(second_conn, 200) assert second_status["id"] == to_string(second_activity.id) From 3dd1de61a78f9571a1d886411d70cd52584e084a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 8 Jun 2020 22:08:57 +0400 Subject: [PATCH 251/375] Add `url` field to AdminAPI.AccountView --- .../web/admin_api/views/account_view.ex | 3 +- .../controllers/admin_api_controller_test.exs | 66 ++++++++++++------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 120159527..e1e929632 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -76,7 +76,8 @@ def render("show.json", %{user: user}) do "local" => user.local, "roles" => User.roles(user), "tags" => user.tags || [], - "confirmation_pending" => user.confirmation_pending + "confirmation_pending" => user.confirmation_pending, + "url" => user.uri || user.ap_id } end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index bea810c4a..e3d3ccb8d 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -337,7 +337,8 @@ test "Show", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } assert expected == json_response(conn, 200) @@ -614,7 +615,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id }, %{ "deactivated" => user.deactivated, @@ -625,7 +627,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do "tags" => ["foo", "bar"], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -697,7 +700,8 @@ test "regular search", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -722,7 +726,8 @@ test "search by domain", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -747,7 +752,8 @@ test "search by full nickname", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -772,7 +778,8 @@ test "search by display name", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -797,7 +804,8 @@ test "search by email", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -822,7 +830,8 @@ test "regular search with page size", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -842,7 +851,8 @@ test "regular search with page size", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user2) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user2.ap_id } ] } @@ -874,7 +884,8 @@ test "only local users" do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -899,7 +910,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id }, %{ "deactivated" => admin.deactivated, @@ -910,7 +922,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id }, %{ "deactivated" => false, @@ -921,7 +934,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do "tags" => [], "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => old_admin.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -951,7 +965,8 @@ test "load only admins", %{conn: conn, admin: admin} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id }, %{ "deactivated" => false, @@ -962,7 +977,8 @@ test "load only admins", %{conn: conn, admin: admin} do "tags" => [], "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => second_admin.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -994,7 +1010,8 @@ test "load only moderators", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(moderator) |> MediaProxy.url(), "display_name" => HTML.strip_tags(moderator.name || moderator.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => moderator.ap_id } ] } @@ -1019,7 +1036,8 @@ test "load users with tags list", %{conn: conn} do "tags" => ["first"], "avatar" => User.avatar_url(user1) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user1.name || user1.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user1.ap_id }, %{ "deactivated" => false, @@ -1030,7 +1048,8 @@ test "load users with tags list", %{conn: conn} do "tags" => ["second"], "avatar" => User.avatar_url(user2) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user2.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -1069,7 +1088,8 @@ test "it works with multiple filters" do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -1093,7 +1113,8 @@ test "it omits relay user", %{admin: admin, conn: conn} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id } ] } @@ -1155,7 +1176,8 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admi "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } log_entry = Repo.one(ModerationLog) From c4f267b3bef90dcac21b7db2a91f86d3ba5dc7c2 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 08:02:26 +0000 Subject: [PATCH 252/375] Apply suggestion to lib/pleroma/web/controller_helper.ex --- lib/pleroma/web/controller_helper.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5e33e0810..6cb19d539 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -57,6 +57,7 @@ def add_link_headers(conn, activities, extra_params) do end end + @id_keys Pagination.page_keys() -- ["limit", "order"] defp build_pagination_fields(conn, min_id, max_id, extra_params) do params = conn.params From be7c322865b2b7aa1c8c25147cc598b6362ab187 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 08:02:35 +0000 Subject: [PATCH 253/375] Apply suggestion to lib/pleroma/web/controller_helper.ex --- lib/pleroma/web/controller_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 6cb19d539..b7971e940 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -63,7 +63,7 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do conn.params |> Map.drop(Map.keys(conn.path_params)) |> Map.merge(extra_params) - |> Map.drop(Pagination.page_keys() -- ["limit", "order"]) + |> Map.drop(@id_keys) fields = %{ "next" => current_url(conn, Map.put(params, :max_id, max_id)), From b4c50be9df701dc9faf0a25f776f631d2175c99f Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 08:12:29 +0000 Subject: [PATCH 254/375] Apply suggestion to lib/pleroma/web/controller_helper.ex --- lib/pleroma/web/controller_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b7971e940..ab6e6c61a 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -75,7 +75,7 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do # instead of the `q.id > ^min_id` and `q.id < ^max_id`. # This is because we only have ids present inside of the page, while # `min_id`, `since_id` and `max_id` requires to know one outside of it. - if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do + if Map.take(conn.params, @id_keys) != %{} do Map.put(fields, "id", current_url(conn, conn.params)) else fields From 86fec45f40dfa45cc89eddc6dcc7799e89d6f461 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 11:09:45 +0200 Subject: [PATCH 255/375] ControllerHelper: Fix wrong comparison. --- lib/pleroma/web/controller_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index ab6e6c61a..88f2cc6f1 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -75,7 +75,7 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do # instead of the `q.id > ^min_id` and `q.id < ^max_id`. # This is because we only have ids present inside of the page, while # `min_id`, `since_id` and `max_id` requires to know one outside of it. - if Map.take(conn.params, @id_keys) != %{} do + if Map.take(conn.params, @id_keys) != [] do Map.put(fields, "id", current_url(conn, conn.params)) else fields From 9e411372d0b7ae286941063956305c0a2eae46a6 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 12:10:09 +0200 Subject: [PATCH 256/375] ActivityPub: Don't show announces of your own objects in timeline. --- lib/pleroma/web/activity_pub/activity_pub.ex | 40 ++++++++++--------- .../controllers/timeline_controller.ex | 1 + test/web/activity_pub/activity_pub_test.exs | 24 +++++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index eb73c95fe..4182275bc 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -31,25 +31,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do require Logger require Pleroma.Constants - # For Announce activities, we filter the recipients based on following status for any actors - # that match actual users. See issue #164 for more information about why this is necessary. - defp get_recipients(%{"type" => "Announce"} = data) do - to = Map.get(data, "to", []) - cc = Map.get(data, "cc", []) - bcc = Map.get(data, "bcc", []) - actor = User.get_cached_by_ap_id(data["actor"]) - - recipients = - Enum.filter(Enum.concat([to, cc, bcc]), fn recipient -> - case User.get_cached_by_ap_id(recipient) do - nil -> true - user -> User.following?(user, actor) - end - end) - - {recipients, to, cc} - end - defp get_recipients(%{"type" => "Create"} = data) do to = Map.get(data, "to", []) cc = Map.get(data, "cc", []) @@ -702,6 +683,26 @@ defp user_activities_recipients(%{reading_user: reading_user}) do end end + defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do + raise "Can't use the child object without preloading!" + end + + defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do + from( + [activity, object] in query, + where: + fragment( + "?->>'type' != ? or ?->>'actor' != ?", + activity.data, + "Announce", + object.data, + ^actor + ) + ) + end + + defp restrict_announce_object_actor(query, _), do: query + defp restrict_since(query, %{since_id: ""}), do: query defp restrict_since(query, %{since_id: since_id}) do @@ -1113,6 +1114,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_pinned(opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_instance(opts) + |> restrict_announce_object_actor(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) |> exclude_invisible_actors(opts) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 9270ca267..4bdd46d7e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -48,6 +48,7 @@ def home(%{assigns: %{user: user}} = conn, params) do |> Map.put(:blocking_user, user) |> Map.put(:muting_user, user) |> Map.put(:reply_filtering_user, user) + |> Map.put(:announce_filtering_user, user) |> Map.put(:user, user) activities = diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 2f65dfc8e..e17cc4ab1 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1643,6 +1643,30 @@ test "home timeline with reply_visibility `self`", %{ assert Enum.all?(visible_ids, &(&1 in activities_ids)) end + + test "filtering out announces where the user is the actor of the announced message" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + User.follow(user, other_user) + + {:ok, post} = CommonAPI.post(user, %{status: "yo"}) + {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"}) + {:ok, _announce} = CommonAPI.repeat(post.id, other_user) + {:ok, _announce} = CommonAPI.repeat(post.id, third_user) + {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) + + params = %{ + type: ["Announce"], + announce_filtering_user: user + } + + [result] = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert result.id == announce.id + end end describe "replies filtering with private messages" do From 600e2ea07396489325e06dee3e8432288e0e13c2 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 12:15:56 +0200 Subject: [PATCH 257/375] ActivityPubTest: Make test easier to understand. --- test/web/activity_pub/activity_pub_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index e17cc4ab1..6cd3b8d1b 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1656,6 +1656,16 @@ test "filtering out announces where the user is the actor of the announced messa {:ok, _announce} = CommonAPI.repeat(post.id, third_user) {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) + params = %{ + type: ["Announce"] + } + + results = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(results) == 3 + params = %{ type: ["Announce"], announce_filtering_user: user From 570123ae21382c7e78b99442e3c025b0e66b8f6d Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 7 Jun 2020 18:21:11 +0200 Subject: [PATCH 258/375] Add test --- test/web/activity_pub/activity_pub_test.exs | 35 ++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 6cd3b8d1b..72d3f3dfa 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -574,7 +574,7 @@ test "doesn't return transitive interactions concerning blocked users" do refute Enum.member?(activities, activity_four) end - test "doesn't return announce activities concerning blocked users" do + test "doesn't return announce activities with blocked users in 'to'" do blocker = insert(:user) blockee = insert(:user) friend = insert(:user) @@ -596,6 +596,39 @@ test "doesn't return announce activities concerning blocked users" do refute Enum.member?(activities, activity_three.id) end + test "doesn't return announce activities with blocked users in 'cc'" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + assert object = Pleroma.Object.normalize(activity_two) + + data = %{ + "actor" => friend.ap_id, + "object" => object.data["id"], + "context" => object.data["context"], + "type" => "Announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [blockee.ap_id] + } + + assert {:ok, activity_three} = ActivityPub.insert(data) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => blocker}) + |> Enum.map(fn act -> act.id end) + + assert Enum.member?(activities, activity_one.id) + refute Enum.member?(activities, activity_two.id) + refute Enum.member?(activities, activity_three.id) + end + test "doesn't return activities from blocked domains" do domain = "dogwhistle.zone" domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) From 5d87405b51efe9f99fea669090a5914db22ca9ed Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 16:55:30 +0200 Subject: [PATCH 259/375] ActivityPubTest: Update test for atomized parameters. --- test/web/activity_pub/activity_pub_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 72d3f3dfa..b239b812f 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -621,7 +621,7 @@ test "doesn't return announce activities with blocked users in 'cc'" do assert {:ok, activity_three} = ActivityPub.insert(data) activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker}) + ActivityPub.fetch_activities([], %{blocking_user: blocker}) |> Enum.map(fn act -> act.id end) assert Enum.member?(activities, activity_one.id) From 99afc7f4e423997079aaee1287b9ffb28a851d8b Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 10 Jun 2020 20:09:16 +0300 Subject: [PATCH 260/375] HTTP security plug: add media proxy base url host to csp --- lib/pleroma/plugs/http_security_plug.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 6a339b32c..620408d0f 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -113,6 +113,10 @@ defp get_proxy_and_attachment_sources do add_source(acc, host) end) + media_proxy_base_url = + if Config.get([Pleroma.Upload, :base_url]), + do: URI.parse(Config.get([:media_proxy, :base_url])).host + upload_base_url = if Config.get([Pleroma.Upload, :base_url]), do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host @@ -122,6 +126,7 @@ defp get_proxy_and_attachment_sources do do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host [] + |> add_source(media_proxy_base_url) |> add_source(upload_base_url) |> add_source(s3_endpoint) |> add_source(media_proxy_whitelist) From 7c47f791a803aa5cee2f2f6931b8445d2c0551e5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 10 Jun 2020 13:02:08 -0500 Subject: [PATCH 261/375] Add command to reload emoji packs from cli for OTP users Not useful for source releases as we don't have a way to automate connecting to the running instance. --- docs/administration/CLI_tasks/emoji.md | 8 ++++++++ lib/mix/tasks/pleroma/emoji.ex | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/docs/administration/CLI_tasks/emoji.md b/docs/administration/CLI_tasks/emoji.md index 3d524a52b..ddcb7e62c 100644 --- a/docs/administration/CLI_tasks/emoji.md +++ b/docs/administration/CLI_tasks/emoji.md @@ -44,3 +44,11 @@ Currently, only .zip archives are recognized as remote pack files and packs are The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously. The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted). + +## Reload emoji packs + +```sh tab="OTP" +./bin/pleroma_ctl emoji reload +``` + +This command only works with OTP releases. diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 29a5fa99c..f4eaeac98 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -237,6 +237,12 @@ def run(["gen-pack" | args]) do end end + def run(["reload"]) do + start_pleroma() + Pleroma.Emoji.reload() + IO.puts("Emoji packs have been reloaded.") + end + defp fetch_and_decode(from) do with {:ok, json} <- fetch(from) do Jason.decode!(json) From 5e44e9d69871f2e5805a8dddcfce43ae713eb52d Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 10 Jun 2020 18:56:46 +0000 Subject: [PATCH 262/375] Apply suggestion to lib/pleroma/web/controller_helper.ex --- lib/pleroma/web/controller_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 88f2cc6f1..a5eb3e9e0 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -76,7 +76,7 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do # This is because we only have ids present inside of the page, while # `min_id`, `since_id` and `max_id` requires to know one outside of it. if Map.take(conn.params, @id_keys) != [] do - Map.put(fields, "id", current_url(conn, conn.params)) + Map.put(fields, "id", current_url(conn)) else fields end From b28cec4271c52d55f6e6cf8a1bcdb41efec3ef03 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 11 Jun 2020 16:05:14 +0300 Subject: [PATCH 263/375] [#1794] Fixes URI query handling for hashtags extraction in search. --- .../mastodon_api/controllers/search_controller.ex | 14 ++++++++++++++ .../controllers/search_controller_test.exs | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 8840fc19c..46bcf4228 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -124,6 +124,7 @@ defp resource_search(:v1, "hashtags", query, _options) do defp prepare_tags(query, add_joined_tag \\ true) do tags = query + |> preprocess_uri_query() |> String.split(~r/[^#\w]+/u, trim: true) |> Enum.uniq_by(&String.downcase/1) @@ -147,6 +148,19 @@ defp prepare_tags(query, add_joined_tag \\ true) do end end + # If `query` is a URI, returns last component of its path, otherwise returns `query` + defp preprocess_uri_query(query) do + if query =~ ~r/https?:\/\// do + query + |> URI.parse() + |> Map.get(:path) + |> String.split("/") + |> Enum.at(-1) + else + query + end + end + defp joined_tag(tags) do tags |> Enum.map(fn tag -> String.capitalize(tag) end) diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 84d46895e..0e025adca 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -111,6 +111,15 @@ test "constructs hashtags from search query", %{conn: conn} do %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} + ] end test "excludes a blocked users from search results", %{conn: conn} do From 1f35acce54ea6924a54b4fc387be3346a6f5551e Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 11 Jun 2020 17:57:31 +0400 Subject: [PATCH 264/375] Merge OGP parser with TwitterCard --- CHANGELOG.md | 1 + config/config.exs | 1 - config/description.exs | 2 - lib/pleroma/web/rich_media/parser.ex | 4 +- .../rich_media/parsers/meta_tags_parser.ex | 23 ++-- .../web/rich_media/parsers/oembed_parser.ex | 4 +- lib/pleroma/web/rich_media/parsers/ogp.ex | 3 +- .../web/rich_media/parsers/twitter_card.ex | 15 +- .../rich_media/parsers/twitter_card_test.exs | 130 ++++++++++-------- 9 files changed, 90 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf2210f5..575eb67b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Changed +- OGP rich media parser merged with TwitterCard
API Changes - **Breaking:** Emoji API: changed methods and renamed routes. diff --git a/config/config.exs b/config/config.exs index 9508ae077..cafa40820 100644 --- a/config/config.exs +++ b/config/config.exs @@ -385,7 +385,6 @@ ignore_tld: ["local", "localdomain", "lan"], parsers: [ Pleroma.Web.RichMedia.Parsers.TwitterCard, - Pleroma.Web.RichMedia.Parsers.OGP, Pleroma.Web.RichMedia.Parsers.OEmbed ], ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl] diff --git a/config/description.exs b/config/description.exs index 807c945e0..b993959d7 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2091,9 +2091,7 @@ description: "List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.", suggestions: [ - Pleroma.Web.RichMedia.Parsers.MetaTagsParser, Pleroma.Web.RichMedia.Parsers.OEmbed, - Pleroma.Web.RichMedia.Parsers.OGP, Pleroma.Web.RichMedia.Parsers.TwitterCard ] }, diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 40980def8..78e9048f3 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -105,8 +105,8 @@ defp parse_html(html), do: Floki.parse_document!(html) defp maybe_parse(html) do Enum.reduce_while(parsers(), %{}, fn parser, acc -> case parser.parse(html, acc) do - {:ok, data} -> {:halt, data} - {:error, _msg} -> {:cont, acc} + data when data != %{} -> {:halt, data} + _ -> {:cont, acc} end end) end diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index ae0f36702..c09b96eae 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -3,22 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do - def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do - meta_data = - html - |> get_elements(key_name, prefix) - |> Enum.reduce(data, fn el, acc -> - attributes = normalize_attributes(el, prefix, key_name, value_name) + def parse(data, html, prefix, key_name, value_name \\ "content") do + html + |> get_elements(key_name, prefix) + |> Enum.reduce(data, fn el, acc -> + attributes = normalize_attributes(el, prefix, key_name, value_name) - Map.merge(acc, attributes) - end) - |> maybe_put_title(html) - - if Enum.empty?(meta_data) do - {:error, error_message} - else - {:ok, meta_data} - end + Map.merge(acc, attributes) + end) + |> maybe_put_title(html) end defp get_elements(html, key_name, prefix) do diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex index 8f32bf91b..5d87a90e9 100644 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -7,9 +7,9 @@ def parse(html, _data) do with elements = [_ | _] <- get_discovery_data(html), {:ok, oembed_url} <- get_oembed_url(elements), {:ok, oembed_data} <- get_oembed_data(oembed_url) do - {:ok, oembed_data} + oembed_data else - _e -> {:error, "No OEmbed data found"} + _e -> %{} end end diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex index 3e9012588..5eebe42f7 100644 --- a/lib/pleroma/web/rich_media/parsers/ogp.ex +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -5,10 +5,9 @@ defmodule Pleroma.Web.RichMedia.Parsers.OGP do def parse(html, data) do Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( - html, data, + html, "og", - "No OGP metadata found", "property" ) end diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex index 09d4b526e..4a04865d2 100644 --- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex +++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex @@ -5,18 +5,11 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do alias Pleroma.Web.RichMedia.Parsers.MetaTagsParser - @spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()} + @spec parse(list(), map()) :: map() def parse(html, data) do data - |> parse_name_attrs(html) - |> parse_property_attrs(html) - end - - defp parse_name_attrs(data, html) do - MetaTagsParser.parse(html, data, "twitter", %{}, "name") - end - - defp parse_property_attrs({_, data}, html) do - MetaTagsParser.parse(html, data, "twitter", "No twitter card metadata found", "property") + |> MetaTagsParser.parse(html, "og", "property") + |> MetaTagsParser.parse(html, "twitter", "name") + |> MetaTagsParser.parse(html, "twitter", "property") end end diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs index 87c767c15..3ccf26651 100644 --- a/test/web/rich_media/parsers/twitter_card_test.exs +++ b/test/web/rich_media/parsers/twitter_card_test.exs @@ -7,8 +7,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do alias Pleroma.Web.RichMedia.Parsers.TwitterCard test "returns error when html not contains twitter card" do - assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == - {:error, "No twitter card metadata found"} + assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == %{} end test "parses twitter card with only name attributes" do @@ -17,15 +16,21 @@ test "parses twitter card with only name attributes" do |> Floki.parse_document!() assert TwitterCard.parse(html, %{}) == - {:ok, - %{ - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - site: nil, - title: - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times" - }} + %{ + "app:id:googleplay": "com.nytimes.android", + "app:name:googleplay": "NYTimes", + "app:url:googleplay": "nytimes://reader/id/100000006583622", + site: nil, + description: + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + image: + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", + type: "article", + url: + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database." + } end test "parses twitter card with only property attributes" do @@ -34,19 +39,19 @@ test "parses twitter card with only property attributes" do |> Floki.parse_document!() assert TwitterCard.parse(html, %{}) == - {:ok, - %{ - card: "summary_large_image", - description: - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt": "", - title: - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - url: - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" - }} + %{ + card: "summary_large_image", + description: + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + image: + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt": "", + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + url: + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + type: "article" + } end test "parses twitter card with name & property attributes" do @@ -55,23 +60,23 @@ test "parses twitter card with name & property attributes" do |> Floki.parse_document!() assert TwitterCard.parse(html, %{}) == - {:ok, - %{ - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - card: "summary_large_image", - description: - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt": "", - site: nil, - title: - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - url: - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" - }} + %{ + "app:id:googleplay": "com.nytimes.android", + "app:name:googleplay": "NYTimes", + "app:url:googleplay": "nytimes://reader/id/100000006583622", + card: "summary_large_image", + description: + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + image: + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt": "", + site: nil, + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + url: + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + type: "article" + } end test "respect only first title tag on the page" do @@ -84,14 +89,17 @@ test "respect only first title tag on the page" do File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!() assert TwitterCard.parse(html, %{}) == - {:ok, - %{ - site: "@atlasobscura", - title: - "The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura", - card: "summary_large_image", - image: image_path - }} + %{ + site: "@atlasobscura", + title: "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", + card: "summary_large_image", + image: image_path, + description: + "She's the only woman veteran honored with a monument at West Point. But where was she buried?", + site_name: "Atlas Obscura", + type: "article", + url: "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" + } end test "takes first founded title in html head if there is html markup error" do @@ -100,14 +108,20 @@ test "takes first founded title in html head if there is html markup error" do |> Floki.parse_document!() assert TwitterCard.parse(html, %{}) == - {:ok, - %{ - site: nil, - title: - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times", - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622" - }} + %{ + site: nil, + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "app:id:googleplay": "com.nytimes.android", + "app:name:googleplay": "NYTimes", + "app:url:googleplay": "nytimes://reader/id/100000006583622", + description: + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + image: + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", + type: "article", + url: + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" + } end end From 7f7a1a467677471e0e1ec688e4eca9ba759d976a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 11 Jun 2020 11:05:22 -0500 Subject: [PATCH 265/375] Check for media proxy base_url, not Upload base_url --- lib/pleroma/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 620408d0f..1420a9611 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -114,7 +114,7 @@ defp get_proxy_and_attachment_sources do end) media_proxy_base_url = - if Config.get([Pleroma.Upload, :base_url]), + if Config.get([:media_proxy, :base_url]), do: URI.parse(Config.get([:media_proxy, :base_url])).host upload_base_url = From 2419776e192316cefbdbe607306c9b92ec558319 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 11 Jun 2020 23:11:46 +0400 Subject: [PATCH 266/375] Deprecate Pleroma.Web.RichMedia.Parsers.OGP --- lib/pleroma/web/rich_media/parsers/ogp.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex index 5eebe42f7..363815f81 100644 --- a/lib/pleroma/web/rich_media/parsers/ogp.ex +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parsers.OGP do + @deprecated "OGP parser is deprecated. Use TwitterCard instead." def parse(html, data) do Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( data, From 40970f6bb94760d19cc1d3201405df5bb32f5083 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 11 Jun 2020 22:54:39 +0200 Subject: [PATCH 267/375] New mix task: pleroma.user reset_mfa --- docs/administration/CLI_tasks/user.md | 10 +++++++++ lib/mix/tasks/pleroma/user.ex | 12 +++++++++++ test/tasks/user_test.exs | 30 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index afeb8d52f..1e6f4a8b4 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -135,6 +135,16 @@ mix pleroma.user reset_password ``` +## Disable Multi Factor Authentication (MFA/2FA) for a user +```sh tab="OTP" + ./bin/pleroma_ctl user reset_mfa +``` + +```sh tab="From Source" +mix pleroma.user reset_mfa +``` + + ## Set the value of the given user's settings ```sh tab="OTP" ./bin/pleroma_ctl user set [option ...] diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 3635c02bc..bca7e87bf 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -144,6 +144,18 @@ def run(["reset_password", nickname]) do end end + def run(["reset_mfa", nickname]) do + start_pleroma() + + with %User{local: true} = user <- User.get_cached_by_nickname(nickname), + {:ok, _token} <- Pleroma.MFA.disable(user) do + shell_info("Multi-Factor Authentication disabled for #{user.nickname}") + else + _ -> + shell_error("No local user #{nickname}") + end + end + def run(["deactivate", nickname]) do start_pleroma() diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index b55aa1cdb..9220d23fc 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -4,6 +4,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do alias Pleroma.Activity + alias Pleroma.MFA alias Pleroma.Object alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers @@ -278,6 +279,35 @@ test "no user to reset password" do end end + describe "running reset_mfa" do + test "disables MFA" do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} + } + ) + + Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message == "Multi-Factor Authentication disabled for #{user.nickname}" + + assert %{enabled: false, totp: false} == + user.nickname + |> User.get_cached_by_nickname() + |> MFA.mfa_settings() + end + + test "no user to reset MFA" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + describe "running invite" do test "invite token is generated" do assert capture_io(fn -> From 122328b93a708e396b5c0cd1930a4b759e7b7db6 Mon Sep 17 00:00:00 2001 From: normandy Date: Fri, 12 Jun 2020 01:41:09 +0000 Subject: [PATCH 268/375] Update pleroma.nginx to support TLSv1.3 Based on SSL config from https://ssl-config.mozilla.org/ --- installation/pleroma.nginx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index 688be3e71..d301ca615 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -37,18 +37,17 @@ server { listen 443 ssl http2; listen [::]:443 ssl http2; - ssl_session_timeout 5m; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem; ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; - # Add TLSv1.0 to support older devices - ssl_protocols TLSv1.2; - # Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.) - # ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; + ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - ssl_prefer_server_ciphers on; + ssl_prefer_server_ciphers off; # In case of an old server with an OpenSSL version of 1.0.2 or below, # leave only prime256v1 or comment out the following line. ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; From 21880970660906d8072dc501e6a8b25fb4a4b0c7 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 12 Jun 2020 14:25:41 +0300 Subject: [PATCH 269/375] [#1794] Fixes URI query handling for hashtags extraction in search. --- .../controllers/search_controller.ex | 1 + .../controllers/search_controller_test.exs | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 46bcf4228..3be0ca095 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -152,6 +152,7 @@ defp prepare_tags(query, add_joined_tag \\ true) do defp preprocess_uri_query(query) do if query =~ ~r/https?:\/\// do query + |> String.trim_trailing("/") |> URI.parse() |> Map.get(:path) |> String.split("/") diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 0e025adca..c605957b1 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -120,6 +120,35 @@ test "constructs hashtags from search query", %{conn: conn} do assert results["hashtags"] == [ %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} ] + + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{ + q: + "https://www.washingtonpost.com/sports/2020/06/10/" <> + "nascar-ban-display-confederate-flag-all-events-properties/" + }) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"}, + %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"}, + %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"}, + %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"}, + %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"}, + %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"}, + %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"}, + %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"}, + %{ + "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", + "url" => + "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" + } + ] end test "excludes a blocked users from search results", %{conn: conn} do From f9dcf15ecb684b4b802d731a216448c76913d462 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Fri, 12 Jun 2020 14:49:54 +0300 Subject: [PATCH 270/375] added admin api for MediaProxy cache invalidation --- .../media_proxy_cache_controller.ex | 38 ++++++ .../admin_api/views/media_proxy_cache_view.ex | 11 ++ .../admin/media_proxy_cache_operation.ex | 109 ++++++++++++++++++ lib/pleroma/web/router.ex | 4 + .../media_proxy_cache_controller_test.exs | 66 +++++++++++ 5 files changed, 228 insertions(+) create mode 100644 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex create mode 100644 lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex create mode 100644 test/web/admin_api/controllers/media_proxy_cache_controller_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex new file mode 100644 index 000000000..7b28f7c72 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ApiSpec.Admin, as: Spec + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["read:media_proxy_caches"], admin: true} when action in [:index] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["write:media_proxy_caches"], admin: true} when action in [:purge, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation + + def index(%{assigns: %{user: _}} = conn, _) do + render(conn, "index.json", urls: []) + end + + def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do + render(conn, "index.json", urls: urls) + end + + def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: _ban}} = conn, _) do + render(conn, "index.json", urls: urls) + end +end diff --git a/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex new file mode 100644 index 000000000..c97400beb --- /dev/null +++ b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex @@ -0,0 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.MediaProxyCacheView do + use Pleroma.Web, :view + + def render("index.json", %{urls: urls}) do + %{urls: urls} + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex new file mode 100644 index 000000000..0358cfbad --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex @@ -0,0 +1,109 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "MediaProxyCache"], + summary: "Fetch a paginated list of all banned MediaProxy URLs in Cachex", + operationId: "AdminAPI.MediaProxyCacheController.index", + security: [%{"oAuth" => ["read:media_proxy_caches"]}], + parameters: [ + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of statuses to return" + ) + ], + responses: %{ + 200 => success_response() + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "MediaProxyCache"], + summary: "Remove a banned MediaProxy URL from Cachex", + operationId: "AdminAPI.MediaProxyCacheController.delete", + security: [%{"oAuth" => ["write:media_proxy_caches"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + required: [:urls], + properties: %{ + urls: %Schema{type: :array, items: %Schema{type: :string, format: :uri}} + } + }, + required: true + ), + responses: %{ + 200 => success_response(), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def purge_operation do + %Operation{ + tags: ["Admin", "MediaProxyCache"], + summary: "Purge and optionally ban a MediaProxy URL", + operationId: "AdminAPI.MediaProxyCacheController.purge", + security: [%{"oAuth" => ["write:media_proxy_caches"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + required: [:urls], + properties: %{ + urls: %Schema{type: :array, items: %Schema{type: :string, format: :uri}}, + ban: %Schema{type: :boolean, default: true} + } + }, + required: true + ), + responses: %{ + 200 => success_response(), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp success_response do + Operation.response("Array of banned MediaProxy URLs in Cachex", "application/json", %Schema{ + type: :object, + properties: %{ + urls: %Schema{ + type: :array, + items: %Schema{ + type: :string, + format: :uri, + description: "MediaProxy URLs" + } + } + } + }) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 57570b672..eda74a171 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -209,6 +209,10 @@ defmodule Pleroma.Web.Router do post("/oauth_app", OAuthAppController, :create) patch("/oauth_app/:id", OAuthAppController, :update) delete("/oauth_app/:id", OAuthAppController, :delete) + + get("/media_proxy_caches", MediaProxyCacheController, :index) + post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) + post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs new file mode 100644 index 000000000..1b1d6bc36 --- /dev/null +++ b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/media_proxy_caches" do + test "shows banned MediaProxy URLs", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches") + |> json_response_and_validate_schema(200) + + assert response["urls"] == [] + end + end + + describe "DELETE /api/pleroma/admin/media_proxy_caches/delete" do + test "deleted MediaProxy URLs from banned", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/delete", %{ + urls: ["http://example.com/media/a688346.jpg", "http://example.com/media/fb1f4d.jpg"] + }) + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + end + end + + describe "PURGE /api/pleroma/admin/media_proxy_caches/purge" do + test "perform invalidates cache of MediaProxy", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/purge", %{ + urls: ["http://example.com/media/a688346.jpg", "http://example.com/media/fb1f4d.jpg"] + }) + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + end + end +end From c2048f75cd09696e30b443423cae4ba6ef3e593b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 12 Jun 2020 08:42:23 -0500 Subject: [PATCH 271/375] Add changelog entry for emoji pack reload command --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf2210f5..b19cae8b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add support for filtering replies in public and home timelines - Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoint for status view. +- OTP: Add command to reload emoji packs
### Fixed From e505e59d9c43db286ccf7fe70da2fa974ae3d700 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 12 Jun 2020 08:51:11 -0500 Subject: [PATCH 272/375] Document new mix task feature to reset mfa --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf2210f5..c23beec9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit). - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required. - Mix task to create trusted OAuth App. +- Mix task to reset MFA for user accounts - Notifications: Added `follow_request` notification type. - Added `:reject_deletes` group to SimplePolicy - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances From 4655407451c8dd05b6024f607e598359047efce2 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 12 Jun 2020 14:03:33 +0000 Subject: [PATCH 273/375] Apply suggestion to config/description.exs --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index 086a28ace..add1601e2 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1476,7 +1476,7 @@ key: :mrf_activity_expiration, label: "MRF Activity Expiration Policy", type: :group, - description: "Adds expiration to all local Create activities", + description: "Adds expiration to all local Create Note activities", children: [ %{ key: :days, From 09d31d24de568aac06fe203beeb8bb2a9de8f602 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 12 Jun 2020 18:37:32 +0400 Subject: [PATCH 274/375] Return an empty map from Pleroma.Web.RichMedia.Parsers.OGP.parse/2 --- lib/pleroma/web/rich_media/parsers/ogp.ex | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex index 363815f81..b3b3b059c 100644 --- a/lib/pleroma/web/rich_media/parsers/ogp.ex +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -4,12 +4,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.OGP do @deprecated "OGP parser is deprecated. Use TwitterCard instead." - def parse(html, data) do - Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( - data, - html, - "og", - "property" - ) + def parse(_html, _data) do + %{} end end From 520367d6fd8a268e0bc8c145a46aca46a62e8b66 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 9 Jun 2020 21:49:24 +0400 Subject: [PATCH 275/375] Fix atom leak in Rich Media Parser --- .../web/mastodon_api/views/status_view.ex | 14 ++-- lib/pleroma/web/rich_media/helpers.ex | 6 +- lib/pleroma/web/rich_media/parser.ex | 12 +-- .../rich_media/parsers/meta_tags_parser.ex | 8 +- .../web/rich_media/parsers/oembed_parser.ex | 18 ++--- test/web/rich_media/parser_test.exs | 75 ++++++++++--------- .../rich_media/parsers/twitter_card_test.exs | 60 +++++++-------- 7 files changed, 91 insertions(+), 102 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 8e3715093..2c49bedb3 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -377,8 +377,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do page_url_data = URI.parse(page_url) page_url_data = - if rich_media[:url] != nil do - URI.merge(page_url_data, URI.parse(rich_media[:url])) + if is_binary(rich_media["url"]) do + URI.merge(page_url_data, URI.parse(rich_media["url"])) else page_url_data end @@ -386,11 +386,9 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do page_url = page_url_data |> to_string image_url = - if rich_media[:image] != nil do - URI.merge(page_url_data, URI.parse(rich_media[:image])) + if is_binary(rich_media["image"]) do + URI.merge(page_url_data, URI.parse(rich_media["image"])) |> to_string - else - nil end %{ @@ -399,8 +397,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do provider_url: page_url_data.scheme <> "://" <> page_url_data.host, url: page_url, image: image_url |> MediaProxy.url(), - title: rich_media[:title] || "", - description: rich_media[:description] || "", + title: rich_media["title"] || "", + description: rich_media["description"] || "", pleroma: %{ opengraph: rich_media } diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 9d3d7f978..1729141e9 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do alias Pleroma.Object alias Pleroma.Web.RichMedia.Parser - @spec validate_page_url(any()) :: :ok | :error + @spec validate_page_url(URI.t() | binary()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld] @@ -18,8 +18,8 @@ defp validate_page_url(page_url) when is_binary(page_url) do |> parse_uri(page_url) end - defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority}) - when scheme == "https" and not is_nil(authority) do + defp validate_page_url(%URI{host: host, scheme: "https", authority: authority}) + when is_binary(authority) do cond do host in Config.get([:rich_media, :ignore_hosts], []) -> :error diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 40980def8..d9b5068b1 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -91,7 +91,7 @@ defp parse_url(url) do html |> parse_html() |> maybe_parse() - |> Map.put(:url, url) + |> Map.put("url", url) |> clean_parsed_data() |> check_parsed_data() rescue @@ -111,8 +111,8 @@ defp maybe_parse(html) do end) end - defp check_parsed_data(%{title: title} = data) - when is_binary(title) and byte_size(title) > 0 do + defp check_parsed_data(%{"title" => title} = data) + when is_binary(title) and title != "" do {:ok, data} end @@ -123,11 +123,7 @@ defp check_parsed_data(data) do defp clean_parsed_data(data) do data |> Enum.reject(fn {key, val} -> - with {:ok, _} <- Jason.encode(%{key => val}) do - false - else - _ -> true - end + not match?({:ok, _}, Jason.encode(%{key => val})) end) |> Map.new() end diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index ae0f36702..2762b5902 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -29,19 +29,19 @@ defp normalize_attributes(html_node, prefix, key_name, value_name) do {_tag, attributes, _children} = html_node data = - Enum.into(attributes, %{}, fn {name, value} -> + Map.new(attributes, fn {name, value} -> {name, String.trim_leading(value, "#{prefix}:")} end) - %{String.to_atom(data[key_name]) => data[value_name]} + %{data[key_name] => data[value_name]} end - defp maybe_put_title(%{title: _} = meta, _), do: meta + defp maybe_put_title(%{"title" => _} = meta, _), do: meta defp maybe_put_title(meta, html) when meta != %{} do case get_page_title(html) do "" -> meta - title -> Map.put_new(meta, :title, title) + title -> Map.put_new(meta, "title", title) end end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex index 8f32bf91b..db8ccf15d 100644 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do def parse(html, _data) do with elements = [_ | _] <- get_discovery_data(html), - {:ok, oembed_url} <- get_oembed_url(elements), + oembed_url when is_binary(oembed_url) <- get_oembed_url(elements), {:ok, oembed_data} <- get_oembed_data(oembed_url) do {:ok, oembed_data} else @@ -17,19 +17,13 @@ defp get_discovery_data(html) do html |> Floki.find("link[type='application/json+oembed']") end - defp get_oembed_url(nodes) do - {"link", attributes, _children} = nodes |> hd() - - {:ok, Enum.into(attributes, %{})["href"]} + defp get_oembed_url([{"link", attributes, _children} | _]) do + Enum.find_value(attributes, fn {k, v} -> if k == "href", do: v end) end defp get_oembed_data(url) do - {:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media]) - - {:ok, data} = Jason.decode(json) - - data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) - - {:ok, data} + with {:ok, %Tesla.Env{body: json}} <- Pleroma.HTTP.get(url, [], adapter: [pool: :media]) do + Jason.decode(json) + end end end diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs index e54a13bc8..420a612c6 100644 --- a/test/web/rich_media/parser_test.exs +++ b/test/web/rich_media/parser_test.exs @@ -60,19 +60,19 @@ test "returns error when no metadata present" do test "doesn't just add a title" do assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/non-ogp") == {:error, - "Found metadata was invalid or incomplete: %{url: \"http://example.com/non-ogp\"}"} + "Found metadata was invalid or incomplete: %{\"url\" => \"http://example.com/non-ogp\"}"} end test "parses ogp" do assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp") == {:ok, %{ - image: "http://ia.media-imdb.com/images/rock.jpg", - title: "The Rock", - description: + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock", + "description" => "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - type: "video.movie", - url: "http://example.com/ogp" + "type" => "video.movie", + "url" => "http://example.com/ogp" }} end @@ -80,12 +80,12 @@ test "falls back to when ogp:title is missing" do assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp-missing-title") == {:ok, %{ - image: "http://ia.media-imdb.com/images/rock.jpg", - title: "The Rock (1996)", - description: + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock (1996)", + "description" => "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - type: "video.movie", - url: "http://example.com/ogp-missing-title" + "type" => "video.movie", + "url" => "http://example.com/ogp-missing-title" }} end @@ -93,12 +93,12 @@ test "parses twitter card" do assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") == {:ok, %{ - card: "summary", - site: "@flickr", - image: "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", - title: "Small Island Developing States Photo Submission", - description: "View the album on Flickr.", - url: "http://example.com/twitter-card" + "card" => "summary", + "site" => "@flickr", + "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", + "title" => "Small Island Developing States Photo Submission", + "description" => "View the album on Flickr.", + "url" => "http://example.com/twitter-card" }} end @@ -106,27 +106,28 @@ test "parses OEmbed" do assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") == {:ok, %{ - author_name: "‮‭‬bees‬", - author_url: "https://www.flickr.com/photos/bees/", - cache_age: 3600, - flickr_type: "photo", - height: "768", - html: + "author_name" => "‮‭‬bees‬", + "author_url" => "https://www.flickr.com/photos/bees/", + "cache_age" => 3600, + "flickr_type" => "photo", + "height" => "768", + "html" => "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by ‮‭‬bees‬, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", - license: "All Rights Reserved", - license_id: 0, - provider_name: "Flickr", - provider_url: "https://www.flickr.com/", - thumbnail_height: 150, - thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", - thumbnail_width: 150, - title: "Bacon Lollys", - type: "photo", - url: "http://example.com/oembed", - version: "1.0", - web_page: "https://www.flickr.com/photos/bees/2362225867/", - web_page_short_url: "https://flic.kr/p/4AK2sc", - width: "1024" + "license" => "All Rights Reserved", + "license_id" => 0, + "provider_name" => "Flickr", + "provider_url" => "https://www.flickr.com/", + "thumbnail_height" => 150, + "thumbnail_url" => + "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", + "thumbnail_width" => 150, + "title" => "Bacon Lollys", + "type" => "photo", + "url" => "http://example.com/oembed", + "version" => "1.0", + "web_page" => "https://www.flickr.com/photos/bees/2362225867/", + "web_page_short_url" => "https://flic.kr/p/4AK2sc", + "width" => "1024" }} end diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs index 87c767c15..847623535 100644 --- a/test/web/rich_media/parsers/twitter_card_test.exs +++ b/test/web/rich_media/parsers/twitter_card_test.exs @@ -19,11 +19,11 @@ test "parses twitter card with only name attributes" do assert TwitterCard.parse(html, %{}) == {:ok, %{ - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - site: nil, - title: + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "site" => nil, + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times" }} end @@ -36,15 +36,15 @@ test "parses twitter card with only property attributes" do assert TwitterCard.parse(html, %{}) == {:ok, %{ - card: "summary_large_image", - description: + "card" => "summary_large_image", + "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: + "image" => "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt": "", - title: + "image:alt" => "", + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - url: + "url" => "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" }} end @@ -57,19 +57,19 @@ test "parses twitter card with name & property attributes" do assert TwitterCard.parse(html, %{}) == {:ok, %{ - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - card: "summary_large_image", - description: + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "card" => "summary_large_image", + "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: + "image" => "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt": "", - site: nil, - title: + "image:alt" => "", + "site" => nil, + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - url: + "url" => "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" }} end @@ -86,11 +86,11 @@ test "respect only first title tag on the page" do assert TwitterCard.parse(html, %{}) == {:ok, %{ - site: "@atlasobscura", - title: + "site" => "@atlasobscura", + "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura", - card: "summary_large_image", - image: image_path + "card" => "summary_large_image", + "image" => image_path }} end @@ -102,12 +102,12 @@ test "takes first founded title in html head if there is html markup error" do assert TwitterCard.parse(html, %{}) == {:ok, %{ - site: nil, - title: + "site" => nil, + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times", - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622" + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622" }} end end From cb7be6eef252216d7ba5d5f72c8005d66b04986c Mon Sep 17 00:00:00 2001 From: href <href@random.sh> Date: Wed, 10 Jun 2020 17:34:23 +0200 Subject: [PATCH 276/375] Remove use of atoms in MRF.UserAllowListPolicy --- config/description.exs | 6 ++--- docs/configuration/cheatsheet.md | 5 ++-- lib/pleroma/config/deprecation_warnings.ex | 25 ++++++++++++++++++- .../mrf/user_allow_list_policy.ex | 2 +- .../mrf/user_allowlist_policy_test.exs | 6 ++--- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/config/description.exs b/config/description.exs index add1601e2..2f1eaf5f2 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1623,14 +1623,12 @@ # %{ # group: :pleroma, # key: :mrf_user_allowlist, - # type: :group, + # type: :map, # description: # "The keys in this section are the domain names that the policy should apply to." <> # " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID", - # children: [ - # ["example.org": ["https://example.org/users/admin"]], # suggestions: [ - # ["example.org": ["https://example.org/users/admin"]] + # %{"example.org" => ["https://example.org/users/admin"]} # ] # ] # }, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 456762151..fad67fc4d 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -138,8 +138,9 @@ their ActivityPub ID. An example: ```elixir -config :pleroma, :mrf_user_allowlist, - "example.org": ["https://example.org/users/admin"] +config :pleroma, :mrf_user_allowlist, %{ + "example.org" => ["https://example.org/users/admin"] +} ``` #### :mrf_object_age diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index c39a8984b..b68ded01f 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -4,9 +4,10 @@ defmodule Pleroma.Config.DeprecationWarnings do require Logger + alias Pleroma.Config def check_hellthread_threshold do - if Pleroma.Config.get([:mrf_hellthread, :threshold]) do + if Config.get([:mrf_hellthread, :threshold]) do Logger.warn(""" !!!DEPRECATION WARNING!!! You are using the old configuration mechanism for the hellthread filter. Please check config.md. @@ -14,7 +15,29 @@ def check_hellthread_threshold do end end + def mrf_user_allowlist do + config = Config.get(:mrf_user_allowlist) + + if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do + rewritten = + Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc -> + Map.put(acc, to_string(k), v) + end) + + Config.put(:mrf_user_allowlist, rewritten) + + Logger.error(""" + !!!DEPRECATION WARNING!!! + As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format. + Pleroma 2.1 will remove support for the old format. Please change your configuration to match this: + + config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)} + """) + end + end + def warn do check_hellthread_threshold() + mrf_user_allowlist() end end diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex index a927a4ed8..651aed70f 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex @@ -24,7 +24,7 @@ def filter(%{"actor" => actor} = object) do allow_list = Config.get( - [:mrf_user_allowlist, String.to_atom(actor_info.host)], + [:mrf_user_allowlist, actor_info.host], [] ) 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 724bae058..ba1b69658 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 - setup do: clear_config([:mrf_user_allowlist, :localhost]) + setup do: clear_config(:mrf_user_allowlist) test "pass filter if allow list is empty" do actor = insert(:user) @@ -17,14 +17,14 @@ test "pass filter if allow list is empty" do test "pass filter if allow list isn't empty and user in allow list" do actor = insert(:user) - Pleroma.Config.put([:mrf_user_allowlist, :localhost], [actor.ap_id, "test-ap-id"]) + Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]}) message = %{"actor" => actor.ap_id} assert UserAllowListPolicy.filter(message) == {:ok, message} end test "rejected if allow list isn't empty and user not in allow list" do actor = insert(:user) - Pleroma.Config.put([:mrf_user_allowlist, :localhost], ["test-ap-id"]) + Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]}) message = %{"actor" => actor.ap_id} assert UserAllowListPolicy.filter(message) == {:reject, nil} end From 4b865bba107b0db1de886cefd14227454cbece1e Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Sat, 13 Jun 2020 10:37:15 +0000 Subject: [PATCH 277/375] Apply suggestion to lib/pleroma/web/controller_helper.ex --- lib/pleroma/web/controller_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index a5eb3e9e0..d5e9c33f5 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -75,7 +75,7 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do # instead of the `q.id > ^min_id` and `q.id < ^max_id`. # This is because we only have ids present inside of the page, while # `min_id`, `since_id` and `max_id` requires to know one outside of it. - if Map.take(conn.params, @id_keys) != [] do + if Map.take(conn.params, @id_keys) != %{} do Map.put(fields, "id", current_url(conn)) else fields From 1d625c29a09cf7c0fb415d5606a91315902efaad Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Sat, 13 Jun 2020 13:12:43 +0200 Subject: [PATCH 278/375] ControllerHelper: Always return id field. --- lib/pleroma/web/controller_helper.ex | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index d5e9c33f5..69946fb81 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -65,21 +65,11 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do |> Map.merge(extra_params) |> Map.drop(@id_keys) - fields = %{ + %{ "next" => current_url(conn, Map.put(params, :max_id, max_id)), - "prev" => current_url(conn, Map.put(params, :min_id, min_id)) + "prev" => current_url(conn, Map.put(params, :min_id, min_id)), + "id" => current_url(conn) } - - # Generating an `id` without already present pagination keys would - # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` - # instead of the `q.id > ^min_id` and `q.id < ^max_id`. - # This is because we only have ids present inside of the page, while - # `min_id`, `since_id` and `max_id` requires to know one outside of it. - if Map.take(conn.params, @id_keys) != %{} do - Map.put(fields, "id", current_url(conn)) - else - fields - end end def get_pagination_fields(conn, activities, extra_params \\ %{}) do From b15cfc3d365dcfa5f99159fe06e29de6f8aceb4f Mon Sep 17 00:00:00 2001 From: eugenijm <eugenijm@protonmail.com> Date: Mon, 18 May 2020 18:46:04 +0300 Subject: [PATCH 279/375] Mastodon API: ensure the notification endpoint doesn't return less than the requested amount of records unless it's the last page --- CHANGELOG.md | 1 + lib/pleroma/notification.ex | 19 +++++- lib/pleroma/user.ex | 8 +++ .../mastodon_api/views/notification_view.ex | 68 +++++++++---------- ...ete_notifications_from_invisible_users.exs | 18 +++++ test/notification_test.exs | 8 +++ .../notification_controller_test.exs | 27 ++++++++ .../views/notification_view_test.exs | 4 +- 8 files changed, 112 insertions(+), 41 deletions(-) create mode 100644 priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9361fa260..b3f2dd10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Filtering of push notifications on activities from blocked domains - Resolving Peertube accounts with Webfinger - `blob:` urls not being allowed by connect-src CSP +- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set ## [Unreleased (patch)] diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 3386a1933..9ee9606be 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -166,8 +166,16 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility}) query |> join(:left, [n, a], mutated_activity in Pleroma.Activity, on: - fragment("?->>'context'", a.data) == - fragment("?->>'context'", mutated_activity.data) and + fragment( + "COALESCE((?->'object')->>'id', ?->>'object')", + a.data, + a.data + ) == + fragment( + "COALESCE((?->'object')->>'id', ?->>'object')", + mutated_activity.data, + mutated_activity.data + ) and fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and fragment("?->>'type'", mutated_activity.data) == "Create", as: :mutated_activity @@ -541,6 +549,7 @@ def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do def skip?(%Activity{} = activity, %User{} = user) do [ :self, + :invisible, :followers, :follows, :non_followers, @@ -557,6 +566,12 @@ def skip?(:self, %Activity{} = activity, %User{} = user) do activity.data["actor"] == user.ap_id end + def skip?(:invisible, %Activity{} = activity, _) do + actor = activity.data["actor"] + user = User.get_cached_by_ap_id(actor) + User.invisible?(user) + end + def skip?( :followers, %Activity{} = activity, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c5c74d132..52ac9052b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1488,6 +1488,7 @@ def perform(:delete, %User{} = user) do end) delete_user_activities(user) + delete_notifications_from_user_activities(user) delete_outgoing_pending_follow_requests(user) @@ -1576,6 +1577,13 @@ def follow_import(%User{} = follower, followed_identifiers) }) end + def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do + Notification + |> join(:inner, [n], activity in assoc(n, :activity)) + |> where([n, a], fragment("? = ?", a.actor, ^ap_id)) + |> Repo.delete_all() + end + def delete_user_activities(%User{ap_id: ap_id} = user) do ap_id |> Activity.Queries.by_actor() diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index b11578623..3865be280 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -46,6 +46,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op activities |> Enum.filter(&(&1.data["type"] == "Move")) |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) + |> Enum.filter(& &1) actors = activities @@ -84,50 +85,45 @@ def render( # Note: :relationships contain user mutes (needed for :muted flag in :status) status_render_opts = %{relationships: opts[:relationships]} - with %{id: _} = account <- - AccountView.render( - "show.json", - %{user: actor, for: reading_user} - ) do - response = %{ - id: to_string(notification.id), - type: notification.type, - created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), - account: account, - pleroma: %{ - is_seen: notification.seen - } + account = + AccountView.render( + "show.json", + %{user: actor, for: reading_user} + ) + + response = %{ + id: to_string(notification.id), + type: notification.type, + created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), + account: account, + pleroma: %{ + is_seen: notification.seen } + } - case notification.type do - "mention" -> - put_status(response, activity, reading_user, status_render_opts) + case notification.type do + "mention" -> + put_status(response, activity, reading_user, status_render_opts) - "favourite" -> - put_status(response, parent_activity_fn.(), reading_user, status_render_opts) + "favourite" -> + put_status(response, parent_activity_fn.(), reading_user, status_render_opts) - "reblog" -> - put_status(response, parent_activity_fn.(), reading_user, status_render_opts) + "reblog" -> + put_status(response, parent_activity_fn.(), reading_user, status_render_opts) - "move" -> - put_target(response, activity, reading_user, %{}) + "move" -> + put_target(response, activity, reading_user, %{}) - "pleroma:emoji_reaction" -> - response - |> put_status(parent_activity_fn.(), reading_user, status_render_opts) - |> put_emoji(activity) + "pleroma:emoji_reaction" -> + response + |> put_status(parent_activity_fn.(), reading_user, status_render_opts) + |> put_emoji(activity) - "pleroma:chat_mention" -> - put_chat_message(response, activity, reading_user, status_render_opts) + "pleroma:chat_mention" -> + put_chat_message(response, activity, reading_user, status_render_opts) - type when type in ["follow", "follow_request"] -> - response - - _ -> - nil - end - else - _ -> nil + type when type in ["follow", "follow_request"] -> + response end end diff --git a/priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs b/priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs new file mode 100644 index 000000000..9e95a8111 --- /dev/null +++ b/priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs @@ -0,0 +1,18 @@ +defmodule Pleroma.Repo.Migrations.DeleteNotificationsFromInvisibleUsers do + use Ecto.Migration + + import Ecto.Query + alias Pleroma.Repo + + def up do + Pleroma.Notification + |> join(:inner, [n], activity in assoc(n, :activity)) + |> where( + [n, a], + fragment("? in (SELECT ap_id FROM users WHERE invisible = true)", a.actor) + ) + |> Repo.delete_all() + end + + def down, do: :ok +end diff --git a/test/notification_test.exs b/test/notification_test.exs index b9bbdceca..526f43fab 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -306,6 +306,14 @@ test "it doesn't create subscription notifications if the recipient cannot see t assert {:ok, []} == Notification.create_notifications(status) end + + test "it disables notifications from people who are invisible" do + author = insert(:user, invisible: true) + user = insert(:user) + + {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) + refute Notification.create_notification(status, user) + end end describe "follow / follow_request notifications" do diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index 698c99711..70ef0e8b5 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -313,6 +313,33 @@ test "filters notifications for Announce activities" do assert public_activity.id in activity_ids refute unlisted_activity.id in activity_ids end + + test "doesn't return less than the requested amount of records when the user's reply is liked" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, mention} = + CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"}) + + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, reply} = + CommonAPI.post(other_user, %{ + status: ".", + visibility: "public", + in_reply_to_status_id: activity.id + }) + + {:ok, _favorite} = CommonAPI.favorite(user, reply.id) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert [reply.id, mention.id] == activity_ids + end end test "filters notifications using exclude_types" do diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index b2fa5b302..9c399b2df 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -139,9 +139,7 @@ test "Follow notification" do test_notifications_rendering([notification], followed, [expected]) User.perform(:delete, follower) - notification = Notification |> Repo.one() |> Repo.preload(:activity) - - test_notifications_rendering([notification], followed, []) + refute Repo.one(Notification) end @tag capture_log: true From 2e8a236cef28c0b754aecb04a5c60c3b7655c5a6 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Sun, 14 Jun 2020 21:02:57 +0300 Subject: [PATCH 280/375] fix invalidates media url's --- config/config.exs | 7 + config/description.exs | 64 +++++++++ docs/configuration/cheatsheet.md | 6 +- installation/nginx-cache-purge.sh.example | 4 +- lib/pleroma/application.ex | 3 +- lib/pleroma/plugs/uploaded_media.ex | 16 ++- lib/pleroma/web/media_proxy/invalidation.ex | 29 ++-- .../web/media_proxy/invalidations/http.ex | 8 +- .../web/media_proxy/invalidations/script.ex | 36 ++--- lib/pleroma/web/media_proxy/media_proxy.ex | 35 ++++- .../web/media_proxy/media_proxy_controller.ex | 3 +- .../workers/attachments_cleanup_worker.ex | 133 ++++++++++-------- test/web/media_proxy/invalidation_test.exs | 65 +++++++++ .../media_proxy/invalidations/http_test.exs | 13 +- .../media_proxy/invalidations/script_test.exs | 21 ++- .../media_proxy_controller_test.exs | 17 +++ 16 files changed, 346 insertions(+), 114 deletions(-) create mode 100644 test/web/media_proxy/invalidation_test.exs diff --git a/config/config.exs b/config/config.exs index 9508ae077..e299fb8dd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -406,6 +406,13 @@ ], whitelist: [] +config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http, + method: :purge, + headers: [], + options: [] + +config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil + config :pleroma, :chat, enabled: true config :phoenix, :format_encoders, json: Jason diff --git a/config/description.exs b/config/description.exs index 807c945e0..857293794 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1637,6 +1637,31 @@ "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.", suggestions: ["https://example.com"] }, + %{ + key: :invalidation, + type: :keyword, + descpiption: "", + suggestions: [ + enabled: true, + provider: Pleroma.Web.MediaProxy.Invalidation.Script + ], + children: [ + %{ + key: :enabled, + type: :boolean, + description: "Enables invalidate media cache" + }, + %{ + key: :provider, + type: :module, + description: "Module which will be used to cache purge.", + suggestions: [ + Pleroma.Web.MediaProxy.Invalidation.Script, + Pleroma.Web.MediaProxy.Invalidation.Http + ] + } + ] + }, %{ key: :proxy_opts, type: :keyword, @@ -1709,6 +1734,45 @@ } ] }, + %{ + group: :pleroma, + key: Pleroma.Web.MediaProxy.Invalidation.Http, + type: :group, + description: "HTTP invalidate settings", + children: [ + %{ + key: :method, + type: :atom, + description: "HTTP method of request. Default: :purge" + }, + %{ + key: :headers, + type: {:list, :tuple}, + description: "HTTP headers of request.", + suggestions: [{"x-refresh", 1}] + }, + %{ + key: :options, + type: :keyword, + description: "Request options.", + suggestions: [params: %{ts: "xxx"}] + } + ] + }, + %{ + group: :pleroma, + key: Pleroma.Web.MediaProxy.Invalidation.Script, + type: :group, + description: "Script invalidate settings", + children: [ + %{ + key: :script_path, + type: :string, + description: "Path to shell script. Which will run purge cache.", + suggestions: ["./installation/nginx-cache-purge.sh.example"] + } + ] + }, %{ group: :pleroma, key: :gopher, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 505acb293..20bd0ed85 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -262,7 +262,7 @@ This section describe PWA manifest instance-specific values. Currently this opti #### Pleroma.Web.MediaProxy.Invalidation.Script -This strategy allow perform external bash script to purge cache. +This strategy allow perform external shell script to purge cache. Urls of attachments pass to script as arguments. * `script_path`: path to external script. @@ -278,8 +278,8 @@ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, This strategy allow perform custom http request to purge cache. * `method`: http method. default is `purge` -* `headers`: http headers. default is empty -* `options`: request options. default is empty +* `headers`: http headers. +* `options`: request options. Example: ```elixir diff --git a/installation/nginx-cache-purge.sh.example b/installation/nginx-cache-purge.sh.example index b2915321c..5f6cbb128 100755 --- a/installation/nginx-cache-purge.sh.example +++ b/installation/nginx-cache-purge.sh.example @@ -13,7 +13,7 @@ CACHE_DIRECTORY="/tmp/pleroma-media-cache" ## $3 - (optional) the number of parallel processes to run for grep. get_cache_files() { local max_parallel=${3-16} - find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E Rl "^KEY:.*$1" | sort -u + find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E -Rl "^KEY:.*$1" | sort -u } ## Removes an item from the given cache zone. @@ -37,4 +37,4 @@ purge() { } -purge $1 +purge $@ diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9d3d92b38..adebebc7a 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -148,7 +148,8 @@ defp cachex_children do build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500), build_cachex("web_resp", limit: 2500), build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), - build_cachex("failed_proxy_url", limit: 2500) + build_cachex("failed_proxy_url", limit: 2500), + build_cachex("deleted_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000) ] end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index 94147e0c4..2f3fde002 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Plugs.UploadedMedia do import Pleroma.Web.Gettext require Logger + alias Pleroma.Web.MediaProxy + @behaviour Plug # no slashes @path "media" @@ -35,8 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do %{query_params: %{"name" => name}} = conn -> name = String.replace(name, "\"", "\\\"") - conn - |> put_resp_header("content-disposition", "filename=\"#{name}\"") + put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") conn -> conn @@ -47,7 +48,8 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do with uploader <- Keyword.fetch!(config, :uploader), proxy_remote = Keyword.get(config, :proxy_remote, false), - {:ok, get_method} <- uploader.get_file(file) do + {:ok, get_method} <- uploader.get_file(file), + false <- media_is_deleted(conn, get_method) do get_media(conn, get_method, proxy_remote, opts) else _ -> @@ -59,6 +61,14 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do def call(conn, _opts), do: conn + defp media_is_deleted(%{request_path: path} = _conn, {:static_dir, _}) do + MediaProxy.in_deleted_urls(Pleroma.Web.base_url() <> path) + end + + defp media_is_deleted(_, {:url, url}), do: MediaProxy.in_deleted_urls(url) + + defp media_is_deleted(_, _), do: false + defp get_media(conn, {:static_dir, directory}, _, opts) do static_opts = Map.get(opts, :static_plug_opts) diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex index c037ff13e..83ff8589c 100644 --- a/lib/pleroma/web/media_proxy/invalidation.ex +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -5,22 +5,33 @@ defmodule Pleroma.Web.MediaProxy.Invalidation do @moduledoc false - @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()} + @callback purge(list(String.t()), Keyword.t()) :: {:ok, list(String.t())} | {:error, String.t()} alias Pleroma.Config + alias Pleroma.Web.MediaProxy - @spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()} + @spec enabled?() :: boolean() + def enabled?, do: Config.get([:media_proxy, :invalidation, :enabled]) + + @spec purge(list(String.t()) | String.t()) :: {:ok, list(String.t())} | {:error, String.t()} def purge(urls) do - [:media_proxy, :invalidation, :enabled] - |> Config.get() - |> do_purge(urls) + prepared_urls = prepare_urls(urls) + + if enabled?() do + do_purge(prepared_urls) + else + {:ok, prepared_urls} + end end - defp do_purge(true, urls) do + defp do_purge(urls) do provider = Config.get([:media_proxy, :invalidation, :provider]) - options = Config.get(provider) - provider.purge(urls, options) + provider.purge(urls, Config.get(provider)) end - defp do_purge(_, _), do: :ok + def prepare_urls(urls) do + urls + |> List.wrap() + |> Enum.map(&MediaProxy.url(&1)) + end end diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex index 07248df6e..3694b56e8 100644 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -10,9 +10,9 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do @impl Pleroma.Web.MediaProxy.Invalidation def purge(urls, opts) do - method = Map.get(opts, :method, :purge) - headers = Map.get(opts, :headers, []) - options = Map.get(opts, :options, []) + method = Keyword.get(opts, :method, :purge) + headers = Keyword.get(opts, :headers, []) + options = Keyword.get(opts, :options, []) Logger.debug("Running cache purge: #{inspect(urls)}") @@ -22,7 +22,7 @@ def purge(urls, opts) do end end) - {:ok, "success"} + {:ok, urls} end defp do_purge(method, url, headers, options) do diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex index 6be782132..d41d647bb 100644 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -10,32 +10,34 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do require Logger @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, %{script_path: script_path} = _options) do + def purge(urls, opts) do args = urls |> List.wrap() |> Enum.uniq() |> Enum.join(" ") - path = Path.expand(script_path) - - Logger.debug("Running cache purge: #{inspect(urls)}, #{path}") - - case do_purge(path, [args]) do - {result, exit_status} when exit_status > 0 -> - Logger.error("Error while cache purge: #{inspect(result)}") - {:error, inspect(result)} - - _ -> - {:ok, "success"} - end + opts + |> Keyword.get(:script_path, nil) + |> do_purge([args]) + |> handle_result(urls) end - def purge(_, _), do: {:error, "not found script path"} - - defp do_purge(path, args) do + defp do_purge(script_path, args) when is_binary(script_path) do + path = Path.expand(script_path) + Logger.debug("Running cache purge: #{inspect(args)}, #{inspect(path)}") System.cmd(path, args) rescue - error -> {inspect(error), 1} + error -> error + end + + defp do_purge(_, _), do: {:error, "not found script path"} + + defp handle_result({_result, 0}, urls), do: {:ok, urls} + defp handle_result({:error, error}, urls), do: handle_result(error, urls) + + defp handle_result(error, _) do + Logger.error("Error while cache purge: #{inspect(error)}") + {:error, inspect(error)} end end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index b2b524524..59ca217ab 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -6,20 +6,53 @@ defmodule Pleroma.Web.MediaProxy do alias Pleroma.Config alias Pleroma.Upload alias Pleroma.Web + alias Pleroma.Web.MediaProxy.Invalidation @base64_opts [padding: false] + @spec in_deleted_urls(String.t()) :: boolean() + def in_deleted_urls(url), do: elem(Cachex.exists?(:deleted_urls_cache, url(url)), 1) + + def remove_from_deleted_urls(urls) when is_list(urls) do + Cachex.execute!(:deleted_urls_cache, fn cache -> + Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1)) + end) + end + + def remove_from_deleted_urls(url) when is_binary(url) do + Cachex.del(:deleted_urls_cache, url(url)) + end + + def put_in_deleted_urls(urls) when is_list(urls) do + Cachex.execute!(:deleted_urls_cache, fn cache -> + Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true)) + end) + end + + def put_in_deleted_urls(url) when is_binary(url) do + Cachex.put(:deleted_urls_cache, url(url), true) + end + def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url def url(url) do - if disabled?() or local?(url) or whitelisted?(url) do + if disabled?() or not is_url_proxiable?(url) do url else encode_url(url) end end + @spec is_url_proxiable?(String.t()) :: boolean() + def is_url_proxiable?(url) do + if local?(url) or whitelisted?(url) do + false + else + true + end + end + defp disabled?, do: !Config.get([:media_proxy, :enabled], false) defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 4657a4383..ff0158d83 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -14,10 +14,11 @@ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do with config <- Pleroma.Config.get([:media_proxy], []), true <- Keyword.get(config, :enabled, false), {:ok, url} <- MediaProxy.decode_url(sig64, url64), + {_, false} <- {:in_deleted_urls, MediaProxy.in_deleted_urls(url)}, :ok <- filename_matches(params, conn.request_path, url) do ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) else - false -> + error when error in [false, {:in_deleted_urls, true}] -> send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) {:error, :invalid_signature} -> diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 49352db2a..4ad19c0fc 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -23,8 +23,25 @@ def perform( Enum.map(attachment["url"], & &1["href"]) end) - names = Enum.map(attachments, & &1["name"]) + # find all objects for copies of the attachments, name and actor doesn't matter here + hrefs + |> fetch_objects + |> prepare_objects(actor, Enum.map(attachments, & &1["name"])) + |> Enum.reduce({[], []}, fn {href, %{id: id, count: count}}, {ids, hrefs} -> + with 1 <- count do + {ids ++ [id], hrefs ++ [href]} + else + _ -> {ids ++ [id], hrefs} + end + end) + |> do_clean + {:ok, :success} + end + + def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} + + defp do_clean({object_ids, attachment_urls}) do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) prefix = @@ -39,68 +56,60 @@ def perform( "/" ) - # find all objects for copies of the attachments, name and actor doesn't matter here - object_ids_and_hrefs = - from(o in Object, - where: - fragment( - "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)", - o.data, - o.data, - ^hrefs - ) - ) - # The query above can be time consumptive on large instances until we - # refactor how uploads are stored - |> Repo.all(timeout: :infinity) - # we should delete 1 object for any given attachment, but don't delete - # files if there are more than 1 object for it - |> Enum.reduce(%{}, fn %{ - id: id, - data: %{ - "url" => [%{"href" => href}], - "actor" => obj_actor, - "name" => name - } - }, - acc -> - Map.update(acc, href, %{id: id, count: 1}, fn val -> - case obj_actor == actor and name in names do - true -> - # set id of the actor's object that will be deleted - %{val | id: id, count: val.count + 1} + Enum.each(attachment_urls, fn href -> + href + |> String.trim_leading("#{base_url}/#{prefix}") + |> uploader.delete_file() + end) - false -> - # another actor's object, just increase count to not delete file - %{val | count: val.count + 1} - end - end) - end) - |> Enum.map(fn {href, %{id: id, count: count}} -> - # only delete files that have single instance - with 1 <- count do - href - |> String.trim_leading("#{base_url}/#{prefix}") - |> uploader.delete_file() - - {id, href} - else - _ -> {id, nil} - end - end) - - object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end) - - from(o in Object, where: o.id in ^object_ids) - |> Repo.delete_all() - - object_ids_and_hrefs - |> Enum.filter(fn {_, href} -> not is_nil(href) end) - |> Enum.map(&elem(&1, 1)) - |> Pleroma.Web.MediaProxy.Invalidation.purge() - - {:ok, :success} + delete_objects(object_ids) end - def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} + defp delete_objects([_ | _] = object_ids) do + Repo.delete_all(from(o in Object, where: o.id in ^object_ids)) + end + + defp delete_objects(_), do: :ok + + # we should delete 1 object for any given attachment, but don't delete + # files if there are more than 1 object for it + def prepare_objects(objects, actor, names) do + objects + |> Enum.reduce(%{}, fn %{ + id: id, + data: %{ + "url" => [%{"href" => href}], + "actor" => obj_actor, + "name" => name + } + }, + acc -> + Map.update(acc, href, %{id: id, count: 1}, fn val -> + case obj_actor == actor and name in names do + true -> + # set id of the actor's object that will be deleted + %{val | id: id, count: val.count + 1} + + false -> + # another actor's object, just increase count to not delete file + %{val | count: val.count + 1} + end + end) + end) + end + + def fetch_objects(hrefs) do + from(o in Object, + where: + fragment( + "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)", + o.data, + o.data, + ^hrefs + ) + ) + # The query above can be time consumptive on large instances until we + # refactor how uploads are stored + |> Repo.all(timeout: :infinity) + end end diff --git a/test/web/media_proxy/invalidation_test.exs b/test/web/media_proxy/invalidation_test.exs new file mode 100644 index 000000000..3a9fa8c88 --- /dev/null +++ b/test/web/media_proxy/invalidation_test.exs @@ -0,0 +1,65 @@ +defmodule Pleroma.Web.MediaProxy.InvalidationTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + alias Pleroma.Config + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + import Mock + import Tesla.Mock + + setup do: clear_config([:media_proxy]) + + setup do + on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + :ok + end + + describe "Invalidation.Http" do + test "perform request to clear cache" do + Config.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], Invalidation.Http) + + Config.put([Invalidation.Http], method: :purge, headers: [{"x-refresh", 1}]) + image_url = "http://example.com/media/example.jpg" + Pleroma.Web.MediaProxy.put_in_deleted_urls(image_url) + + mock(fn + %{ + method: :purge, + url: "http://example.com/media/example.jpg", + headers: [{"x-refresh", 1}] + } -> + %Tesla.Env{status: 200} + end) + + assert capture_log(fn -> + assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + assert Invalidation.purge([image_url]) == {:ok, [image_url]} + assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + end) =~ "Running cache purge: [\"#{image_url}\"]" + end + end + + describe "Invalidation.Script" do + test "run script to clear cache" do + Config.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], Invalidation.Script) + Config.put([Invalidation.Script], script_path: "purge-nginx") + + image_url = "http://example.com/media/example.jpg" + Pleroma.Web.MediaProxy.put_in_deleted_urls(image_url) + + with_mocks [{System, [], [cmd: fn _, _ -> {"ok", 0} end]}] do + assert capture_log(fn -> + assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + assert Invalidation.purge([image_url]) == {:ok, [image_url]} + assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + end) =~ "Running cache purge: [\"#{image_url}\"]" + end + end + end +end diff --git a/test/web/media_proxy/invalidations/http_test.exs b/test/web/media_proxy/invalidations/http_test.exs index 8a3b4141c..09e7ca0fb 100644 --- a/test/web/media_proxy/invalidations/http_test.exs +++ b/test/web/media_proxy/invalidations/http_test.exs @@ -5,6 +5,11 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do import ExUnit.CaptureLog import Tesla.Mock + setup do + on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + :ok + end + test "logs hasn't error message when request is valid" do mock(fn %{method: :purge, url: "http://example.com/media/example.jpg"} -> @@ -14,8 +19,8 @@ test "logs hasn't error message when request is valid" do refute capture_log(fn -> assert Invalidation.Http.purge( ["http://example.com/media/example.jpg"], - %{} - ) == {:ok, "success"} + [] + ) == {:ok, ["http://example.com/media/example.jpg"]} end) =~ "Error while cache purge" end @@ -28,8 +33,8 @@ test "it write error message in logs when request invalid" do assert capture_log(fn -> assert Invalidation.Http.purge( ["http://example.com/media/example1.jpg"], - %{} - ) == {:ok, "success"} + [] + ) == {:ok, ["http://example.com/media/example1.jpg"]} end) =~ "Error while cache purge: url - http://example.com/media/example1.jpg" end end diff --git a/test/web/media_proxy/invalidations/script_test.exs b/test/web/media_proxy/invalidations/script_test.exs index 1358963ab..c69cec07a 100644 --- a/test/web/media_proxy/invalidations/script_test.exs +++ b/test/web/media_proxy/invalidations/script_test.exs @@ -4,17 +4,24 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do import ExUnit.CaptureLog + setup do + on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + :ok + end + test "it logger error when script not found" do assert capture_log(fn -> assert Invalidation.Script.purge( ["http://example.com/media/example.jpg"], - %{script_path: "./example"} - ) == {:error, "\"%ErlangError{original: :enoent}\""} - end) =~ "Error while cache purge: \"%ErlangError{original: :enoent}\"" + script_path: "./example" + ) == {:error, "%ErlangError{original: :enoent}"} + end) =~ "Error while cache purge: %ErlangError{original: :enoent}" - assert Invalidation.Script.purge( - ["http://example.com/media/example.jpg"], - %{} - ) == {:error, "not found script path"} + capture_log(fn -> + assert Invalidation.Script.purge( + ["http://example.com/media/example.jpg"], + [] + ) == {:error, "\"not found script path\""} + 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 da79d38a5..2b6b25221 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -10,6 +10,11 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do setup do: clear_config(:media_proxy) setup do: clear_config([Pleroma.Web.Endpoint, :secret_key_base]) + setup do + on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + :ok + end + test "it returns 404 when MediaProxy disabled", %{conn: conn} do Config.put([:media_proxy, :enabled], false) @@ -66,4 +71,16 @@ test "it performs ReverseProxy.call when signature valid", %{conn: conn} do assert %Plug.Conn{status: :success} = get(conn, url) end end + + test "it returns 404 when url contains in deleted_urls cache", %{conn: conn} do + Config.put([:media_proxy, :enabled], true) + Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") + Pleroma.Web.MediaProxy.put_in_deleted_urls("https://google.fn/test.png") + + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do + assert %Plug.Conn{status: 404, resp_body: "Not Found"} = get(conn, url) + end + end end From b7df7436c813bfcb4f27ac64c85ebc1507153601 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 15 Jun 2020 12:27:13 +0200 Subject: [PATCH 281/375] Conversations: Return last dm for conversation, not last message. --- lib/pleroma/conversation/participation.ex | 11 +++++++---- lib/pleroma/web/activity_pub/activity_pub.ex | 7 ++++--- .../web/mastodon_api/views/conversation_view.ex | 11 +++++++---- .../web/mastodon_api/views/conversation_view_test.exs | 11 ++++++++++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index ce7bd2396..8bc3e85d6 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -162,10 +162,13 @@ def for_user_with_last_activity_id(user, params \\ %{}) do for_user(user, params) |> Enum.map(fn participation -> activity_id = - ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - user: user, - blocking_user: user - }) + ActivityPub.fetch_latest_direct_activity_id_for_context( + participation.conversation.ap_id, + %{ + user: user, + blocking_user: user + } + ) %{ participation diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c9dc6135c..3e4f3ad30 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -210,7 +210,7 @@ def stream_out_participations(%Object{data: %{"context" => context}}, user) do conversation = Repo.preload(conversation, :participations) last_activity_id = - fetch_latest_activity_id_for_context(conversation.ap_id, %{ + fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{ user: user, blocking_user: user }) @@ -517,11 +517,12 @@ def fetch_activities_for_context(context, opts \\ %{}) do |> Repo.all() end - @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) :: + @spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) :: FlakeId.Ecto.CompatType.t() | nil - def fetch_latest_activity_id_for_context(context, opts \\ %{}) do + def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do context |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts)) + |> restrict_visibility(%{visibility: "direct"}) |> limit(1) |> select([a], a.id) |> Repo.one() diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index fbe618377..06f0c1728 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -23,10 +23,13 @@ def render("participation.json", %{participation: participation, for: user}) do last_activity_id = with nil <- participation.last_activity_id do - ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - user: user, - blocking_user: user - }) + ActivityPub.fetch_latest_direct_activity_id_for_context( + participation.conversation.ap_id, + %{ + user: user, + blocking_user: user + } + ) end activity = Activity.get_by_id_with_object(last_activity_id) diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs index 6f84366f8..2e8203c9b 100644 --- a/test/web/mastodon_api/views/conversation_view_test.exs +++ b/test/web/mastodon_api/views/conversation_view_test.exs @@ -15,8 +15,17 @@ test "represents a Mastodon Conversation entity" do user = insert(:user) other_user = insert(:user) + {:ok, parent} = CommonAPI.post(user, %{status: "parent"}) + {:ok, activity} = - CommonAPI.post(user, %{status: "hey @#{other_user.nickname}", visibility: "direct"}) + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}", + visibility: "direct", + in_reply_to_id: parent.id + }) + + {:ok, _reply_activity} = + CommonAPI.post(user, %{status: "hu", visibility: "public", in_reply_to_id: parent.id}) [participation] = Participation.for_user_with_last_activity_id(user) From 1092b3650068169ece0ac95cd88ec0e4da30036b Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 15 Jun 2020 12:30:11 +0200 Subject: [PATCH 282/375] Changelog: Add info about conversation view changes. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f2dd10f..c546f1f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Changed +- In Conversations, return only direct messages as `last_status` - MFR policy to set global expiration for all local Create activities <details> <summary>API Changes</summary> From 62b8c31b7a84dadb2a46861fe0f2dd1dbf9d40f0 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Mon, 15 Jun 2020 14:55:00 +0300 Subject: [PATCH 283/375] added tests --- .../media_proxy_cache_controller.ex | 31 ++++- .../web/media_proxy/invalidations/script.ex | 2 +- .../media_proxy_cache_controller_test.exs | 116 +++++++++++++++--- 3 files changed, 127 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex index 7b28f7c72..e3fa0ac28 100644 --- a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ApiSpec.Admin, as: Spec + alias Pleroma.Web.MediaProxy plug(Pleroma.Web.ApiSpec.CastAndValidate) @@ -24,15 +25,39 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation - def index(%{assigns: %{user: _}} = conn, _) do - render(conn, "index.json", urls: []) + def index(%{assigns: %{user: _}} = conn, params) do + cursor = + :deleted_urls_cache + |> :ets.table([{:traverse, {:select, Cachex.Query.create(true, :key)}}]) + |> :qlc.cursor() + + urls = + case params.page do + 1 -> + :qlc.next_answers(cursor, params.page_size) + + _ -> + :qlc.next_answers(cursor, (params.page - 1) * params.page_size) + :qlc.next_answers(cursor, params.page_size) + end + + :qlc.delete_cursor(cursor) + + render(conn, "index.json", urls: urls) end def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do + MediaProxy.remove_from_deleted_urls(urls) render(conn, "index.json", urls: urls) end - def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: _ban}} = conn, _) do + def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do + MediaProxy.Invalidation.purge(urls) + + if ban do + MediaProxy.put_in_deleted_urls(urls) + end + render(conn, "index.json", urls: urls) end end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex index d41d647bb..0217b119d 100644 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do require Logger @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, opts) do + def purge(urls, opts \\ %{}) do args = urls |> List.wrap() diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs index 1b1d6bc36..76a96f46f 100644 --- a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -6,6 +6,16 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory + import Mock + + alias Pleroma.Web.MediaProxy + + setup do: clear_config([:media_proxy]) + + setup do + on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + :ok + end setup do admin = insert(:user, is_admin: true) @@ -16,51 +26,121 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do |> assign(:user, admin) |> assign(:token, token) + Config.put([:media_proxy, :enabled], true) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], MediaProxy.Invalidation.Script) + {:ok, %{admin: admin, token: token, conn: conn}} end describe "GET /api/pleroma/admin/media_proxy_caches" do test "shows banned MediaProxy URLs", %{conn: conn} do + MediaProxy.put_in_deleted_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/fb1f4d.jpg" + ]) + + MediaProxy.put_in_deleted_urls("http://localhost:4001/media/gb1f44.jpg") + MediaProxy.put_in_deleted_urls("http://localhost:4001/media/tb13f47.jpg") + MediaProxy.put_in_deleted_urls("http://localhost:4001/media/wb1f46.jpg") + response = conn - |> get("/api/pleroma/admin/media_proxy_caches") + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2") |> json_response_and_validate_schema(200) - assert response["urls"] == [] + assert response["urls"] == [ + "http://localhost:4001/media/fb1f4d.jpg", + "http://localhost:4001/media/a688346.jpg" + ] + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=2") + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://localhost:4001/media/gb1f44.jpg", + "http://localhost:4001/media/tb13f47.jpg" + ] + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=3") + |> json_response_and_validate_schema(200) + + assert response["urls"] == ["http://localhost:4001/media/wb1f46.jpg"] end end describe "DELETE /api/pleroma/admin/media_proxy_caches/delete" do test "deleted MediaProxy URLs from banned", %{conn: conn} do + MediaProxy.put_in_deleted_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/fb1f4d.jpg" + ]) + response = conn |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/media_proxy_caches/delete", %{ - urls: ["http://example.com/media/a688346.jpg", "http://example.com/media/fb1f4d.jpg"] + urls: ["http://localhost:4001/media/a688346.jpg"] }) |> json_response_and_validate_schema(200) - assert response["urls"] == [ - "http://example.com/media/a688346.jpg", - "http://example.com/media/fb1f4d.jpg" - ] + assert response["urls"] == ["http://localhost:4001/media/a688346.jpg"] + refute MediaProxy.in_deleted_urls("http://localhost:4001/media/a688346.jpg") + assert MediaProxy.in_deleted_urls("http://localhost:4001/media/fb1f4d.jpg") end end describe "PURGE /api/pleroma/admin/media_proxy_caches/purge" do test "perform invalidates cache of MediaProxy", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/media_proxy_caches/purge", %{ - urls: ["http://example.com/media/a688346.jpg", "http://example.com/media/fb1f4d.jpg"] - }) - |> json_response_and_validate_schema(200) + urls = [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] - assert response["urls"] == [ - "http://example.com/media/a688346.jpg", - "http://example.com/media/fb1f4d.jpg" - ] + with_mocks [ + {MediaProxy.Invalidation.Script, [], + [ + purge: fn _, _ -> {"ok", 0} end + ]} + ] do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false}) + |> json_response_and_validate_schema(200) + + assert response["urls"] == urls + + refute MediaProxy.in_deleted_urls("http://example.com/media/a688346.jpg") + refute MediaProxy.in_deleted_urls("http://example.com/media/fb1f4d.jpg") + end + end + + test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: conn} do + urls = [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + + with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/purge", %{ + urls: urls, + ban: true + }) + |> json_response_and_validate_schema(200) + + assert response["urls"] == urls + + assert MediaProxy.in_deleted_urls("http://example.com/media/a688346.jpg") + assert MediaProxy.in_deleted_urls("http://example.com/media/fb1f4d.jpg") + end end end end From bd63089a633099233d4fc19faece2796253a7ee0 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn <egor@kislitsyn.com> Date: Mon, 15 Jun 2020 16:20:05 +0400 Subject: [PATCH 284/375] Fix tests --- .../rich_media/parsers/twitter_card_test.exs | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs index 3ccf26651..219f005a2 100644 --- a/test/web/rich_media/parsers/twitter_card_test.exs +++ b/test/web/rich_media/parsers/twitter_card_test.exs @@ -17,18 +17,18 @@ test "parses twitter card with only name attributes" do assert TwitterCard.parse(html, %{}) == %{ - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - site: nil, - description: + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "site" => nil, + "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: + "image" => "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", - type: "article", - url: + "type" => "article", + "url" => "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - title: + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database." } end @@ -40,17 +40,17 @@ test "parses twitter card with only property attributes" do assert TwitterCard.parse(html, %{}) == %{ - card: "summary_large_image", - description: + "card" => "summary_large_image", + "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: + "image" => "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt": "", - title: + "image:alt" => "", + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - url: + "url" => "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - type: "article" + "type" => "article" } end @@ -61,21 +61,21 @@ test "parses twitter card with name & property attributes" do assert TwitterCard.parse(html, %{}) == %{ - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - card: "summary_large_image", - description: + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "card" => "summary_large_image", + "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: + "image" => "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt": "", - site: nil, - title: + "image:alt" => "", + "site" => nil, + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - url: + "url" => "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - type: "article" + "type" => "article" } end @@ -90,15 +90,15 @@ test "respect only first title tag on the page" do assert TwitterCard.parse(html, %{}) == %{ - site: "@atlasobscura", - title: "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", - card: "summary_large_image", - image: image_path, - description: + "site" => "@atlasobscura", + "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", + "card" => "summary_large_image", + "image" => image_path, + "description" => "She's the only woman veteran honored with a monument at West Point. But where was she buried?", - site_name: "Atlas Obscura", - type: "article", - url: "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" + "site_name" => "Atlas Obscura", + "type" => "article", + "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" } end @@ -109,18 +109,18 @@ test "takes first founded title in html head if there is html markup error" do assert TwitterCard.parse(html, %{}) == %{ - site: nil, - title: + "site" => nil, + "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "app:id:googleplay": "com.nytimes.android", - "app:name:googleplay": "NYTimes", - "app:url:googleplay": "nytimes://reader/id/100000006583622", - description: + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - image: + "image" => "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", - type: "article", - url: + "type" => "article", + "url" => "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" } end From efdfc85c2d8e5118c1aa18e4f04026ec90cd11d2 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Mon, 15 Jun 2020 15:24:00 +0300 Subject: [PATCH 285/375] update docs --- docs/API/admin_api.md | 64 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 92816baf9..6659b605d 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1224,4 +1224,66 @@ Loads json generated from `config/descriptions.exs`. - Response: - On success: `204`, empty response - On failure: - - 400 Bad Request `"Invalid parameters"` when `status` is missing \ No newline at end of file + - 400 Bad Request `"Invalid parameters"` when `status` is missing + +## `GET /api/pleroma/admin/media_proxy_caches` + +### Get a list of all banned MediaProxy URLs in Cachex + +- Authentication: required +- Params: +- *optional* `page`: **integer** page number +- *optional* `page_size`: **integer** number of log entries per page (default is `50`) + +- Response: + +``` json +{ + "urls": [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] +} + +``` + +## `POST /api/pleroma/admin/media_proxy_caches/delete` + +### Remove a banned MediaProxy URL from Cachex + +- Authentication: required +- Params: + - `urls` + +- Response: + +``` json +{ + "urls": [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] +} + +``` + +## `POST /api/pleroma/admin/media_proxy_caches/purge` + +### Purge a MediaProxy URL + +- Authentication: required +- Params: + - `urls` + - `ban` + +- Response: + +``` json +{ + "urls": [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] +} + +``` From e1ee8bc1da17a356c88b535db7a9228fccc5251f Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 15 Jun 2020 14:29:34 +0200 Subject: [PATCH 286/375] User: update_follower_count refactor. --- lib/pleroma/user.ex | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 52ac9052b..39a9e13e8 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -747,7 +747,6 @@ def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do follower |> update_following_count() - |> set_cache() end end @@ -776,7 +775,6 @@ defp do_unfollow(%User{} = follower, %User{} = followed) do {:ok, follower} = follower |> update_following_count() - |> set_cache() {:ok, follower, followed} @@ -1128,35 +1126,25 @@ defp follow_information_changeset(user, params) do ]) end + @spec update_follower_count(User.t()) :: {:ok, User.t()} def update_follower_count(%User{} = user) do if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do - follower_count_query = - User.Query.build(%{followers: user, deactivated: false}) - |> select([u], %{count: count(u.id)}) + follower_count = FollowingRelationship.follower_count(user) - User - |> where(id: ^user.id) - |> join(:inner, [u], s in subquery(follower_count_query)) - |> update([u, s], - set: [follower_count: s.count] - ) - |> select([u], u) - |> Repo.update_all([]) - |> case do - {1, [user]} -> set_cache(user) - _ -> {:error, user} - end + user + |> follow_information_changeset(%{follower_count: follower_count}) + |> update_and_set_cache else {:ok, maybe_fetch_follow_information(user)} end end - @spec update_following_count(User.t()) :: User.t() + @spec update_following_count(User.t()) :: {:ok, User.t()} def update_following_count(%User{local: false} = user) do if Pleroma.Config.get([:instance, :external_user_synchronization]) do - maybe_fetch_follow_information(user) + {:ok, maybe_fetch_follow_information(user)} else - user + {:ok, user} end end @@ -1165,7 +1153,7 @@ def update_following_count(%User{local: true} = user) do user |> follow_information_changeset(%{following_count: following_count}) - |> Repo.update!() + |> update_and_set_cache() end def set_unread_conversation_count(%User{local: true} = user) do From b02311079961c5193af1c144516a3caeee72b582 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Mon, 15 Jun 2020 20:47:02 +0300 Subject: [PATCH 287/375] fixed a visibility of functions --- .../workers/attachments_cleanup_worker.ex | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 4ad19c0fc..8deeabda0 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -18,22 +18,11 @@ def perform( }, _job ) do - hrefs = - Enum.flat_map(attachments, fn attachment -> - Enum.map(attachment["url"], & &1["href"]) - end) - - # find all objects for copies of the attachments, name and actor doesn't matter here - hrefs + attachments + |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end) |> fetch_objects |> prepare_objects(actor, Enum.map(attachments, & &1["name"])) - |> Enum.reduce({[], []}, fn {href, %{id: id, count: count}}, {ids, hrefs} -> - with 1 <- count do - {ids ++ [id], hrefs ++ [href]} - else - _ -> {ids ++ [id], hrefs} - end - end) + |> filter_objects |> do_clean {:ok, :success} @@ -73,7 +62,17 @@ defp delete_objects(_), do: :ok # we should delete 1 object for any given attachment, but don't delete # files if there are more than 1 object for it - def prepare_objects(objects, actor, names) do + defp filter_objects(objects) do + Enum.reduce(objects, {[], []}, fn {href, %{id: id, count: count}}, {ids, hrefs} -> + with 1 <- count do + {ids ++ [id], hrefs ++ [href]} + else + _ -> {ids ++ [id], hrefs} + end + end) + end + + defp prepare_objects(objects, actor, names) do objects |> Enum.reduce(%{}, fn %{ id: id, @@ -98,7 +97,7 @@ def prepare_objects(objects, actor, names) do end) end - def fetch_objects(hrefs) do + defp fetch_objects(hrefs) do from(o in Object, where: fragment( From 1eb6cedaadee4e1ab3e0885b4e03a8dd17ba08ea Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 16 Jun 2020 13:08:27 +0200 Subject: [PATCH 288/375] ActivityPub: When restricting to media posts, only show 'Creates'. --- lib/pleroma/web/activity_pub/activity_pub.ex | 3 ++- .../controllers/account_controller_test.exs | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c9dc6135c..efb8b81db 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -833,7 +833,8 @@ defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do defp restrict_media(query, %{only_media: true}) do from( - [_activity, object] in query, + [activity, object] in query, + where: fragment("(?)->>'type' = ?", activity.data, "Create"), where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) ) end diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 1ce97378d..2343a9d2d 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -350,9 +350,10 @@ test "unimplemented pinned statuses feature", %{conn: conn} do assert json_response_and_validate_schema(conn, 200) == [] end - test "gets an users media", %{conn: conn} do + test "gets an users media, excludes reblogs", %{conn: conn} do note = insert(:note_activity) user = User.get_cached_by_ap_id(note.data["actor"]) + other_user = insert(:user) file = %Plug.Upload{ content_type: "image/jpg", @@ -364,6 +365,13 @@ test "gets an users media", %{conn: conn} do {:ok, %{id: image_post_id}} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]}) + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: other_user.ap_id) + + {:ok, %{id: other_image_post_id}} = + CommonAPI.post(other_user, %{status: "cofe2", media_ids: [media_id]}) + + {:ok, _announce} = CommonAPI.repeat(other_image_post_id, user) + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true") assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) From 4733f6a3371504ebb3eeb447d7c20d56c10b43bf Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 16 Jun 2020 13:09:28 +0200 Subject: [PATCH 289/375] Changelog: Add info about `only_media` changes. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2629bf84..eee442817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Changed +- Using the `only_media` filter on timelines will now exclude reblog media - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard <details> From 015f9258a9bd1430ab079f449b118b664c3b9664 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 16 Jun 2020 14:48:46 +0200 Subject: [PATCH 290/375] Transmogrifier: Extract user update handling tests. --- .../user_update_handling_test.exs | 154 +++++++++++++++++ test/web/activity_pub/transmogrifier_test.exs | 156 ------------------ 2 files changed, 154 insertions(+), 156 deletions(-) create mode 100644 test/web/activity_pub/transmogrifier/user_update_handling_test.exs diff --git a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs new file mode 100644 index 000000000..8e5d3b883 --- /dev/null +++ b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -0,0 +1,154 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming update activities" do + user = insert(:user, local: false) + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) + + assert data["id"] == update_data["id"] + + user = User.get_cached_by_ap_id(data["actor"]) + assert user.name == "gargle" + + assert user.avatar["url"] == [ + %{ + "href" => + "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + } + ] + + assert user.banner["url"] == [ + %{ + "href" => + "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } + ] + + assert user.bio == "<p>Some bio</p>" + end + + test "it works with alsoKnownAs" do + %{ap_id: actor} = insert(:user, local: false) + + assert User.get_cached_by_ap_id(actor).also_known_as == [] + + {:ok, _activity} = + "test/fixtures/mastodon-update.json" + |> File.read!() + |> Poison.decode!() + |> Map.put("actor", actor) + |> Map.update!("object", fn object -> + object + |> Map.put("actor", actor) + |> Map.put("id", actor) + |> Map.put("alsoKnownAs", [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ]) + end) + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ] + end + + test "it works with custom profile fields" do + user = insert(:user, local: false) + + assert user.fields == [] + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + Pleroma.Config.put([:instance, :max_remote_account_fields], 2) + + update_data = + put_in(update_data, ["object", "attachment"], [ + %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, + %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, + %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} + ]) + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + update_data = put_in(update_data, ["object", "attachment"], []) + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [] + end + + test "it works for incoming update activities which lock the account" do + user = insert(:user, local: false) + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + |> Map.put("manuallyApprovesFollowers", true) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + assert user.locked == true + end +end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 94d8552e8..b542bb7b8 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -401,162 +401,6 @@ test "it strips internal reactions" do refute Map.has_key?(object_data, "reaction_count") end - test "it works for incoming update activities" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", data["actor"]) - |> Map.put("id", data["actor"]) - - update_data = - update_data - |> Map.put("actor", data["actor"]) - |> Map.put("object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) - - assert data["id"] == update_data["id"] - - user = User.get_cached_by_ap_id(data["actor"]) - assert user.name == "gargle" - - assert user.avatar["url"] == [ - %{ - "href" => - "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - - assert user.banner["url"] == [ - %{ - "href" => - "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - - assert user.bio == "<p>Some bio</p>" - end - - test "it works with alsoKnownAs" do - {:ok, %Activity{data: %{"actor" => actor}}} = - "test/fixtures/mastodon-post-activity.json" - |> File.read!() - |> Poison.decode!() - |> Transmogrifier.handle_incoming() - - assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"] - - {:ok, _activity} = - "test/fixtures/mastodon-update.json" - |> File.read!() - |> Poison.decode!() - |> Map.put("actor", actor) - |> Map.update!("object", fn object -> - object - |> Map.put("actor", actor) - |> Map.put("id", actor) - |> Map.put("alsoKnownAs", [ - "http://mastodon.example.org/users/foo", - "http://example.org/users/bar" - ]) - end) - |> Transmogrifier.handle_incoming() - - assert User.get_cached_by_ap_id(actor).also_known_as == [ - "http://mastodon.example.org/users/foo", - "http://example.org/users/bar" - ] - end - - test "it works with custom profile fields" do - {:ok, activity} = - "test/fixtures/mastodon-post-activity.json" - |> File.read!() - |> Poison.decode!() - |> Transmogrifier.handle_incoming() - - user = User.get_cached_by_ap_id(activity.actor) - - assert user.fields == [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "foo1", "value" => "bar1"} - ] - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} - ] - - Pleroma.Config.put([:instance, :max_remote_account_fields], 2) - - update_data = - put_in(update_data, ["object", "attachment"], [ - %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, - %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, - %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} - ]) - - {:ok, _} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} - ] - - update_data = put_in(update_data, ["object", "attachment"], []) - - {:ok, _} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [] - end - - test "it works for incoming update activities which lock the account" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", data["actor"]) - |> Map.put("id", data["actor"]) - |> Map.put("manuallyApprovesFollowers", true) - - update_data = - update_data - |> Map.put("actor", data["actor"]) - |> Map.put("object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(data["actor"]) - assert user.locked == true - end - test "it works for incomming unfollows with an existing follow" do user = insert(:user) From 9a4fde97661595630ea840917ef83b4786f2e2d3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 31 May 2020 10:46:02 +0300 Subject: [PATCH 291/375] Mogrify args as custom tuples --- lib/mix/tasks/pleroma/config.ex | 10 +- lib/pleroma/config/config_db.ex | 274 ++++---- lib/pleroma/config/transfer_task.ex | 12 +- lib/pleroma/config/type/atom.ex | 22 + lib/pleroma/config/type/binary_value.ex | 23 + .../controllers/config_controller.ex | 34 +- .../web/admin_api/views/config_view.ex | 21 +- test/config/config_db_test.exs | 587 +++++++----------- test/config/transfer_task_test.exs | 94 +-- test/support/factory.ex | 17 +- test/tasks/config_test.exs | 41 +- test/upload/filter/mogrify_test.exs | 8 +- .../controllers/config_controller_test.exs | 256 +++++--- 13 files changed, 620 insertions(+), 779 deletions(-) create mode 100644 lib/pleroma/config/type/atom.ex create mode 100644 lib/pleroma/config/type/binary_value.ex diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 5c9ef6904..f1b3a8766 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -72,8 +72,7 @@ defp create(group, settings) do group |> Pleroma.Config.Loader.filter_group(settings) |> Enum.each(fn {key, value} -> - key = inspect(key) - {:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value}) + {:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value}) shell_info("Settings for key #{key} migrated.") end) @@ -131,12 +130,9 @@ defp write_and_delete(config, file, delete?) do end defp write(config, file) do - value = - config.value - |> ConfigDB.from_binary() - |> inspect(limit: :infinity) + value = inspect(config.value, limit: :infinity) - IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n") + IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n") config end diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 2b43d4c36..39b37c42e 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -6,7 +6,7 @@ defmodule Pleroma.ConfigDB do use Ecto.Schema import Ecto.Changeset - import Ecto.Query + import Ecto.Query, only: [select: 3] import Pleroma.Web.Gettext alias __MODULE__ @@ -14,16 +14,6 @@ defmodule Pleroma.ConfigDB do @type t :: %__MODULE__{} - @full_key_update [ - {:pleroma, :ecto_repos}, - {:quack, :meta}, - {:mime, :types}, - {:cors_plug, [:max_age, :methods, :expose, :headers]}, - {:auto_linker, :opts}, - {:swarm, :node_blacklist}, - {:logger, :backends} - ] - @full_subkey_update [ {:pleroma, :assets, :mascots}, {:pleroma, :emoji, :groups}, @@ -32,14 +22,10 @@ defmodule Pleroma.ConfigDB do {:pleroma, :mrf_keyword, :replace} ] - @regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u - - @delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}] - schema "config" do - field(:key, :string) - field(:group, :string) - field(:value, :binary) + field(:key, Pleroma.Config.Type.Atom) + field(:group, Pleroma.Config.Type.Atom) + field(:value, Pleroma.Config.Type.BinaryValue) field(:db, {:array, :string}, virtual: true, default: []) timestamps() @@ -51,10 +37,6 @@ def get_all_as_keyword do |> select([c], {c.group, c.key, c.value}) |> Repo.all() |> Enum.reduce([], fn {group, key, value}, acc -> - group = ConfigDB.from_string(group) - key = ConfigDB.from_string(key) - value = from_binary(value) - Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}])) end) end @@ -64,50 +46,41 @@ def get_by_params(params), do: Repo.get_by(ConfigDB, params) @spec changeset(ConfigDB.t(), map()) :: Changeset.t() def changeset(config, params \\ %{}) do - params = Map.put(params, :value, transform(params[:value])) - config |> cast(params, [:key, :group, :value]) |> validate_required([:key, :group, :value]) |> unique_constraint(:key, name: :config_group_key_index) end - @spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} - def create(params) do + defp create(params) do %ConfigDB{} |> changeset(params) |> Repo.insert() end - @spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} - def update(%ConfigDB{} = config, %{value: value}) do + defp update(%ConfigDB{} = config, %{value: value}) do config |> changeset(%{value: value}) |> Repo.update() end - @spec get_db_keys(ConfigDB.t()) :: [String.t()] - def get_db_keys(%ConfigDB{} = config) do - config.value - |> ConfigDB.from_binary() - |> get_db_keys(config.key) - end - @spec get_db_keys(keyword(), any()) :: [String.t()] def get_db_keys(value, key) do - if Keyword.keyword?(value) do - value |> Keyword.keys() |> Enum.map(&convert(&1)) - else - [convert(key)] - end + keys = + if Keyword.keyword?(value) do + Keyword.keys(value) + else + [key] + end + + Enum.map(keys, &to_json_types(&1)) end @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword() def merge_group(group, key, old_value, new_value) do - new_keys = to_map_set(new_value) + new_keys = to_mapset(new_value) - intersect_keys = - old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list() + intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list() merged_value = ConfigDB.merge(old_value, new_value) @@ -120,12 +93,10 @@ def merge_group(group, key, old_value, new_value) do [] end) |> List.flatten() - |> Enum.reduce(merged_value, fn subkey, acc -> - Keyword.put(acc, subkey, new_value[subkey]) - end) + |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1])) end - defp to_map_set(keyword) do + defp to_mapset(keyword) do keyword |> Keyword.keys() |> MapSet.new() @@ -159,43 +130,39 @@ defp deep_merge(_key, value1, value2) do @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} def update_or_create(params) do + params = Map.put(params, :value, to_elixir_types(params[:value])) search_opts = Map.take(params, [:group, :key]) with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), - {:partial_update, true, config} <- - {:partial_update, can_be_partially_updated?(config), config}, - old_value <- from_binary(config.value), - transformed_value <- do_transform(params[:value]), - {:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config}, - new_value <- - merge_group( - ConfigDB.from_string(config.group), - ConfigDB.from_string(config.key), - old_value, - transformed_value - ) do - ConfigDB.update(config, %{value: new_value}) + {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config}, + {_, true, config} <- {:can_be_merged, is_list(params[:value]), config} do + new_value = merge_group(config.group, config.key, config.value, params[:value]) + update(config, %{value: new_value}) else {reason, false, config} when reason in [:partial_update, :can_be_merged] -> - ConfigDB.update(config, params) + update(config, params) nil -> - ConfigDB.create(params) + create(params) end end defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config) - defp only_full_update?(%ConfigDB{} = config) do - config_group = ConfigDB.from_string(config.group) - config_key = ConfigDB.from_string(config.key) + defp only_full_update?(%ConfigDB{group: group, key: key}) do + full_key_update = [ + {:pleroma, :ecto_repos}, + {:quack, :meta}, + {:mime, :types}, + {:cors_plug, [:max_age, :methods, :expose, :headers]}, + {:auto_linker, :opts}, + {:swarm, :node_blacklist}, + {:logger, :backends} + ] - Enum.any?(@full_key_update, fn - {group, key} when is_list(key) -> - config_group == group and config_key in key - - {group, key} -> - config_group == group and config_key == key + Enum.any?(full_key_update, fn + {s_group, s_key} -> + group == s_group and ((is_list(s_key) and key in s_key) or key == s_key) end) end @@ -205,11 +172,10 @@ def delete(params) do with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]}, - old_value <- from_binary(config.value), - keys <- Enum.map(sub_keys, &do_transform_string(&1)), - {:partial_remove, config, new_value} when new_value != [] <- - {:partial_remove, config, Keyword.drop(old_value, keys)} do - ConfigDB.update(config, %{value: new_value}) + keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)), + {_, config, new_value} when new_value != [] <- + {:partial_remove, config, Keyword.drop(config.value, keys)} do + update(config, %{value: new_value}) else {:partial_remove, config, []} -> Repo.delete(config) @@ -225,37 +191,32 @@ def delete(params) do end end - @spec from_binary(binary()) :: term() - def from_binary(binary), do: :erlang.binary_to_term(binary) - - @spec from_binary_with_convert(binary()) :: any() - def from_binary_with_convert(binary) do - binary - |> from_binary() - |> do_convert() + @spec to_json_types(term()) :: map() | list() | boolean() | String.t() + def to_json_types(entity) when is_list(entity) do + Enum.map(entity, &to_json_types/1) end - @spec from_string(String.t()) :: atom() | no_return() - def from_string(string), do: do_transform_string(string) + def to_json_types(%Regex{} = entity), do: inspect(entity) - @spec convert(any()) :: any() - def convert(entity), do: do_convert(entity) - - defp do_convert(entity) when is_list(entity) do - for v <- entity, into: [], do: do_convert(v) + def to_json_types(entity) when is_map(entity) do + Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end) end - defp do_convert(%Regex{} = entity), do: inspect(entity) + def to_json_types({:args, args}) when is_list(args) do + arguments = + Enum.map(args, fn + arg when is_tuple(arg) -> inspect(arg) + arg -> to_json_types(arg) + end) - defp do_convert(entity) when is_map(entity) do - for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)} + %{"tuple" => [":args", arguments]} end - defp do_convert({:proxy_url, {type, :localhost, port}}) do - %{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]} + def to_json_types({:proxy_url, {type, :localhost, port}}) do + %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]} end - defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do + def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do ip = host |> :inet_parse.ntoa() @@ -264,66 +225,64 @@ defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do %{ "tuple" => [ ":proxy_url", - %{"tuple" => [do_convert(type), ip, port]} + %{"tuple" => [to_json_types(type), ip, port]} ] } end - defp do_convert({:proxy_url, {type, host, port}}) do + def to_json_types({:proxy_url, {type, host, port}}) do %{ "tuple" => [ ":proxy_url", - %{"tuple" => [do_convert(type), to_string(host), port]} + %{"tuple" => [to_json_types(type), to_string(host), port]} ] } end - defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]} + def to_json_types({:partial_chain, entity}), + do: %{"tuple" => [":partial_chain", inspect(entity)]} - defp do_convert(entity) when is_tuple(entity) do + def to_json_types(entity) when is_tuple(entity) do value = entity |> Tuple.to_list() - |> do_convert() + |> to_json_types() %{"tuple" => value} end - defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do + def to_json_types(entity) when is_binary(entity), do: entity + + def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do entity end - defp do_convert(entity) - when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do + def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do ":#{entity}" end - defp do_convert(entity) when is_atom(entity), do: inspect(entity) + def to_json_types(entity) when is_atom(entity), do: inspect(entity) - defp do_convert(entity) when is_binary(entity), do: entity + @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term() + def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do + arguments = + Enum.map(args, fn arg -> + if String.contains?(arg, ["{", "}"]) do + {elem, []} = Code.eval_string(arg) + elem + else + to_elixir_types(arg) + end + end) - @spec transform(any()) :: binary() | no_return() - def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do - entity - |> do_transform() - |> to_binary() + {:args, arguments} end - def transform(entity), do: to_binary(entity) - - @spec transform_with_out_binary(any()) :: any() - def transform_with_out_binary(entity), do: do_transform(entity) - - @spec to_binary(any()) :: binary() - def to_binary(entity), do: :erlang.term_to_binary(entity) - - defp do_transform(%Regex{} = entity), do: entity - - defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do - {:proxy_url, {do_transform_string(type), parse_host(host), port}} + def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do + {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}} end - defp do_transform(%{"tuple" => [":partial_chain", entity]}) do + def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do {partial_chain, []} = entity |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") @@ -332,25 +291,51 @@ defp do_transform(%{"tuple" => [":partial_chain", entity]}) do {:partial_chain, partial_chain} end - defp do_transform(%{"tuple" => entity}) do - Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) + def to_elixir_types(%{"tuple" => entity}) do + Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1))) end - defp do_transform(entity) when is_map(entity) do - for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)} + def to_elixir_types(entity) when is_map(entity) do + Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end) end - defp do_transform(entity) when is_list(entity) do - for v <- entity, into: [], do: do_transform(v) + def to_elixir_types(entity) when is_list(entity) do + Enum.map(entity, &to_elixir_types/1) end - defp do_transform(entity) when is_binary(entity) do + def to_elixir_types(entity) when is_binary(entity) do entity |> String.trim() - |> do_transform_string() + |> string_to_elixir_types() end - defp do_transform(entity), do: entity + def to_elixir_types(entity), do: entity + + @spec string_to_elixir_types(String.t()) :: + atom() | Regex.t() | module() | String.t() | no_return() + def string_to_elixir_types("~r" <> _pattern = regex) do + pattern = + ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u + + delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}] + + with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <- + Regex.named_captures(pattern, regex), + {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter), + {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do + result + end + end + + def string_to_elixir_types(":" <> atom), do: String.to_atom(atom) + + def string_to_elixir_types(value) do + if is_module_name?(value) do + String.to_existing_atom("Elixir." <> value) + else + value + end + end defp parse_host("localhost"), do: :localhost @@ -387,25 +372,6 @@ defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do end end - defp do_transform_string("~r" <> _pattern = regex) do - with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <- - Regex.named_captures(@regex, regex), - {:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter), - {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do - result - end - end - - defp do_transform_string(":" <> atom), do: String.to_atom(atom) - - defp do_transform_string(value) do - if is_module_name?(value) do - String.to_existing_atom("Elixir." <> value) - else - value - end - end - @spec is_module_name?(String.t()) :: boolean() def is_module_name?(string) do Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index c02b70e96..eb86b8ff4 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -28,10 +28,6 @@ defmodule Pleroma.Config.TransferTask do {:pleroma, Pleroma.Captcha, [:seconds_valid]}, {:pleroma, Pleroma.Upload, [:proxy_remote]}, {:pleroma, :instance, [:upload_limit]}, - {:pleroma, :email_notifications, [:digest]}, - {:pleroma, :oauth2, [:clean_expired_tokens]}, - {:pleroma, Pleroma.ActivityExpiration, [:enabled]}, - {:pleroma, Pleroma.ScheduledActivity, [:enabled]}, {:pleroma, :gopher, [:enabled]} ] @@ -48,7 +44,7 @@ def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do {logger, other} = (Repo.all(ConfigDB) ++ deleted_settings) - |> Enum.map(&transform_and_merge/1) + |> Enum.map(&merge_with_default/1) |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end) logger @@ -92,11 +88,7 @@ defp maybe_set_pleroma_last(apps) do end end - defp transform_and_merge(%{group: group, key: key, value: value} = setting) do - group = ConfigDB.from_string(group) - key = ConfigDB.from_string(key) - value = ConfigDB.from_binary(value) - + defp merge_with_default(%{group: group, key: key, value: value} = setting) do default = Config.Holder.default_config(group, key) merged = diff --git a/lib/pleroma/config/type/atom.ex b/lib/pleroma/config/type/atom.ex new file mode 100644 index 000000000..387869284 --- /dev/null +++ b/lib/pleroma/config/type/atom.ex @@ -0,0 +1,22 @@ +defmodule Pleroma.Config.Type.Atom do + use Ecto.Type + + def type, do: :atom + + def cast(key) when is_atom(key) do + {:ok, key} + end + + def cast(key) when is_binary(key) do + {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)} + end + + def cast(_), do: :error + + def load(key) do + {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)} + end + + def dump(key) when is_atom(key), do: {:ok, inspect(key)} + def dump(_), do: :error +end diff --git a/lib/pleroma/config/type/binary_value.ex b/lib/pleroma/config/type/binary_value.ex new file mode 100644 index 000000000..17c5524a3 --- /dev/null +++ b/lib/pleroma/config/type/binary_value.ex @@ -0,0 +1,23 @@ +defmodule Pleroma.Config.Type.BinaryValue do + use Ecto.Type + + def type, do: :term + + def cast(value) when is_binary(value) do + if String.valid?(value) do + {:ok, value} + else + {:ok, :erlang.binary_to_term(value)} + end + end + + def cast(value), do: {:ok, value} + + def load(value) when is_binary(value) do + {:ok, :erlang.binary_to_term(value)} + end + + def dump(value) do + {:ok, :erlang.term_to_binary(value)} + end +end diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index d6e2019bc..7f60470cb 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -33,7 +33,11 @@ def descriptions(conn, _params) do def show(conn, %{only_db: true}) do with :ok <- configurable_from_database() do configs = Pleroma.Repo.all(ConfigDB) - render(conn, "index.json", %{configs: configs}) + + render(conn, "index.json", %{ + configs: configs, + need_reboot: Restarter.Pleroma.need_reboot?() + }) end end @@ -61,17 +65,20 @@ def show(conn, _params) do value end - %{ - group: ConfigDB.convert(group), - key: ConfigDB.convert(key), - value: ConfigDB.convert(merged_value) + %ConfigDB{ + group: group, + key: key, + value: merged_value } |> Pleroma.Maps.put_if_present(:db, db) end) end) |> List.flatten() - json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) + render(conn, "index.json", %{ + configs: merged, + need_reboot: Restarter.Pleroma.need_reboot?() + }) end end @@ -91,24 +98,17 @@ def update(%{body_params: %{configs: configs}} = conn, _) do {deleted, updated} = results - |> Enum.map(fn {:ok, config} -> - Map.put(config, :db, ConfigDB.get_db_keys(config)) - end) - |> Enum.split_with(fn config -> - Ecto.get_meta(config, :state) == :deleted + |> Enum.map(fn {:ok, %{key: key, value: value} = config} -> + Map.put(config, :db, ConfigDB.get_db_keys(value, key)) end) + |> Enum.split_with(&(Ecto.get_meta(&1, :state) == :deleted)) Config.TransferTask.load_and_update_env(deleted, false) if not Restarter.Pleroma.need_reboot?() do changed_reboot_settings? = (updated ++ deleted) - |> Enum.any?(fn config -> - group = ConfigDB.from_string(config.group) - key = ConfigDB.from_string(config.key) - value = ConfigDB.from_binary(config.value) - Config.TransferTask.pleroma_need_restart?(group, key, value) - end) + |> Enum.any?(&Config.TransferTask.pleroma_need_restart?(&1.group, &1.key, &1.value)) if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() end diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex index 587ef760e..d2d8b5907 100644 --- a/lib/pleroma/web/admin_api/views/config_view.ex +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -5,23 +5,20 @@ defmodule Pleroma.Web.AdminAPI.ConfigView do use Pleroma.Web, :view - def render("index.json", %{configs: configs} = params) do - map = %{ - configs: render_many(configs, __MODULE__, "show.json", as: :config) - } + alias Pleroma.ConfigDB - if params[:need_reboot] do - Map.put(map, :need_reboot, true) - else - map - end + def render("index.json", %{configs: configs} = params) do + %{ + configs: render_many(configs, __MODULE__, "show.json", as: :config), + need_reboot: params[:need_reboot] + } end def render("show.json", %{config: config}) do map = %{ - key: config.key, - group: config.group, - value: Pleroma.ConfigDB.from_binary_with_convert(config.value) + key: ConfigDB.to_json_types(config.key), + group: ConfigDB.to_json_types(config.group), + value: ConfigDB.to_json_types(config.value) } if config.db != [] do diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs index 336de7359..a04575c6f 100644 --- a/test/config/config_db_test.exs +++ b/test/config/config_db_test.exs @@ -7,40 +7,28 @@ defmodule Pleroma.ConfigDBTest do import Pleroma.Factory alias Pleroma.ConfigDB - test "get_by_key/1" do + test "get_by_params/1" do config = insert(:config) insert(:config) assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key}) end - test "create/1" do - {:ok, config} = ConfigDB.create(%{group: ":pleroma", key: ":some_key", value: "some_value"}) - assert config == ConfigDB.get_by_params(%{group: ":pleroma", key: ":some_key"}) - end - - test "update/1" do - config = insert(:config) - {:ok, updated} = ConfigDB.update(config, %{value: "some_value"}) - loaded = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - assert loaded == updated - end - test "get_all_as_keyword/0" do saved = insert(:config) - insert(:config, group: ":quack", key: ":level", value: ConfigDB.to_binary(:info)) - insert(:config, group: ":quack", key: ":meta", value: ConfigDB.to_binary([:none])) + insert(:config, group: ":quack", key: ":level", value: :info) + insert(:config, group: ":quack", key: ":meta", value: [:none]) insert(:config, group: ":quack", key: ":webhook_url", - value: ConfigDB.to_binary("https://hooks.slack.com/services/KEY/some_val") + value: "https://hooks.slack.com/services/KEY/some_val" ) config = ConfigDB.get_all_as_keyword() assert config[:pleroma] == [ - {ConfigDB.from_string(saved.key), ConfigDB.from_binary(saved.value)} + {saved.key, saved.value} ] assert config[:quack][:level] == :info @@ -51,11 +39,11 @@ test "get_all_as_keyword/0" do describe "update_or_create/1" do test "common" do config = insert(:config) - key2 = "another_key" + key2 = :another_key params = [ - %{group: "pleroma", key: key2, value: "another_value"}, - %{group: config.group, key: config.key, value: "new_value"} + %{group: :pleroma, key: key2, value: "another_value"}, + %{group: :pleroma, key: config.key, value: "new_value"} ] assert Repo.all(ConfigDB) |> length() == 1 @@ -65,16 +53,16 @@ test "common" do assert Repo.all(ConfigDB) |> length() == 2 config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - config2 = ConfigDB.get_by_params(%{group: "pleroma", key: key2}) + config2 = ConfigDB.get_by_params(%{group: :pleroma, key: key2}) - assert config1.value == ConfigDB.transform("new_value") - assert config2.value == ConfigDB.transform("another_value") + assert config1.value == "new_value" + assert config2.value == "another_value" end test "partial update" do - config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: :val2)) + config = insert(:config, value: [key1: "val1", key2: :val2]) - {:ok, _config} = + {:ok, config} = ConfigDB.update_or_create(%{ group: config.group, key: config.key, @@ -83,15 +71,14 @@ test "partial update" do updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - value = ConfigDB.from_binary(updated.value) - assert length(value) == 3 - assert value[:key1] == :val1 - assert value[:key2] == :val2 - assert value[:key3] == :val3 + assert config.value == updated.value + assert updated.value[:key1] == :val1 + assert updated.value[:key2] == :val2 + assert updated.value[:key3] == :val3 end test "deep merge" do - config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: [k1: :v1, k2: "v2"])) + config = insert(:config, value: [key1: "val1", key2: [k1: :v1, k2: "v2"]]) {:ok, config} = ConfigDB.update_or_create(%{ @@ -103,18 +90,15 @@ test "deep merge" do updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) assert config.value == updated.value - - value = ConfigDB.from_binary(updated.value) - assert value[:key1] == :val1 - assert value[:key2] == [k1: :v1, k2: :v2, k3: :v3] - assert value[:key3] == :val3 + assert updated.value[:key1] == :val1 + assert updated.value[:key2] == [k1: :v1, k2: :v2, k3: :v3] + assert updated.value[:key3] == :val3 end test "only full update for some keys" do - config1 = insert(:config, key: ":ecto_repos", value: ConfigDB.to_binary(repo: Pleroma.Repo)) + config1 = insert(:config, key: :ecto_repos, value: [repo: Pleroma.Repo]) - config2 = - insert(:config, group: ":cors_plug", key: ":max_age", value: ConfigDB.to_binary(18)) + config2 = insert(:config, group: :cors_plug, key: :max_age, value: 18) {:ok, _config} = ConfigDB.update_or_create(%{ @@ -133,8 +117,8 @@ test "only full update for some keys" do updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key}) updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key}) - assert ConfigDB.from_binary(updated1.value) == [another_repo: [Pleroma.Repo]] - assert ConfigDB.from_binary(updated2.value) == 777 + assert updated1.value == [another_repo: [Pleroma.Repo]] + assert updated2.value == 777 end test "full update if value is not keyword" do @@ -142,7 +126,7 @@ test "full update if value is not keyword" do insert(:config, group: ":tesla", key: ":adapter", - value: ConfigDB.to_binary(Tesla.Adapter.Hackney) + value: Tesla.Adapter.Hackney ) {:ok, _config} = @@ -154,20 +138,20 @@ test "full update if value is not keyword" do updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - assert ConfigDB.from_binary(updated.value) == Tesla.Adapter.Httpc + assert updated.value == Tesla.Adapter.Httpc end test "only full update for some subkeys" do config1 = insert(:config, key: ":emoji", - value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]) + value: [groups: [a: 1, b: 2], key: [a: 1]] ) config2 = insert(:config, key: ":assets", - value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1]) + value: [mascots: [a: 1, b: 2], key: [a: 1]] ) {:ok, _config} = @@ -187,8 +171,8 @@ test "only full update for some subkeys" do updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key}) updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key}) - assert ConfigDB.from_binary(updated1.value) == [groups: [c: 3, d: 4], key: [a: 1, b: 2]] - assert ConfigDB.from_binary(updated2.value) == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]] + assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]] + assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]] end end @@ -206,14 +190,14 @@ test "full delete" do end test "partial subkeys delete" do - config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])) + config = insert(:config, value: [groups: [a: 1, b: 2], key: [a: 1]]) {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]}) assert Ecto.get_meta(deleted, :state) == :loaded - assert deleted.value == ConfigDB.to_binary(key: [a: 1]) + assert deleted.value == [key: [a: 1]] updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) @@ -221,7 +205,7 @@ test "partial subkeys delete" do end test "full delete if remaining value after subkeys deletion is empty list" do - config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2])) + config = insert(:config, value: [groups: [a: 1, b: 2]]) {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]}) @@ -232,234 +216,159 @@ test "full delete if remaining value after subkeys deletion is empty list" do end end - describe "transform/1" do + describe "to_elixir_types/1" do test "string" do - binary = ConfigDB.transform("value as string") - assert binary == :erlang.term_to_binary("value as string") - assert ConfigDB.from_binary(binary) == "value as string" + assert ConfigDB.to_elixir_types("value as string") == "value as string" end test "boolean" do - binary = ConfigDB.transform(false) - assert binary == :erlang.term_to_binary(false) - assert ConfigDB.from_binary(binary) == false + assert ConfigDB.to_elixir_types(false) == false end test "nil" do - binary = ConfigDB.transform(nil) - assert binary == :erlang.term_to_binary(nil) - assert ConfigDB.from_binary(binary) == nil + assert ConfigDB.to_elixir_types(nil) == nil end test "integer" do - binary = ConfigDB.transform(150) - assert binary == :erlang.term_to_binary(150) - assert ConfigDB.from_binary(binary) == 150 + assert ConfigDB.to_elixir_types(150) == 150 end test "atom" do - binary = ConfigDB.transform(":atom") - assert binary == :erlang.term_to_binary(:atom) - assert ConfigDB.from_binary(binary) == :atom + assert ConfigDB.to_elixir_types(":atom") == :atom end test "ssl options" do - binary = ConfigDB.transform([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) - assert binary == :erlang.term_to_binary([:tlsv1, :"tlsv1.1", :"tlsv1.2"]) - assert ConfigDB.from_binary(binary) == [:tlsv1, :"tlsv1.1", :"tlsv1.2"] + assert ConfigDB.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [ + :tlsv1, + :"tlsv1.1", + :"tlsv1.2" + ] end test "pleroma module" do - binary = ConfigDB.transform("Pleroma.Bookmark") - assert binary == :erlang.term_to_binary(Pleroma.Bookmark) - assert ConfigDB.from_binary(binary) == Pleroma.Bookmark + assert ConfigDB.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark end test "pleroma string" do - binary = ConfigDB.transform("Pleroma") - assert binary == :erlang.term_to_binary("Pleroma") - assert ConfigDB.from_binary(binary) == "Pleroma" + assert ConfigDB.to_elixir_types("Pleroma") == "Pleroma" end test "phoenix module" do - binary = ConfigDB.transform("Phoenix.Socket.V1.JSONSerializer") - assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer) - assert ConfigDB.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer + assert ConfigDB.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") == + Phoenix.Socket.V1.JSONSerializer end test "tesla module" do - binary = ConfigDB.transform("Tesla.Adapter.Hackney") - assert binary == :erlang.term_to_binary(Tesla.Adapter.Hackney) - assert ConfigDB.from_binary(binary) == Tesla.Adapter.Hackney + assert ConfigDB.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney end test "ExSyslogger module" do - binary = ConfigDB.transform("ExSyslogger") - assert binary == :erlang.term_to_binary(ExSyslogger) - assert ConfigDB.from_binary(binary) == ExSyslogger + assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger end test "Quack.Logger module" do - binary = ConfigDB.transform("Quack.Logger") - assert binary == :erlang.term_to_binary(Quack.Logger) - assert ConfigDB.from_binary(binary) == Quack.Logger + assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger end test "Swoosh.Adapters modules" do - binary = ConfigDB.transform("Swoosh.Adapters.SMTP") - assert binary == :erlang.term_to_binary(Swoosh.Adapters.SMTP) - assert ConfigDB.from_binary(binary) == Swoosh.Adapters.SMTP - binary = ConfigDB.transform("Swoosh.Adapters.AmazonSES") - assert binary == :erlang.term_to_binary(Swoosh.Adapters.AmazonSES) - assert ConfigDB.from_binary(binary) == Swoosh.Adapters.AmazonSES + assert ConfigDB.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP + assert ConfigDB.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES end test "sigil" do - binary = ConfigDB.transform("~r[comp[lL][aA][iI][nN]er]") - assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/) - assert ConfigDB.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/ + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/ end test "link sigil" do - binary = ConfigDB.transform("~r/https:\/\/example.com/") - assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/) - assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/ + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/ end test "link sigil with um modifiers" do - binary = ConfigDB.transform("~r/https:\/\/example.com/um") - assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/um) - assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/um + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") == + ~r/https:\/\/example.com/um end test "link sigil with i modifier" do - binary = ConfigDB.transform("~r/https:\/\/example.com/i") - assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i) - assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/i + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i end test "link sigil with s modifier" do - binary = ConfigDB.transform("~r/https:\/\/example.com/s") - assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s) - assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/s + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s end test "raise if valid delimiter not found" do assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn -> - ConfigDB.transform("~r/https://[]{}<>\"'()|example.com/s") + ConfigDB.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s") end end test "2 child tuple" do - binary = ConfigDB.transform(%{"tuple" => ["v1", ":v2"]}) - assert binary == :erlang.term_to_binary({"v1", :v2}) - assert ConfigDB.from_binary(binary) == {"v1", :v2} + assert ConfigDB.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2} end test "proxy tuple with localhost" do - binary = - ConfigDB.transform(%{ - "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}] - }) - - assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, :localhost, 1234}}) - assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, :localhost, 1234}} + assert ConfigDB.to_elixir_types(%{ + "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}] + }) == {:proxy_url, {:socks5, :localhost, 1234}} end test "proxy tuple with domain" do - binary = - ConfigDB.transform(%{ - "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}] - }) - - assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, 'domain.com', 1234}}) - assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, 'domain.com', 1234}} + assert ConfigDB.to_elixir_types(%{ + "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}] + }) == {:proxy_url, {:socks5, 'domain.com', 1234}} end test "proxy tuple with ip" do - binary = - ConfigDB.transform(%{ - "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}] - }) - - assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}) - assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}} + assert ConfigDB.to_elixir_types(%{ + "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}] + }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}} end test "tuple with n childs" do - binary = - ConfigDB.transform(%{ - "tuple" => [ - "v1", - ":v2", - "Pleroma.Bookmark", - 150, - false, - "Phoenix.Socket.V1.JSONSerializer" - ] - }) - - assert binary == - :erlang.term_to_binary( - {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer} - ) - - assert ConfigDB.from_binary(binary) == - {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer} + assert ConfigDB.to_elixir_types(%{ + "tuple" => [ + "v1", + ":v2", + "Pleroma.Bookmark", + 150, + false, + "Phoenix.Socket.V1.JSONSerializer" + ] + }) == {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer} end test "map with string key" do - binary = ConfigDB.transform(%{"key" => "value"}) - assert binary == :erlang.term_to_binary(%{"key" => "value"}) - assert ConfigDB.from_binary(binary) == %{"key" => "value"} + assert ConfigDB.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"} end test "map with atom key" do - binary = ConfigDB.transform(%{":key" => "value"}) - assert binary == :erlang.term_to_binary(%{key: "value"}) - assert ConfigDB.from_binary(binary) == %{key: "value"} + assert ConfigDB.to_elixir_types(%{":key" => "value"}) == %{key: "value"} end test "list of strings" do - binary = ConfigDB.transform(["v1", "v2", "v3"]) - assert binary == :erlang.term_to_binary(["v1", "v2", "v3"]) - assert ConfigDB.from_binary(binary) == ["v1", "v2", "v3"] + assert ConfigDB.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"] end test "list of modules" do - binary = ConfigDB.transform(["Pleroma.Repo", "Pleroma.Activity"]) - assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity]) - assert ConfigDB.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity] + assert ConfigDB.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [ + Pleroma.Repo, + Pleroma.Activity + ] end test "list of atoms" do - binary = ConfigDB.transform([":v1", ":v2", ":v3"]) - assert binary == :erlang.term_to_binary([:v1, :v2, :v3]) - assert ConfigDB.from_binary(binary) == [:v1, :v2, :v3] + assert ConfigDB.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3] end test "list of mixed values" do - binary = - ConfigDB.transform([ - "v1", - ":v2", - "Pleroma.Repo", - "Phoenix.Socket.V1.JSONSerializer", - 15, - false - ]) - - assert binary == - :erlang.term_to_binary([ - "v1", - :v2, - Pleroma.Repo, - Phoenix.Socket.V1.JSONSerializer, - 15, - false - ]) - - assert ConfigDB.from_binary(binary) == [ + assert ConfigDB.to_elixir_types([ + "v1", + ":v2", + "Pleroma.Repo", + "Phoenix.Socket.V1.JSONSerializer", + 15, + false + ]) == [ "v1", :v2, Pleroma.Repo, @@ -470,40 +379,23 @@ test "list of mixed values" do end test "simple keyword" do - binary = ConfigDB.transform([%{"tuple" => [":key", "value"]}]) - assert binary == :erlang.term_to_binary([{:key, "value"}]) - assert ConfigDB.from_binary(binary) == [{:key, "value"}] - assert ConfigDB.from_binary(binary) == [key: "value"] + assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"] end test "keyword with partial_chain key" do - binary = - ConfigDB.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}]) - - assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1) - assert ConfigDB.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1] + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]} + ]) == [partial_chain: &:hackney_connect.partial_chain/1] end test "keyword" do - binary = - ConfigDB.transform([ - %{"tuple" => [":types", "Pleroma.PostgresTypes"]}, - %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]}, - %{"tuple" => [":migration_lock", nil]}, - %{"tuple" => [":key1", 150]}, - %{"tuple" => [":key2", "string"]} - ]) - - assert binary == - :erlang.term_to_binary( - types: Pleroma.PostgresTypes, - telemetry_event: [Pleroma.Repo.Instrumenter], - migration_lock: nil, - key1: 150, - key2: "string" - ) - - assert ConfigDB.from_binary(binary) == [ + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":types", "Pleroma.PostgresTypes"]}, + %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]}, + %{"tuple" => [":migration_lock", nil]}, + %{"tuple" => [":key1", 150]}, + %{"tuple" => [":key2", "string"]} + ]) == [ types: Pleroma.PostgresTypes, telemetry_event: [Pleroma.Repo.Instrumenter], migration_lock: nil, @@ -513,85 +405,55 @@ test "keyword" do end test "complex keyword with nested mixed childs" do - binary = - ConfigDB.transform([ - %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]}, - %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]}, - %{"tuple" => [":link_name", true]}, - %{"tuple" => [":proxy_remote", false]}, - %{"tuple" => [":common_map", %{":key" => "value"}]}, - %{ - "tuple" => [ - ":proxy_opts", - [ - %{"tuple" => [":redirect_on_failure", false]}, - %{"tuple" => [":max_body_length", 1_048_576]}, - %{ - "tuple" => [ - ":http", - [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}] - ] - } - ] - ] - } - ]) - - assert binary == - :erlang.term_to_binary( - uploader: Pleroma.Uploaders.Local, - filters: [Pleroma.Upload.Filter.Dedupe], - link_name: true, - proxy_remote: false, - common_map: %{key: "value"}, - proxy_opts: [ - redirect_on_failure: false, - max_body_length: 1_048_576, - http: [ - follow_redirect: true, - pool: :upload + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]}, + %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]}, + %{"tuple" => [":link_name", true]}, + %{"tuple" => [":proxy_remote", false]}, + %{"tuple" => [":common_map", %{":key" => "value"}]}, + %{ + "tuple" => [ + ":proxy_opts", + [ + %{"tuple" => [":redirect_on_failure", false]}, + %{"tuple" => [":max_body_length", 1_048_576]}, + %{ + "tuple" => [ + ":http", + [ + %{"tuple" => [":follow_redirect", true]}, + %{"tuple" => [":pool", ":upload"]} + ] + ] + } ] ] - ) - - assert ConfigDB.from_binary(binary) == - [ - uploader: Pleroma.Uploaders.Local, - filters: [Pleroma.Upload.Filter.Dedupe], - link_name: true, - proxy_remote: false, - common_map: %{key: "value"}, - proxy_opts: [ - redirect_on_failure: false, - max_body_length: 1_048_576, - http: [ - follow_redirect: true, - pool: :upload - ] + } + ]) == [ + uploader: Pleroma.Uploaders.Local, + filters: [Pleroma.Upload.Filter.Dedupe], + link_name: true, + proxy_remote: false, + common_map: %{key: "value"}, + proxy_opts: [ + redirect_on_failure: false, + max_body_length: 1_048_576, + http: [ + follow_redirect: true, + pool: :upload ] ] + ] end test "common keyword" do - binary = - ConfigDB.transform([ - %{"tuple" => [":level", ":warn"]}, - %{"tuple" => [":meta", [":all"]]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":val", nil]}, - %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]} - ]) - - assert binary == - :erlang.term_to_binary( - level: :warn, - meta: [:all], - path: "", - val: nil, - webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE" - ) - - assert ConfigDB.from_binary(binary) == [ + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":level", ":warn"]}, + %{"tuple" => [":meta", [":all"]]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":val", nil]}, + %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]} + ]) == [ level: :warn, meta: [:all], path: "", @@ -601,98 +463,73 @@ test "common keyword" do end test "complex keyword with sigil" do - binary = - ConfigDB.transform([ - %{"tuple" => [":federated_timeline_removal", []]}, - %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, - %{"tuple" => [":replace", []]} - ]) - - assert binary == - :erlang.term_to_binary( - federated_timeline_removal: [], - reject: [~r/comp[lL][aA][iI][nN]er/], - replace: [] - ) - - assert ConfigDB.from_binary(binary) == - [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []] + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":federated_timeline_removal", []]}, + %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, + %{"tuple" => [":replace", []]} + ]) == [ + federated_timeline_removal: [], + reject: [~r/comp[lL][aA][iI][nN]er/], + replace: [] + ] end test "complex keyword with tuples with more than 2 values" do - binary = - ConfigDB.transform([ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key1", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ]) - - assert binary == - :erlang.term_to_binary( - http: [ - key1: [ - _: [ - {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, - {"/websocket", Phoenix.Endpoint.CowboyWebSocket, - {Phoenix.Transports.WebSocket, - {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}}, - {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} - ] + assert ConfigDB.to_elixir_types([ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key1", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } ] ] - ) - - assert ConfigDB.from_binary(binary) == [ + } + ]) == [ http: [ key1: [ {:_, diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 473899d1d..f53829e09 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -6,9 +6,9 @@ defmodule Pleroma.Config.TransferTaskTest do use Pleroma.DataCase import ExUnit.CaptureLog + import Pleroma.Factory alias Pleroma.Config.TransferTask - alias Pleroma.ConfigDB setup do: clear_config(:configurable_from_database, true) @@ -19,31 +19,11 @@ test "transfer config values from db to env" do refute Application.get_env(:postgrex, :test_key) initial = Application.get_env(:logger, :level) - ConfigDB.create(%{ - group: ":pleroma", - key: ":test_key", - value: [live: 2, com: 3] - }) - - ConfigDB.create(%{ - group: ":idna", - key: ":test_key", - value: [live: 15, com: 35] - }) - - ConfigDB.create(%{ - group: ":quack", - key: ":test_key", - value: [:test_value1, :test_value2] - }) - - ConfigDB.create(%{ - group: ":postgrex", - key: ":test_key", - value: :value - }) - - ConfigDB.create(%{group: ":logger", key: ":level", value: :debug}) + insert(:config, key: :test_key, value: [live: 2, com: 3]) + insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35]) + insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2]) + insert(:config, group: :postgrex, key: :test_key, value: :value) + insert(:config, group: :logger, key: :level, value: :debug) TransferTask.start_link([]) @@ -66,17 +46,8 @@ test "transfer config values for 1 group and some keys" do level = Application.get_env(:quack, :level) meta = Application.get_env(:quack, :meta) - ConfigDB.create(%{ - group: ":quack", - key: ":level", - value: :info - }) - - ConfigDB.create(%{ - group: ":quack", - key: ":meta", - value: [:none] - }) + insert(:config, group: :quack, key: :level, value: :info) + insert(:config, group: :quack, key: :meta, value: [:none]) TransferTask.start_link([]) @@ -95,17 +66,8 @@ test "transfer config values with full subkey update" do clear_config(:emoji) clear_config(:assets) - ConfigDB.create(%{ - group: ":pleroma", - key: ":emoji", - value: [groups: [a: 1, b: 2]] - }) - - ConfigDB.create(%{ - group: ":pleroma", - key: ":assets", - value: [mascots: [a: 1, b: 2]] - }) + insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) + insert(:config, key: :assets, value: [mascots: [a: 1, b: 2]]) TransferTask.start_link([]) @@ -122,12 +84,7 @@ test "transfer config values with full subkey update" do test "don't restart if no reboot time settings were changed" do clear_config(:emoji) - - ConfigDB.create(%{ - group: ":pleroma", - key: ":emoji", - value: [groups: [a: 1, b: 2]] - }) + insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) refute String.contains?( capture_log(fn -> TransferTask.start_link([]) end), @@ -137,25 +94,13 @@ test "don't restart if no reboot time settings were changed" do test "on reboot time key" do clear_config(:chat) - - ConfigDB.create(%{ - group: ":pleroma", - key: ":chat", - value: [enabled: false] - }) - + insert(:config, key: :chat, value: [enabled: false]) assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" end test "on reboot time subkey" do clear_config(Pleroma.Captcha) - - ConfigDB.create(%{ - group: ":pleroma", - key: "Pleroma.Captcha", - value: [seconds_valid: 60] - }) - + insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" end @@ -163,17 +108,8 @@ test "don't restart pleroma on reboot time key and subkey if there is false flag clear_config(:chat) clear_config(Pleroma.Captcha) - ConfigDB.create(%{ - group: ":pleroma", - key: ":chat", - value: [enabled: false] - }) - - ConfigDB.create(%{ - group: ":pleroma", - key: "Pleroma.Captcha", - value: [seconds_valid: 60] - }) + insert(:config, key: :chat, value: [enabled: false]) + insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) refute String.contains?( capture_log(fn -> TransferTask.load_and_update_env([], false) end), diff --git a/test/support/factory.ex b/test/support/factory.ex index 6e3676aca..e517d5bc6 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -396,24 +396,17 @@ def registration_factory do } end - def config_factory do + def config_factory(attrs \\ %{}) do %Pleroma.ConfigDB{ - key: - sequence(:key, fn key -> - # Atom dynamic registration hack in tests - "some_key_#{key}" - |> String.to_atom() - |> inspect() - end), - group: ":pleroma", + key: sequence(:key, &String.to_atom("some_key_#{&1}")), + group: :pleroma, value: sequence( :value, - fn key -> - :erlang.term_to_binary(%{another_key: "#{key}somevalue", another: "#{key}somevalue"}) - end + &%{another_key: "#{&1}somevalue", another: "#{&1}somevalue"} ) } + |> merge_attributes(attrs) end def marker_factory do diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index 04bc947a9..e1bddfebf 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -5,6 +5,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.ConfigDB alias Pleroma.Repo @@ -49,24 +51,19 @@ test "filtered settings are migrated to db" do refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"}) - assert ConfigDB.from_binary(config1.value) == [key: "value", key2: [Repo]] - assert ConfigDB.from_binary(config2.value) == [key: "value2", key2: ["Activity"]] - assert ConfigDB.from_binary(config3.value) == :info + assert config1.value == [key: "value", key2: [Repo]] + assert config2.value == [key: "value2", key2: ["Activity"]] + assert config3.value == :info end test "config table is truncated before migration" do - ConfigDB.create(%{ - group: ":pleroma", - key: ":first_setting", - value: [key: "value", key2: ["Activity"]] - }) - + insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]]) assert Repo.aggregate(ConfigDB, :count, :id) == 1 Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) - assert ConfigDB.from_binary(config.value) == [key: "value", key2: [Repo]] + assert config.value == [key: "value", key2: [Repo]] end end @@ -82,19 +79,9 @@ test "config table is truncated before migration" do end test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do - ConfigDB.create(%{ - group: ":pleroma", - key: ":setting_first", - value: [key: "value", key2: ["Activity"]] - }) - - ConfigDB.create(%{ - group: ":pleroma", - key: ":setting_second", - value: [key: "value2", key2: [Repo]] - }) - - ConfigDB.create(%{group: ":quack", key: ":level", value: :info}) + insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]]) + insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]]) + insert(:config, group: :quack, key: :level, value: :info) Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) @@ -107,9 +94,8 @@ test "settings are migrated to file and deleted from db", %{temp_file: temp_file end test "load a settings with large values and pass to file", %{temp_file: temp_file} do - ConfigDB.create(%{ - group: ":pleroma", - key: ":instance", + insert(:config, + key: :instance, value: [ name: "Pleroma", email: "example@example.com", @@ -163,7 +149,6 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil extended_nickname_format: true, multi_factor_authentication: [ totp: [ - # digits 6 or 8 digits: 6, period: 30 ], @@ -173,7 +158,7 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil ] ] ] - }) + ) Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs index b6a463e8c..62ca30487 100644 --- a/test/upload/filter/mogrify_test.exs +++ b/test/upload/filter/mogrify_test.exs @@ -6,21 +6,17 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do use Pleroma.DataCase import Mock - alias Pleroma.Config - alias Pleroma.Upload alias Pleroma.Upload.Filter - setup do: clear_config([Filter.Mogrify, :args]) - test "apply mogrify filter" do - Config.put([Filter.Mogrify, :args], [{"tint", "40"}]) + clear_config(Filter.Mogrify, args: [{"tint", "40"}]) File.cp!( "test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg" ) - upload = %Upload{ + upload = %Pleroma.Upload{ name: "an… image.jpg", content_type: "image/jpg", path: Path.absname("test/fixtures/image_tmp.jpg"), diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs index 780de8d18..064ef9bc7 100644 --- a/test/web/admin_api/controllers/config_controller_test.exs +++ b/test/web/admin_api/controllers/config_controller_test.exs @@ -57,12 +57,12 @@ test "with settings only in db", %{conn: conn} do ] } = json_response_and_validate_schema(conn, 200) - assert key1 == config1.key - assert key2 == config2.key + assert key1 == inspect(config1.key) + assert key2 == inspect(config2.key) end test "db is added to settings that are in db", %{conn: conn} do - _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name")) + _config = insert(:config, key: ":instance", value: [name: "Some name"]) %{"configs" => configs} = conn @@ -83,7 +83,7 @@ test "merged default setting with db settings", %{conn: conn} do config3 = insert(:config, - value: ConfigDB.to_binary(k1: :v1, k2: :v2) + value: [k1: :v1, k2: :v2] ) %{"configs" => configs} = @@ -93,42 +93,45 @@ test "merged default setting with db settings", %{conn: conn} do assert length(configs) > 3 + saved_configs = [config1, config2, config3] + keys = Enum.map(saved_configs, &inspect(&1.key)) + received_configs = Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [config1.key, config2.key, config3.key] + group == ":pleroma" and key in keys end) assert length(received_configs) == 3 db_keys = config3.value - |> ConfigDB.from_binary() |> Keyword.keys() - |> ConfigDB.convert() + |> ConfigDB.to_json_types() + + keys = Enum.map(saved_configs -- [config3], &inspect(&1.key)) + + values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value)) + + mapset_keys = MapSet.new(keys ++ db_keys) Enum.each(received_configs, fn %{"value" => value, "db" => db} -> - assert db in [[config1.key], [config2.key], db_keys] + db = MapSet.new(db) + assert MapSet.subset?(db, mapset_keys) - assert value in [ - ConfigDB.from_binary_with_convert(config1.value), - ConfigDB.from_binary_with_convert(config2.value), - ConfigDB.from_binary_with_convert(config3.value) - ] + assert value in values end) end test "subkeys with full update right merge", %{conn: conn} do - config1 = - insert(:config, - key: ":emoji", - value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]) - ) + insert(:config, + key: ":emoji", + value: [groups: [a: 1, b: 2], key: [a: 1]] + ) - config2 = - insert(:config, - key: ":assets", - value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1]) - ) + insert(:config, + key: ":assets", + value: [mascots: [a: 1, b: 2], key: [a: 1]] + ) %{"configs" => configs} = conn @@ -137,14 +140,14 @@ test "subkeys with full update right merge", %{conn: conn} do vals = Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [config1.key, config2.key] + group == ":pleroma" and key in [":emoji", ":assets"] end) emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) - emoji_val = ConfigDB.transform_with_out_binary(emoji["value"]) - assets_val = ConfigDB.transform_with_out_binary(assets["value"]) + emoji_val = ConfigDB.to_elixir_types(emoji["value"]) + assets_val = ConfigDB.to_elixir_types(assets["value"]) assert emoji_val[:groups] == [a: 1, b: 2] assert assets_val[:mascots] == [a: 1, b: 2] @@ -277,7 +280,8 @@ test "create new config setting in db", %{conn: conn} do "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, "db" => [":key5"] } - ] + ], + "need_reboot" => false } assert Application.get_env(:pleroma, :key1) == "value1" @@ -357,7 +361,8 @@ test "save configs setting without explicit key", %{conn: conn} do "value" => "https://hooks.slack.com/services/KEY", "db" => [":webhook_url"] } - ] + ], + "need_reboot" => false } assert Application.get_env(:quack, :level) == :info @@ -366,14 +371,14 @@ test "save configs setting without explicit key", %{conn: conn} do end test "saving config with partial update", %{conn: conn} do - config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) + insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) conn = conn |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/config", %{ configs: [ - %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]} + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} ] }) @@ -389,7 +394,8 @@ test "saving config with partial update", %{conn: conn} do ], "db" => [":key1", ":key2", ":key3"] } - ] + ], + "need_reboot" => false } end @@ -500,8 +506,7 @@ test "update setting which need reboot, don't change reboot flag until reboot", end test "saving config with nested merge", %{conn: conn} do - config = - insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) + insert(:config, key: :key1, value: [key1: 1, key2: [k1: 1, k2: 2]]) conn = conn @@ -509,8 +514,8 @@ test "saving config with nested merge", %{conn: conn} do |> post("/api/pleroma/admin/config", %{ configs: [ %{ - group: config.group, - key: config.key, + group: ":pleroma", + key: ":key1", value: [ %{"tuple" => [":key3", 3]}, %{ @@ -548,7 +553,8 @@ test "saving config with nested merge", %{conn: conn} do ], "db" => [":key1", ":key3", ":key2"] } - ] + ], + "need_reboot" => false } end @@ -588,7 +594,8 @@ test "saving special atoms", %{conn: conn} do ], "db" => [":ssl_options"] } - ] + ], + "need_reboot" => false } assert Application.get_env(:pleroma, :key1) == [ @@ -600,12 +607,11 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do backends = Application.get_env(:logger, :backends) on_exit(fn -> Application.put_env(:logger, :backends, backends) end) - config = - insert(:config, - group: ":logger", - key: ":backends", - value: :erlang.term_to_binary([]) - ) + insert(:config, + group: :logger, + key: :backends, + value: [] + ) Pleroma.Config.TransferTask.load_and_update_env([], false) @@ -617,8 +623,8 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do |> post("/api/pleroma/admin/config", %{ configs: [ %{ - group: config.group, - key: config.key, + group: ":logger", + key: ":backends", value: [":console"] } ] @@ -634,7 +640,8 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do ], "db" => [":backends"] } - ] + ], + "need_reboot" => false } assert Application.get_env(:logger, :backends) == [ @@ -643,19 +650,18 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do end test "saving full setting if value is not keyword", %{conn: conn} do - config = - insert(:config, - group: ":tesla", - key: ":adapter", - value: :erlang.term_to_binary(Tesla.Adapter.Hackey) - ) + insert(:config, + group: :tesla, + key: :adapter, + value: Tesla.Adapter.Hackey + ) conn = conn |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/config", %{ configs: [ - %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"} + %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"} ] }) @@ -667,7 +673,8 @@ test "saving full setting if value is not keyword", %{conn: conn} do "value" => "Tesla.Adapter.Httpc", "db" => [":adapter"] } - ] + ], + "need_reboot" => false } end @@ -677,13 +684,13 @@ test "update config setting & delete with fallback to default value", %{ token: token } do ueberauth = Application.get_env(:ueberauth, Ueberauth) - config1 = insert(:config, key: ":keyaa1") - config2 = insert(:config, key: ":keyaa2") + insert(:config, key: :keyaa1) + insert(:config, key: :keyaa2) config3 = insert(:config, - group: ":ueberauth", - key: "Ueberauth" + group: :ueberauth, + key: Ueberauth ) conn = @@ -691,8 +698,8 @@ test "update config setting & delete with fallback to default value", %{ |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/config", %{ configs: [ - %{group: config1.group, key: config1.key, value: "another_value"}, - %{group: config2.group, key: config2.key, value: "another_value"} + %{group: ":pleroma", key: ":keyaa1", value: "another_value"}, + %{group: ":pleroma", key: ":keyaa2", value: "another_value"} ] }) @@ -700,22 +707,23 @@ test "update config setting & delete with fallback to default value", %{ "configs" => [ %{ "group" => ":pleroma", - "key" => config1.key, + "key" => ":keyaa1", "value" => "another_value", "db" => [":keyaa1"] }, %{ "group" => ":pleroma", - "key" => config2.key, + "key" => ":keyaa2", "value" => "another_value", "db" => [":keyaa2"] } - ] + ], + "need_reboot" => false } assert Application.get_env(:pleroma, :keyaa1) == "another_value" assert Application.get_env(:pleroma, :keyaa2) == "another_value" - assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value) + assert Application.get_env(:ueberauth, Ueberauth) == config3.value conn = build_conn() @@ -724,7 +732,7 @@ test "update config setting & delete with fallback to default value", %{ |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/config", %{ configs: [ - %{group: config2.group, key: config2.key, delete: true}, + %{group: ":pleroma", key: ":keyaa2", delete: true}, %{ group: ":ueberauth", key: "Ueberauth", @@ -734,7 +742,8 @@ test "update config setting & delete with fallback to default value", %{ }) assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [] + "configs" => [], + "need_reboot" => false } assert Application.get_env(:ueberauth, Ueberauth) == ueberauth @@ -801,7 +810,8 @@ test "common config example", %{conn: conn} do ":name" ] } - ] + ], + "need_reboot" => false } end @@ -935,7 +945,8 @@ test "tuples with more than two values", %{conn: conn} do ], "db" => [":http"] } - ] + ], + "need_reboot" => false } end @@ -1000,7 +1011,8 @@ test "settings with nesting map", %{conn: conn} do ], "db" => [":key2", ":key3"] } - ] + ], + "need_reboot" => false } end @@ -1027,7 +1039,8 @@ test "value as map", %{conn: conn} do "value" => %{"key" => "some_val"}, "db" => [":key1"] } - ] + ], + "need_reboot" => false } end @@ -1077,16 +1090,16 @@ test "queues key as atom", %{conn: conn} do ":background" ] } - ] + ], + "need_reboot" => false } end test "delete part of settings by atom subkeys", %{conn: conn} do - config = - insert(:config, - key: ":keyaa1", - value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") - ) + insert(:config, + key: :keyaa1, + value: [subkey1: "val1", subkey2: "val2", subkey3: "val3"] + ) conn = conn @@ -1094,8 +1107,8 @@ test "delete part of settings by atom subkeys", %{conn: conn} do |> post("/api/pleroma/admin/config", %{ configs: [ %{ - group: config.group, - key: config.key, + group: ":pleroma", + key: ":keyaa1", subkeys: [":subkey1", ":subkey3"], delete: true } @@ -1110,7 +1123,8 @@ test "delete part of settings by atom subkeys", %{conn: conn} do "value" => [%{"tuple" => [":subkey2", "val2"]}], "db" => [":subkey2"] } - ] + ], + "need_reboot" => false } end @@ -1236,6 +1250,90 @@ test "doesn't set keys not in the whitelist", %{conn: conn} do assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" assert Application.get_env(:not_real, :anything) == "value6" end + + test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do + clear_config(Pleroma.Upload.Filter.Mogrify) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: "Pleroma.Upload.Filter.Mogrify", + value: [ + %{"tuple" => [":args", ["auto-orient", "strip"]]} + ] + } + ] + }) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Upload.Filter.Mogrify", + "value" => [ + %{"tuple" => [":args", ["auto-orient", "strip"]]} + ], + "db" => [":args"] + } + ], + "need_reboot" => false + } + + assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]] + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: "Pleroma.Upload.Filter.Mogrify", + value: [ + %{ + "tuple" => [ + ":args", + [ + "auto-orient", + "strip", + "{\"implode\", \"1\"}", + "{\"resize\", \"3840x1080>\"}" + ] + ] + } + ] + } + ] + }) + |> json_response(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Upload.Filter.Mogrify", + "value" => [ + %{ + "tuple" => [ + ":args", + [ + "auto-orient", + "strip", + "{\"implode\", \"1\"}", + "{\"resize\", \"3840x1080>\"}" + ] + ] + } + ], + "db" => [":args"] + } + ], + "need_reboot" => false + } + + assert Config.get(Pleroma.Upload.Filter.Mogrify) == [ + args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] + ] + end end describe "GET /api/pleroma/admin/config/descriptions" do From 23decaab81b900bff0f6eacad7ea6a894239e4ce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 31 May 2020 12:38:24 +0300 Subject: [PATCH 292/375] fix for updated hackney warning: :hackney_connect.partial_chain/1 is undefined or private --- test/config/config_db_test.exs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs index a04575c6f..8d753e255 100644 --- a/test/config/config_db_test.exs +++ b/test/config/config_db_test.exs @@ -382,12 +382,6 @@ test "simple keyword" do assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"] end - test "keyword with partial_chain key" do - assert ConfigDB.to_elixir_types([ - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]} - ]) == [partial_chain: &:hackney_connect.partial_chain/1] - end - test "keyword" do assert ConfigDB.to_elixir_types([ %{"tuple" => [":types", "Pleroma.PostgresTypes"]}, From e1603ac8fee2a660c3dc510dee5967e0fd1bbd98 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 31 May 2020 13:25:04 +0300 Subject: [PATCH 293/375] fix attemps to merge map --- lib/pleroma/config/config_db.ex | 3 ++- test/config/config_db_test.exs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 39b37c42e..70be17ecf 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -135,7 +135,8 @@ def update_or_create(params) do with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config}, - {_, true, config} <- {:can_be_merged, is_list(params[:value]), config} do + {_, true, config} <- + {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do new_value = merge_group(config.group, config.key, config.value, params[:value]) update(config, %{value: new_value}) else diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs index 8d753e255..3895e2cda 100644 --- a/test/config/config_db_test.exs +++ b/test/config/config_db_test.exs @@ -43,7 +43,7 @@ test "common" do params = [ %{group: :pleroma, key: key2, value: "another_value"}, - %{group: :pleroma, key: config.key, value: "new_value"} + %{group: :pleroma, key: config.key, value: [a: 1, b: 2, c: "new_value"]} ] assert Repo.all(ConfigDB) |> length() == 1 @@ -55,7 +55,7 @@ test "common" do config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key}) config2 = ConfigDB.get_by_params(%{group: :pleroma, key: key2}) - assert config1.value == "new_value" + assert config1.value == [a: 1, b: 2, c: "new_value"] assert config2.value == "another_value" end @@ -398,6 +398,10 @@ test "keyword" do ] end + test "trandformed keyword" do + assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"] + end + test "complex keyword with nested mixed childs" do assert ConfigDB.to_elixir_types([ %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]}, From 32c6576b600e2f24310f429f4b2391f95a5b5ba0 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 31 May 2020 14:42:15 +0300 Subject: [PATCH 294/375] naming --- lib/pleroma/config/config_db.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 70be17ecf..30bd51b05 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -331,7 +331,7 @@ def string_to_elixir_types("~r" <> _pattern = regex) do def string_to_elixir_types(":" <> atom), do: String.to_atom(atom) def string_to_elixir_types(value) do - if is_module_name?(value) do + if module_name?(value) do String.to_existing_atom("Elixir." <> value) else value @@ -373,8 +373,8 @@ defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do end end - @spec is_module_name?(String.t()) :: boolean() - def is_module_name?(string) do + @spec module_name?(String.t()) :: boolean() + def module_name?(string) do Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or string in ["Oban", "Ueberauth", "ExSyslogger"] end From aca6a7543ae97da2d1af8a6f9c547a0088d9e240 Mon Sep 17 00:00:00 2001 From: Steven Fuchs <steven.fuchs@dockyard.com> Date: Tue, 16 Jun 2020 13:18:29 +0000 Subject: [PATCH 295/375] Upgrade to Elixir 1.9 --- .gitlab-ci.yml | 2 +- elixir_buildpack.config | 4 ++-- mix.exs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aad28a2d8..bc7b289a2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: elixir:1.8.1 +image: elixir:1.9.4 variables: &global_variables POSTGRES_DB: pleroma_test diff --git a/elixir_buildpack.config b/elixir_buildpack.config index c23b08fb8..946408c12 100644 --- a/elixir_buildpack.config +++ b/elixir_buildpack.config @@ -1,2 +1,2 @@ -elixir_version=1.8.2 -erlang_version=21.3.7 +elixir_version=1.9.4 +erlang_version=22.3.4.1 diff --git a/mix.exs b/mix.exs index 03b060bc0..6040c994e 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ def project do [ app: :pleroma, version: version("2.0.50"), - elixir: "~> 1.8", + elixir: "~> 1.9", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())], From 3c2cee33adcd79a76b192a66f6f2d3772e2fda99 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 16 Jun 2020 17:50:33 +0300 Subject: [PATCH 296/375] moving custom ecto types in context folders --- lib/pleroma/config/config_db.ex | 6 +++--- .../activity_pub/object_validators}/date_time.ex | 6 +++++- .../activity_pub/object_validators}/object_id.ex | 6 +++++- .../activity_pub/object_validators}/recipients.ex | 8 ++++++-- .../activity_pub/object_validators}/safe_text.ex | 2 +- .../activity_pub/object_validators}/uri.ex | 6 +++++- .../{config/type => ecto_type/config}/atom.ex | 6 +++++- .../type => ecto_type/config}/binary_value.ex | 6 +++++- lib/pleroma/signature.ex | 4 ++-- lib/pleroma/user.ex | 4 ++-- lib/pleroma/web/activity_pub/object_validator.ex | 4 ++-- .../object_validators/announce_validator.ex | 14 +++++++------- .../object_validators/chat_message_validator.ex | 12 ++++++------ .../create_chat_message_validator.ex | 10 +++++----- .../object_validators/create_note_validator.ex | 6 +++--- .../object_validators/delete_validator.ex | 14 +++++++------- .../object_validators/emoji_react_validator.ex | 8 ++++---- .../object_validators/like_validator.ex | 14 +++++++------- .../object_validators/note_validator.ex | 12 ++++++------ .../object_validators/undo_validator.ex | 8 ++++---- .../object_validators/url_object_validator.ex | 8 ++++++-- lib/pleroma/web/activity_pub/transmogrifier.ex | 4 ++-- .../object_validators/types/date_time_test.exs | 2 +- .../object_validators/types/object_id_test.exs | 2 +- .../object_validators/types/recipients_test.exs | 2 +- .../object_validators/types/safe_text_test.exs | 2 +- 26 files changed, 102 insertions(+), 74 deletions(-) rename lib/pleroma/{web/activity_pub/object_validators/types => ecto_type/activity_pub/object_validators}/date_time.ex (77%) rename lib/pleroma/{web/activity_pub/object_validators/types => ecto_type/activity_pub/object_validators}/object_id.ex (69%) rename lib/pleroma/{web/activity_pub/object_validators/types => ecto_type/activity_pub/object_validators}/recipients.ex (64%) rename lib/pleroma/{web/activity_pub/object_validators/types => ecto_type/activity_pub/object_validators}/safe_text.ex (85%) rename lib/pleroma/{web/activity_pub/object_validators/types => ecto_type/activity_pub/object_validators}/uri.ex (63%) rename lib/pleroma/{config/type => ecto_type/config}/atom.ex (66%) rename lib/pleroma/{config/type => ecto_type/config}/binary_value.ex (65%) diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 30bd51b05..2f4eb8581 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -23,9 +23,9 @@ defmodule Pleroma.ConfigDB do ] schema "config" do - field(:key, Pleroma.Config.Type.Atom) - field(:group, Pleroma.Config.Type.Atom) - field(:value, Pleroma.Config.Type.BinaryValue) + field(:key, Pleroma.EctoType.Config.Atom) + field(:group, Pleroma.EctoType.Config.Atom) + field(:value, Pleroma.EctoType.Config.BinaryValue) field(:db, {:array, :string}, virtual: true, default: []) timestamps() diff --git a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex similarity index 77% rename from lib/pleroma/web/activity_pub/object_validators/types/date_time.ex rename to lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex index 4f412fcde..d852c0abd 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex @@ -1,4 +1,8 @@ -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime do @moduledoc """ The AP standard defines the date fields in AP as xsd:DateTime. Elixir's DateTime can't parse this, but it can parse the related iso8601. This diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex similarity index 69% rename from lib/pleroma/web/activity_pub/object_validators/types/object_id.ex rename to lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex index f71f76370..8034235b0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex @@ -1,4 +1,8 @@ -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID do use Ecto.Type def type, do: :string diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex similarity index 64% rename from lib/pleroma/web/activity_pub/object_validators/types/recipients.ex rename to lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex index 408e0f6ee..205527a96 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex @@ -1,7 +1,11 @@ -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients do use Ecto.Type - alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID + alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID def type, do: {:array, ObjectID} diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex similarity index 85% rename from lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex rename to lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex index 95c948123..7f0405c7b 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText do +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText do use Ecto.Type alias Pleroma.HTML diff --git a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex similarity index 63% rename from lib/pleroma/web/activity_pub/object_validators/types/uri.ex rename to lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex index 24845bcc0..2054c26be 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex @@ -1,4 +1,8 @@ -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Uri do use Ecto.Type def type, do: :string diff --git a/lib/pleroma/config/type/atom.ex b/lib/pleroma/ecto_type/config/atom.ex similarity index 66% rename from lib/pleroma/config/type/atom.ex rename to lib/pleroma/ecto_type/config/atom.ex index 387869284..df565d432 100644 --- a/lib/pleroma/config/type/atom.ex +++ b/lib/pleroma/ecto_type/config/atom.ex @@ -1,4 +1,8 @@ -defmodule Pleroma.Config.Type.Atom do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.Config.Atom do use Ecto.Type def type, do: :atom diff --git a/lib/pleroma/config/type/binary_value.ex b/lib/pleroma/ecto_type/config/binary_value.ex similarity index 65% rename from lib/pleroma/config/type/binary_value.ex rename to lib/pleroma/ecto_type/config/binary_value.ex index 17c5524a3..bbd2608c5 100644 --- a/lib/pleroma/config/type/binary_value.ex +++ b/lib/pleroma/ecto_type/config/binary_value.ex @@ -1,4 +1,8 @@ -defmodule Pleroma.Config.Type.BinaryValue do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.Config.BinaryValue do use Ecto.Type def type, do: :term diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index d01728361..3aa6909d2 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -5,10 +5,10 @@ defmodule Pleroma.Signature do @behaviour HTTPSignatures.Adapter + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Keys alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.ObjectValidators.Types def key_id_to_actor_id(key_id) do uri = @@ -24,7 +24,7 @@ def key_id_to_actor_id(key_id) do maybe_ap_id = URI.to_string(uri) - case Types.ObjectID.cast(maybe_ap_id) do + case ObjectValidators.ObjectID.cast(maybe_ap_id) do {:ok, ap_id} -> {:ok, ap_id} diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 52ac9052b..686ab0123 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -14,6 +14,7 @@ defmodule Pleroma.User do alias Pleroma.Config alias Pleroma.Conversation.Participation alias Pleroma.Delivery + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Emoji alias Pleroma.FollowingRelationship alias Pleroma.Formatter @@ -30,7 +31,6 @@ defmodule Pleroma.User do alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI @@ -115,7 +115,7 @@ defmodule Pleroma.User do field(:is_admin, :boolean, default: false) field(:show_role, :boolean, default: true) field(:settings, :map, default: nil) - field(:uri, Types.Uri, default: nil) + field(:uri, ObjectValidators.Uri, default: nil) field(:hide_followers_count, :boolean, default: false) field(:hide_follows_count, :boolean, default: false) field(:hide_followers, :boolean, default: false) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index c01c5f780..3d699e8a5 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do """ alias Pleroma.Object + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator @@ -17,7 +18,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @@ -120,7 +120,7 @@ def stringify_keys(object) when is_list(object) do def stringify_keys(object), do: object def fetch_actor(object) do - with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do + with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do User.get_or_fetch_by_ap_id(actor) end end diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex index 40f861f47..6f757f49c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object alias Pleroma.User - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -19,14 +19,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) + field(:id, ObjectValidators.ObjectID, primary_key: true) field(:type, :string) - field(:object, Types.ObjectID) - field(:actor, Types.ObjectID) + field(:object, ObjectValidators.ObjectID) + field(:actor, ObjectValidators.ObjectID) field(:context, :string, autogenerate: {Utils, :generate_context_id, []}) - field(:to, Types.Recipients, default: []) - field(:cc, Types.Recipients, default: []) - field(:published, Types.DateTime) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:published, ObjectValidators.DateTime) end def cast_and_validate(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 138736f23..c481d79e0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] @@ -16,12 +16,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do @derive Jason.Encoder embedded_schema do - field(:id, Types.ObjectID, primary_key: true) - field(:to, Types.Recipients, default: []) + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:to, ObjectValidators.Recipients, default: []) field(:type, :string) - field(:content, Types.SafeText) - field(:actor, Types.ObjectID) - field(:published, Types.DateTime) + field(:content, ObjectValidators.SafeText) + field(:actor, ObjectValidators.ObjectID) + field(:published, ObjectValidators.DateTime) field(:emoji, :map, default: %{}) embeds_one(:attachment, AttachmentValidator) diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index fc582400b..7269f9ff0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -7,9 +7,9 @@ # - doesn't embed, will only get the object id defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -17,11 +17,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) - field(:actor, Types.ObjectID) + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:actor, ObjectValidators.ObjectID) field(:type, :string) - field(:to, Types.Recipients, default: []) - field(:object, Types.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:object, ObjectValidators.ObjectID) end def cast_and_apply(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex index 926804ce7..316bd0c07 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex @@ -5,16 +5,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) - field(:actor, Types.ObjectID) + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:actor, ObjectValidators.ObjectID) field(:type, :string) field(:to, {:array, :string}) field(:cc, {:array, :string}) diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index e5d08eb5c..93a7b0e0b 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.User - alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -15,13 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) + field(:id, ObjectValidators.ObjectID, primary_key: true) field(:type, :string) - field(:actor, Types.ObjectID) - field(:to, Types.Recipients, default: []) - field(:cc, Types.Recipients, default: []) - field(:deleted_activity_id, Types.ObjectID) - field(:object, Types.ObjectID) + field(:actor, ObjectValidators.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:deleted_activity_id, ObjectValidators.ObjectID) + field(:object, ObjectValidators.ObjectID) end def cast_data(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index e87519c59..a543af1f8 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ObjectValidators.Types import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) + field(:id, ObjectValidators.ObjectID, primary_key: true) field(:type, :string) - field(:object, Types.ObjectID) - field(:actor, Types.ObjectID) + field(:object, ObjectValidators.ObjectID) + field(:actor, ObjectValidators.ObjectID) field(:context, :string) field(:content, :string) field(:to, {:array, :string}, default: []) diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex index 034f25492..493e4c247 100644 --- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.Utils import Ecto.Changeset @@ -15,13 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) + field(:id, ObjectValidators.ObjectID, primary_key: true) field(:type, :string) - field(:object, Types.ObjectID) - field(:actor, Types.ObjectID) + field(:object, ObjectValidators.ObjectID) + field(:actor, ObjectValidators.ObjectID) field(:context, :string) - field(:to, Types.Recipients, default: []) - field(:cc, Types.Recipients, default: []) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) end def cast_and_validate(data) do @@ -67,7 +67,7 @@ def fix_recipients(cng) do with {[], []} <- {to, cc}, %Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object), - {:ok, actor} <- Types.ObjectID.cast(actor) do + {:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do cng |> put_change(:to, [actor]) else diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index 462a5620a..a10728ac6 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -5,14 +5,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do use Ecto.Schema - alias Pleroma.Web.ActivityPub.ObjectValidators.Types + alias Pleroma.EctoType.ActivityPub.ObjectValidators import Ecto.Changeset @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) + field(:id, ObjectValidators.ObjectID, primary_key: true) field(:to, {:array, :string}, default: []) field(:cc, {:array, :string}, default: []) field(:bto, {:array, :string}, default: []) @@ -22,10 +22,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:type, :string) field(:content, :string) field(:context, :string) - field(:actor, Types.ObjectID) - field(:attributedTo, Types.ObjectID) + field(:actor, ObjectValidators.ObjectID) + field(:attributedTo, ObjectValidators.ObjectID) field(:summary, :string) - field(:published, Types.DateTime) + field(:published, ObjectValidators.DateTime) # TODO: Write type field(:emoji, :map, default: %{}) field(:sensitive, :boolean, default: false) @@ -35,7 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:inRepyTo, :string) - field(:uri, Types.Uri) + field(:uri, ObjectValidators.Uri) field(:likes, {:array, :string}, default: []) field(:announcements, {:array, :string}, default: []) diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex index d0ba418e8..e8d2d39c1 100644 --- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do use Ecto.Schema alias Pleroma.Activity - alias Pleroma.Web.ActivityPub.ObjectValidators.Types + alias Pleroma.EctoType.ActivityPub.ObjectValidators import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do @primary_key false embedded_schema do - field(:id, Types.ObjectID, primary_key: true) + field(:id, ObjectValidators.ObjectID, primary_key: true) field(:type, :string) - field(:object, Types.ObjectID) - field(:actor, Types.ObjectID) + field(:object, ObjectValidators.ObjectID) + field(:actor, ObjectValidators.ObjectID) field(:to, {:array, :string}, default: []) field(:cc, {:array, :string}, default: []) end diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex index 47e231150..f64fac46d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex @@ -1,14 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do use Ecto.Schema - alias Pleroma.Web.ActivityPub.ObjectValidators.Types + alias Pleroma.EctoType.ActivityPub.ObjectValidators import Ecto.Changeset @primary_key false embedded_schema do field(:type, :string) - field(:href, Types.Uri) + field(:href, ObjectValidators.Uri) field(:mediaType, :string) end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 985921aa0..851f474b8 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do """ alias Pleroma.Activity alias Pleroma.EarmarkRenderer + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.FollowingRelationship alias Pleroma.Maps alias Pleroma.Notification @@ -18,7 +19,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -725,7 +725,7 @@ def handle_incoming( else {:error, {:validate_object, _}} = e -> # Check if we have a create activity for this - with {:ok, object_id} <- Types.ObjectID.cast(data["object"]), + with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]), %Activity{data: %{"actor" => actor}} <- Activity.create_by_object_ap_id(object_id) |> Repo.one(), # We have one, insert a tombstone and retry diff --git a/test/web/activity_pub/object_validators/types/date_time_test.exs b/test/web/activity_pub/object_validators/types/date_time_test.exs index 3e17a9497..43be8e936 100644 --- a/test/web/activity_pub/object_validators/types/date_time_test.exs +++ b/test/web/activity_pub/object_validators/types/date_time_test.exs @@ -1,5 +1,5 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTimeTest do - alias Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime + alias Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime use Pleroma.DataCase test "it validates an xsd:Datetime" do diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs index c8911948e..e0ab76379 100644 --- a/test/web/activity_pub/object_validators/types/object_id_test.exs +++ b/test/web/activity_pub/object_validators/types/object_id_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do - alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID + alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID use Pleroma.DataCase @uris [ diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs index f278f039b..053916bdd 100644 --- a/test/web/activity_pub/object_validators/types/recipients_test.exs +++ b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -1,5 +1,5 @@ defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do - alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients use Pleroma.DataCase test "it asserts that all elements of the list are object ids" do diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs index d4a574554..9c08606f6 100644 --- a/test/web/activity_pub/object_validators/types/safe_text_test.exs +++ b/test/web/activity_pub/object_validators/types/safe_text_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeTextTest do use Pleroma.DataCase - alias Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText + alias Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText test "it lets normal text go through" do text = "hey how are you" From ed189568f3c2c6fc6ae9ba4d676e95902b3019d1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 21 Mar 2020 09:47:05 +0300 Subject: [PATCH 297/375] moving mrf settings from instance to separate group --- CHANGELOG.md | 5 +- config/config.exs | 8 ++- config/description.exs | 64 +++++++++++-------- docs/configuration/cheatsheet.md | 45 +++++++------ docs/configuration/mrf.md | 14 ++-- lib/pleroma/config/config_db.ex | 8 ++- lib/pleroma/config/deprecation_warnings.ex | 44 +++++++++++++ lib/pleroma/web/activity_pub/mrf.ex | 4 +- .../web/activity_pub/mrf/simple_policy.ex | 28 ++++---- .../web/mastodon_api/views/instance_view.ex | 2 +- ...rf_config_move_from_instance_namespace.exs | 39 +++++++++++ test/config/deprecation_warnings_test.exs | 57 +++++++++++++++++ test/tasks/config_test.exs | 5 +- test/web/activity_pub/mrf/mrf_test.exs | 4 +- test/web/federator_test.exs | 4 +- test/web/node_info_test.exs | 46 ++++--------- 16 files changed, 256 insertions(+), 121 deletions(-) create mode 100644 priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs create mode 100644 test/config/deprecation_warnings_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index d2629bf84..12e8d58e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,13 @@ 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 - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard +- Configuration: `rewrite_policy` renamed to `policies` and moved from `instance` to `mrf` group. Old config namespace is deprecated. +- Configuration: `mrf_transparency` renamed to `transparency` and moved from `instance` to `mrf` group. Old config namespace is deprecated. +- Configuration: `mrf_transparency_exclusions` renamed to `transparency_exclusions` and moved from `instance` to `mrf` group. Old config namespace is deprecated. + <details> <summary>API Changes</summary> - **Breaking:** Emoji API: changed methods and renamed routes. diff --git a/config/config.exs b/config/config.exs index 6a7bb9e06..3d6336a66 100644 --- a/config/config.exs +++ b/config/config.exs @@ -209,7 +209,6 @@ Pleroma.Web.ActivityPub.Publisher ], allow_relay: true, - rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, quarantined_instances: [], managed_config: true, @@ -220,8 +219,6 @@ "text/markdown", "text/bbcode" ], - mrf_transparency: true, - mrf_transparency_exclusions: [], autofollowed_nicknames: [], max_pinned_statuses: 1, attachment_links: false, @@ -685,6 +682,11 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false +config :pleroma, :mrf, + policies: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, + transparency: true, + transparency_exclusions: [] + # 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 b21d7840c..2ab95e5ab 100644 --- a/config/description.exs +++ b/config/description.exs @@ -689,17 +689,6 @@ type: :boolean, description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance" }, - %{ - key: :rewrite_policy, - type: [:module, {:list, :module}], - description: - "A list of enabled MRF policies. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.", - suggestions: - Generator.list_modules_in_dir( - "lib/pleroma/web/activity_pub/mrf", - "Elixir.Pleroma.Web.ActivityPub.MRF." - ) - }, %{ key: :public, type: :boolean, @@ -742,23 +731,6 @@ "text/bbcode" ] }, - %{ - key: :mrf_transparency, - label: "MRF transparency", - type: :boolean, - description: - "Make the content of your Message Rewrite Facility settings public (via nodeinfo)" - }, - %{ - key: :mrf_transparency_exclusions, - label: "MRF transparency exclusions", - type: {:list, :string}, - description: - "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.", - suggestions: [ - "exclusion.com" - ] - }, %{ key: :extended_nickname_format, type: :boolean, @@ -3325,5 +3297,41 @@ suggestions: [false] } ] + }, + %{ + group: :pleroma, + key: :mrf, + type: :group, + description: "General MRF settings", + children: [ + %{ + key: :policies, + type: [:module, {:list, :module}], + description: + "A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.", + suggestions: + Generator.list_modules_in_dir( + "lib/pleroma/web/activity_pub/mrf", + "Elixir.Pleroma.Web.ActivityPub.MRF." + ) + }, + %{ + key: :transparency, + label: "MRF transparency", + type: :boolean, + description: + "Make the content of your Message Rewrite Facility settings public (via nodeinfo)" + }, + %{ + key: :transparency_exclusions, + label: "MRF transparency exclusions", + type: {:list, :string}, + description: + "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.", + suggestions: [ + "exclusion.com" + ] + } + ] } ] diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index fad67fc4d..e9af604e2 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -36,26 +36,10 @@ To add configuration to your config file, you can copy it from the base config. * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes. * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance. -* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: - * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). - * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. - * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)). - * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). - * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). - * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). - * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. - * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. - * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. - * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). - * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). - * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). - * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)). * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). -* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). -* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. * `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with older software for theses nicknames. * `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature. @@ -78,11 +62,30 @@ To add configuration to your config file, you can copy it from the base config. * `external_user_synchronization`: Enabling following/followers counters synchronization for external users. * `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances. +## Message rewrite facility + +### :mrf +* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default: + * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). + * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. + * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). + * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). + * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). + * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). + * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. + * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. + * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. + * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). + * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). + * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). +* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). +* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. + ## Federation ### MRF policies !!! note - Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `rewrite_policy` under [:instance](#instance) section. + Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `policies` under [:mrf](#mrf) section. #### :mrf_simple * `media_removal`: List of instances to remove media from. @@ -969,13 +972,13 @@ config :pleroma, :database_config_whitelist, [ Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. -* `timelines` - public and federated timelines - * `local` - public timeline +* `timelines`: public and federated timelines + * `local`: public timeline * `federated` -* `profiles` - user profiles +* `profiles`: user profiles * `local` * `remote` -* `activities` - statuses +* `activities`: statuses * `local` * `remote` diff --git a/docs/configuration/mrf.md b/docs/configuration/mrf.md index d48d0cc99..31c66e098 100644 --- a/docs/configuration/mrf.md +++ b/docs/configuration/mrf.md @@ -34,9 +34,9 @@ config :pleroma, :instance, To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this: ```elixir -config :pleroma, :instance, +config :pleroma, :mrf, [...] - rewrite_policy: Pleroma.Web.ActivityPub.MRF.SimplePolicy + policies: Pleroma.Web.ActivityPub.MRF.SimplePolicy ``` Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are: @@ -58,8 +58,8 @@ Servers should be configured as lists. This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`: ```elixir -config :pleroma, :instance, - rewrite_policy: [Pleroma.Web.ActivityPub.MRF.SimplePolicy] +config :pleroma, :mrf, + policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy] config :pleroma, :mrf_simple, media_removal: ["illegalporn.biz"], @@ -75,7 +75,7 @@ The effects of MRF policies can be very drastic. It is important to use this fun ## Writing your own MRF Policy -As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `rewrite_policy` config setting. +As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `policies` config setting. For example, here is a sample policy module which rewrites all messages to "new message content": @@ -125,8 +125,8 @@ end If you save this file as `lib/pleroma/web/activity_pub/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so: ```elixir -config :pleroma, :instance, - rewrite_policy: [ +config :pleroma, :mrf, + policies: [ Pleroma.Web.ActivityPub.MRF.SimplePolicy, Pleroma.Web.ActivityPub.MRF.RewritePolicy ] diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 30bd51b05..c0f3fe888 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -54,13 +54,13 @@ def changeset(config, params \\ %{}) do defp create(params) do %ConfigDB{} - |> changeset(params) + |> changeset(params, transform?) |> Repo.insert() end defp update(%ConfigDB{} = config, %{value: value}) do config - |> changeset(%{value: value}) + |> changeset(%{value: value}, transform?) |> Repo.update() end @@ -167,7 +167,9 @@ defp only_full_update?(%ConfigDB{group: group, key: key}) do end) end - @spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} + @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} + def delete(%ConfigDB{} = config), do: Repo.delete(config) + def delete(params) do search_opts = Map.delete(params, :subkeys) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index b68ded01f..0a6c724fb 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -3,9 +3,23 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.DeprecationWarnings do + alias Pleroma.Config + require Logger alias Pleroma.Config + @type config_namespace() :: [atom()] + @type config_map() :: {config_namespace(), config_namespace(), String.t()} + + @mrf_config_map [ + {[:instance, :rewrite_policy], [:mrf, :policies], + "\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"}, + {[:instance, :mrf_transparency], [:mrf, :transparency], + "\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"}, + {[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions], + "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"} + ] + def check_hellthread_threshold do if Config.get([:mrf_hellthread, :threshold]) do Logger.warn(""" @@ -39,5 +53,35 @@ def mrf_user_allowlist do def warn do check_hellthread_threshold() mrf_user_allowlist() + check_old_mrf_config() + end + + def check_old_mrf_config do + warning_preface = """ + !!!DEPRECATION WARNING!!! + Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: + """ + + move_namespace_and_warn(@mrf_config_map, warning_preface) + end + + @spec move_namespace_and_warn([config_map()], String.t()) :: :ok + def move_namespace_and_warn(config_map, warning_preface) do + warning = + Enum.reduce(config_map, "", fn + {old, new, err_msg}, acc -> + old_config = Config.get(old) + + if old_config do + Config.put(new, old_config) + acc <> err_msg + else + acc + end + end) + + if warning != "" do + Logger.warn(warning_preface <> warning) + end end end diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 5a4a76085..206d6af52 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -16,7 +16,7 @@ def filter(policies, %{} = object) do def filter(%{} = object), do: get_policies() |> filter(object) def get_policies do - Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies() + Pleroma.Config.get([:mrf, :policies], []) |> get_policies() end defp get_policies(policy) when is_atom(policy), do: [policy] @@ -51,7 +51,7 @@ def describe(policies) do get_policies() |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) - exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions]) + exclusions = Pleroma.Config.get([:mrf, :transparency_exclusions]) base = %{ diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index b7dcb1b86..9cea6bcf9 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -3,21 +3,23 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do - alias Pleroma.User - alias Pleroma.Web.ActivityPub.MRF @moduledoc "Filter activities depending on their origin instance" @behaviour Pleroma.Web.ActivityPub.MRF + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.ActivityPub.MRF + require Pleroma.Constants defp check_accept(%{host: actor_host} = _actor_info, object) do accepts = - Pleroma.Config.get([:mrf_simple, :accept]) + Config.get([:mrf_simple, :accept]) |> MRF.subdomains_regex() cond do accepts == [] -> {:ok, object} - actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} + actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} true -> {:reject, nil} end @@ -25,7 +27,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do defp check_reject(%{host: actor_host} = _actor_info, object) do rejects = - Pleroma.Config.get([:mrf_simple, :reject]) + Config.get([:mrf_simple, :reject]) |> MRF.subdomains_regex() if MRF.subdomain_match?(rejects, actor_host) do @@ -41,7 +43,7 @@ defp check_media_removal( ) when length(child_attachment) > 0 do media_removal = - Pleroma.Config.get([:mrf_simple, :media_removal]) + Config.get([:mrf_simple, :media_removal]) |> MRF.subdomains_regex() object = @@ -65,7 +67,7 @@ defp check_media_nsfw( } = object ) do media_nsfw = - Pleroma.Config.get([:mrf_simple, :media_nsfw]) + Config.get([:mrf_simple, :media_nsfw]) |> MRF.subdomains_regex() object = @@ -85,7 +87,7 @@ defp check_media_nsfw(_actor_info, object), do: {:ok, object} defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do timeline_removal = - Pleroma.Config.get([:mrf_simple, :federated_timeline_removal]) + Config.get([:mrf_simple, :federated_timeline_removal]) |> MRF.subdomains_regex() object = @@ -108,7 +110,7 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do report_removal = - Pleroma.Config.get([:mrf_simple, :report_removal]) + Config.get([:mrf_simple, :report_removal]) |> MRF.subdomains_regex() if MRF.subdomain_match?(report_removal, actor_host) do @@ -122,7 +124,7 @@ defp check_report_removal(_actor_info, object), do: {:ok, object} defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do avatar_removal = - Pleroma.Config.get([:mrf_simple, :avatar_removal]) + Config.get([:mrf_simple, :avatar_removal]) |> MRF.subdomains_regex() if MRF.subdomain_match?(avatar_removal, actor_host) do @@ -136,7 +138,7 @@ defp check_avatar_removal(_actor_info, object), do: {:ok, object} defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do banner_removal = - Pleroma.Config.get([:mrf_simple, :banner_removal]) + Config.get([:mrf_simple, :banner_removal]) |> MRF.subdomains_regex() if MRF.subdomain_match?(banner_removal, actor_host) do @@ -197,10 +199,10 @@ def filter(object), do: {:ok, object} @impl true def describe do - exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions]) + exclusions = Config.get([:mrf, :transparency_exclusions]) mrf_simple = - Pleroma.Config.get(:mrf_simple) + Config.get(:mrf_simple) |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) |> Enum.into(%{}) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index c498fe632..4f0ae4e8f 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -78,7 +78,7 @@ def features do def federation do quarantined = Config.get([:instance, :quarantined_instances], []) - if Config.get([:instance, :mrf_transparency]) do + if Config.get([:mrf, :transparency]) do {:ok, data} = MRF.describe() data diff --git a/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs b/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs new file mode 100644 index 000000000..6f6094613 --- /dev/null +++ b/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs @@ -0,0 +1,39 @@ +defmodule Pleroma.Repo.Migrations.MrfConfigMoveFromInstanceNamespace do + use Ecto.Migration + + alias Pleroma.ConfigDB + + @old_keys [:rewrite_policy, :mrf_transparency, :mrf_transparency_exclusions] + def change do + config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":instance"}) + + if config do + old_instance = ConfigDB.from_binary(config.value) + + mrf = + old_instance + |> Keyword.take(@old_keys) + |> Keyword.new(fn + {:rewrite_policy, policies} -> {:policies, policies} + {:mrf_transparency, transparency} -> {:transparency, transparency} + {:mrf_transparency_exclusions, exclusions} -> {:transparency_exclusions, exclusions} + end) + + if mrf != [] do + {:ok, _} = + ConfigDB.create( + %{group: ":pleroma", key: ":mrf", value: ConfigDB.to_binary(mrf)}, + false + ) + + new_instance = Keyword.drop(old_instance, @old_keys) + + if new_instance != [] do + {:ok, _} = ConfigDB.update(config, %{value: ConfigDB.to_binary(new_instance)}, false) + else + {:ok, _} = ConfigDB.delete(config) + end + end + end + end +end diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs new file mode 100644 index 000000000..548ee87b0 --- /dev/null +++ b/test/config/deprecation_warnings_test.exs @@ -0,0 +1,57 @@ +defmodule Pleroma.Config.DeprecationWarningsTest do + use ExUnit.Case, async: true + use Pleroma.Tests.Helpers + + import ExUnit.CaptureLog + + test "check_old_mrf_config/0" do + clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) + clear_config([:instance, :mrf_transparency], true) + clear_config([:instance, :mrf_transparency_exclusions], []) + + assert capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_old_mrf_config() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: + + * `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies` + * `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency` + * `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions` + """ + end + + test "move_namespace_and_warn/2" do + old_group1 = [:group, :key] + old_group2 = [:group, :key2] + old_group3 = [:group, :key3] + + new_group1 = [:another_group, :key4] + new_group2 = [:another_group, :key5] + new_group3 = [:another_group, :key6] + + clear_config(old_group1, 1) + clear_config(old_group2, 2) + clear_config(old_group3, 3) + + clear_config(new_group1) + clear_config(new_group2) + clear_config(new_group3) + + config_map = [ + {old_group1, new_group1, "\n error :key"}, + {old_group2, new_group2, "\n error :key2"}, + {old_group3, new_group3, "\n error :key3"} + ] + + assert capture_log(fn -> + Pleroma.Config.DeprecationWarnings.move_namespace_and_warn( + config_map, + "Warning preface" + ) + end) =~ "Warning preface\n error :key\n error :key2\n error :key3" + + assert Pleroma.Config.get(new_group1) == 1 + assert Pleroma.Config.get(new_group2) == 2 + assert Pleroma.Config.get(new_group3) == 3 + end +end diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index e1bddfebf..bae171074 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -120,14 +120,11 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil federation_reachability_timeout_days: 7, federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], allow_relay: true, - rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, quarantined_instances: [], managed_config: true, static_dir: "instance/static/", allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], - mrf_transparency: true, - mrf_transparency_exclusions: [], autofollowed_nicknames: [], max_pinned_statuses: 1, attachment_links: false, @@ -174,7 +171,7 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil end assert file == - "#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" + "#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" end end end diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs index c941066f2..a63b25423 100644 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ b/test/web/activity_pub/mrf/mrf_test.exs @@ -60,8 +60,6 @@ test "matches are case-insensitive" do end describe "describe/0" do - setup do: clear_config([:instance, :rewrite_policy]) - test "it works as expected with noop policy" do expected = %{ mrf_policies: ["NoOpPolicy"], @@ -72,7 +70,7 @@ test "it works as expected with noop policy" do end test "it works as expected with mock policy" do - Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock]) + clear_config([:mrf, :policies], [MRFModuleMock]) expected = %{ mrf_policies: ["MRFModuleMock"], diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index de90aa6e0..592fdccd1 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -23,7 +23,7 @@ defmodule Pleroma.Web.FederatorTest do 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, :policies]) setup do: clear_config([:mrf_keyword]) describe "Publish an activity" do @@ -158,7 +158,7 @@ test "it does not crash if MRF rejects the post" do Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) Pleroma.Config.put( - [:instance, :rewrite_policy], + [:mrf, :policies], Pleroma.Web.ActivityPub.MRF.KeywordPolicy ) diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index 00925caad..06b33607f 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -67,10 +67,10 @@ test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do end test "returns fieldsLimits field", %{conn: conn} do - Config.put([:instance, :max_account_fields], 10) - Config.put([:instance, :max_remote_account_fields], 15) - Config.put([:instance, :account_field_name_length], 255) - Config.put([:instance, :account_field_value_length], 2048) + clear_config([:instance, :max_account_fields], 10) + clear_config([:instance, :max_remote_account_fields], 15) + clear_config([:instance, :account_field_name_length], 255) + clear_config([:instance, :account_field_value_length], 2048) response = conn @@ -84,8 +84,7 @@ test "returns fieldsLimits field", %{conn: conn} do end test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do - option = Config.get([:instance, :safe_dm_mentions]) - Config.put([:instance, :safe_dm_mentions], true) + clear_config([:instance, :safe_dm_mentions], true) response = conn @@ -102,8 +101,6 @@ test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do |> json_response(:ok) refute "safe_dm_mentions" in response["metadata"]["features"] - - Config.put([:instance, :safe_dm_mentions], option) end describe "`metadata/federation/enabled`" do @@ -156,14 +153,11 @@ test "it shows default features flags", %{conn: conn} do end test "it shows MRF transparency data if enabled", %{conn: conn} do - config = Config.get([:instance, :rewrite_policy]) - Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - - option = Config.get([:instance, :mrf_transparency]) - Config.put([:instance, :mrf_transparency], true) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) simple_config = %{"reject" => ["example.com"]} - Config.put(:mrf_simple, simple_config) + clear_config(:mrf_simple, simple_config) response = conn @@ -171,26 +165,17 @@ test "it shows MRF transparency data if enabled", %{conn: conn} do |> json_response(:ok) assert response["metadata"]["federation"]["mrf_simple"] == simple_config - - Config.put([:instance, :rewrite_policy], config) - Config.put([:instance, :mrf_transparency], option) - Config.put(:mrf_simple, %{}) end test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do - config = Config.get([:instance, :rewrite_policy]) - Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - - option = Config.get([:instance, :mrf_transparency]) - Config.put([:instance, :mrf_transparency], true) - - exclusions = Config.get([:instance, :mrf_transparency_exclusions]) - Config.put([:instance, :mrf_transparency_exclusions], ["other.site"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) + clear_config([:mrf, :transparency_exclusions], ["other.site"]) simple_config = %{"reject" => ["example.com", "other.site"]} - expected_config = %{"reject" => ["example.com"]} + clear_config(:mrf_simple, simple_config) - Config.put(:mrf_simple, simple_config) + expected_config = %{"reject" => ["example.com"]} response = conn @@ -199,10 +184,5 @@ test "it performs exclusions from MRF transparency data if configured", %{conn: assert response["metadata"]["federation"]["mrf_simple"] == expected_config assert response["metadata"]["federation"]["exclusions"] == true - - Config.put([:instance, :rewrite_policy], config) - Config.put([:instance, :mrf_transparency], option) - Config.put([:instance, :mrf_transparency_exclusions], exclusions) - Config.put(:mrf_simple, %{}) end end From b66e6eb521c2901da119179016c99751cb5e6f95 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 16 Jun 2020 19:03:45 +0300 Subject: [PATCH 298/375] fixes for tests --- docs/configuration/storing_remote_media.md | 4 ++-- lib/pleroma/config/config_db.ex | 4 ++-- test/web/activity_pub/activity_pub_test.exs | 4 ++-- test/workers/cron/purge_expired_activities_worker_test.exs | 6 +----- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/configuration/storing_remote_media.md b/docs/configuration/storing_remote_media.md index 7e91fe7d9..c01985d25 100644 --- a/docs/configuration/storing_remote_media.md +++ b/docs/configuration/storing_remote_media.md @@ -33,6 +33,6 @@ as soon as the post is received by your instance. Add to your `prod.secret.exs`: ``` -config :pleroma, :instance, - rewrite_policy: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy] +config :pleroma, :mrf, + policies: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy] ``` diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index c0f3fe888..134116863 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -54,13 +54,13 @@ def changeset(config, params \\ %{}) do defp create(params) do %ConfigDB{} - |> changeset(params, transform?) + |> changeset(params) |> Repo.insert() end defp update(%ConfigDB{} = config, %{value: value}) do config - |> changeset(%{value: value}, transform?) + |> changeset(%{value: value}) |> Repo.update() end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 7693f6400..1c684df1a 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -2055,11 +2055,11 @@ test "it just returns the input if the user has no following/follower addresses" end describe "global activity expiration" do - setup do: clear_config([:instance, :rewrite_policy]) + setup do: clear_config([:mrf, :policies]) test "creates an activity expiration for local Create activities" do Pleroma.Config.put( - [:instance, :rewrite_policy], + [:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy ) diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs index 6d2991a60..b1db59fdf 100644 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ b/test/workers/cron/purge_expired_activities_worker_test.exs @@ -13,7 +13,6 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do setup do clear_config([ActivityExpiration, :enabled]) - clear_config([:instance, :rewrite_policy]) end test "deletes an expiration activity" do @@ -42,10 +41,7 @@ test "deletes an expiration activity" do test "works with ActivityExpirationPolicy" do Pleroma.Config.put([ActivityExpiration, :enabled], true) - Pleroma.Config.put( - [:instance, :rewrite_policy], - Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy - ) + clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) user = insert(:user) From 5c0e1039ce41a2717598992a590658d4d079451c Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Tue, 16 Jun 2020 23:45:59 +0300 Subject: [PATCH 299/375] Chunk the notification type backfill migration Long-term we want that migration to be done entirely in SQL, but for now this is a hotfix to not cause OOMs on large databases. This is using a homegrown version of `Repo.stream`, it's worse in terms of performance than the upstream since it doesn't use the same prepared query for chunk queries, but unlike the upstream it supports preloads. --- .../migration_helper/notification_backfill.ex | 2 +- lib/pleroma/repo.ex | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex index 09647d12a..b3770307a 100644 --- a/lib/pleroma/migration_helper/notification_backfill.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -18,7 +18,7 @@ def fill_in_notification_types do ) query - |> Repo.all() + |> Repo.chunk_stream(100) |> Enum.each(fn notification -> type = notification.activity diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index f62138466..6d85d70bc 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Repo do adapter: Ecto.Adapters.Postgres, migration_timestamps: [type: :naive_datetime_usec] + import Ecto.Query require Logger defmodule Instrumenter do @@ -78,6 +79,33 @@ def check_migrations_applied!() do :ok end end + + def chunk_stream(query, chunk_size) do + # We don't actually need start and end funcitons of resource streaming, + # but it seems to be the only way to not fetch records one-by-one and + # have individual records be the elements of the stream, instead of + # lists of records + Stream.resource( + fn -> 0 end, + fn + last_id -> + query + |> order_by(asc: :id) + |> where([r], r.id > ^last_id) + |> limit(^chunk_size) + |> all() + |> case do + [] -> + {:halt, last_id} + + records -> + last_id = List.last(records).id + {records, last_id} + end + end, + fn _ -> :ok end + ) + end end defmodule Pleroma.Repo.UnappliedMigrationsError do From 55d8263c0040b32075b9bb90ab1d6693e627f6bf Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 17 Jun 2020 02:27:28 +0300 Subject: [PATCH 300/375] Update OTP releases to official images of 1.10.3 This is necessary since we bumped required version of elixir to 1.9. The dlsym bug should be gone by now. --- .gitlab-ci.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc7b289a2..b4bd59b43 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -170,8 +170,7 @@ stop_review_app: amd64: stage: release - # TODO: Replace with upstream image when 1.9.0 comes out - image: rinpatch/elixir:1.9.0-rc.0 + image: elixir:1.10.3 only: &release-only - stable@pleroma/pleroma - develop@pleroma/pleroma @@ -208,8 +207,7 @@ amd64-musl: stage: release artifacts: *release-artifacts only: *release-only - # TODO: Replace with upstream image when 1.9.0 comes out - image: rinpatch/elixir:1.9.0-rc.0-alpine + image: elixir:1.10.3-alpine cache: *release-cache variables: *release-variables before_script: &before-release-musl @@ -225,8 +223,7 @@ arm: only: *release-only tags: - arm32 - # TODO: Replace with upstream image when 1.9.0 comes out - image: rinpatch/elixir:1.9.0-rc.0-arm + image: elixir:1.10.3 cache: *release-cache variables: *release-variables before_script: *before-release @@ -238,8 +235,7 @@ arm-musl: only: *release-only tags: - arm32 - # TODO: Replace with upstream image when 1.9.0 comes out - image: rinpatch/elixir:1.9.0-rc.0-arm-alpine + image: elixir:1.10.3-alpine cache: *release-cache variables: *release-variables before_script: *before-release-musl @@ -251,8 +247,7 @@ arm64: only: *release-only tags: - arm - # TODO: Replace with upstream image when 1.9.0 comes out - image: rinpatch/elixir:1.9.0-rc.0-arm64 + image: elixir:1.10.3 cache: *release-cache variables: *release-variables before_script: *before-release @@ -265,7 +260,7 @@ arm64-musl: tags: - arm # TODO: Replace with upstream image when 1.9.0 comes out - image: rinpatch/elixir:1.9.0-rc.0-arm64-alpine + image: elixir:1.10.3-alpine cache: *release-cache variables: *release-variables before_script: *before-release-musl From 281ecd6b30a165843c5b6a1899894646dc25c0f9 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 17 Jun 2020 02:29:32 +0300 Subject: [PATCH 301/375] CHANGELOG.md: mention minimal elixir version update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f291ad2a..3ee13904f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Changed +- **Breaking:** Elixir >=1.9 is now required (was >= 1.8) - In Conversations, return only direct messages as `last_status` - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard From 02a5648febb8a508116c29e2271e1ade2ffafb2d Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Wed, 17 Jun 2020 09:15:35 +0300 Subject: [PATCH 302/375] fixed migration the settings to DB --- lib/mix/tasks/pleroma/config.ex | 1 + lib/pleroma/config/loader.ex | 1 - ...510135645_add_fts_index_to_objects_two.exs | 33 +++++++++++-------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index f1b3a8766..65691f9c1 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -52,6 +52,7 @@ def migrate_to_db(file_path \\ nil) do defp do_migrate_to_db(config_file) do if File.exists?(config_file) do + shell_info("Running migrate settings from file: #{Path.expand(config_file)}") Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 0f3ecf1ed..76559e70c 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Config.Loader do Pleroma.Web.Endpoint, :env, :configurable_from_database, - :database, :swarm ] diff --git a/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs b/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs index 6227769dc..79bde163d 100644 --- a/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs +++ b/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs @@ -2,24 +2,29 @@ defmodule Pleroma.Repo.Migrations.AddFtsIndexToObjectsTwo do use Ecto.Migration def up do - execute("create extension if not exists rum") - drop_if_exists index(:objects, ["(to_tsvector('english', data->>'content'))"], using: :gin, name: :objects_fts) - alter table(:objects) do - add(:fts_content, :tsvector) - end + if Pleroma.Config.get([:database, :rum_enabled]) do + execute("create extension if not exists rum") + drop_if_exists index(:objects, ["(to_tsvector('english', data->>'content'))"], using: :gin, name: :objects_fts) + alter table(:objects) do + add(:fts_content, :tsvector) + end - execute("CREATE FUNCTION objects_fts_update() RETURNS trigger AS $$ - begin + execute("CREATE FUNCTION objects_fts_update() RETURNS trigger AS $$ + begin new.fts_content := to_tsvector('english', new.data->>'content'); return new; + end + $$ LANGUAGE plpgsql") + execute("create index if not exists objects_fts on objects using RUM (fts_content rum_tsvector_addon_ops, inserted_at) with (attach = 'inserted_at', to = 'fts_content');") + + execute("CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON objects + FOR EACH ROW EXECUTE PROCEDURE objects_fts_update()") + + execute("UPDATE objects SET updated_at = NOW()") + else + raise Ecto.MigrationError, + message: "Migration is not allowed. You can change this behavior by setting `database/rum_enabled` to true." end - $$ LANGUAGE plpgsql") - execute("create index if not exists objects_fts on objects using RUM (fts_content rum_tsvector_addon_ops, inserted_at) with (attach = 'inserted_at', to = 'fts_content');") - - execute("CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON objects - FOR EACH ROW EXECUTE PROCEDURE objects_fts_update()") - - execute("UPDATE objects SET updated_at = NOW()") end def down do From a77b0388f48084bd3a420855a232bf2e504f0bce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 17 Jun 2020 10:31:06 +0300 Subject: [PATCH 303/375] credo fix --- lib/pleroma/web/activity_pub/object_validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 3d699e8a5..6a83a2c33 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -9,8 +9,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do the system. """ - alias Pleroma.Object alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator From abda3f2d92eda0888e018cea0a2fffb21d9e0a60 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 17 Jun 2020 10:47:20 +0300 Subject: [PATCH 304/375] suggestion for changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e8d58e6..6095cf139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard -- Configuration: `rewrite_policy` renamed to `policies` and moved from `instance` to `mrf` group. Old config namespace is deprecated. -- Configuration: `mrf_transparency` renamed to `transparency` and moved from `instance` to `mrf` group. Old config namespace is deprecated. -- Configuration: `mrf_transparency_exclusions` renamed to `transparency_exclusions` and moved from `instance` to `mrf` group. Old config namespace is deprecated. +- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated. <details> <summary>API Changes</summary> From 9a82de219c264f467b485316570c5425e3fe2f00 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 17 Jun 2020 10:50:05 +0300 Subject: [PATCH 305/375] formatting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6095cf139..fab87b569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 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 - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard From 90613348ed8078e1906f7ffd18eebfa1a3b7f25a Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:56:13 +0000 Subject: [PATCH 306/375] Apply suggestion to docs/API/admin_api.md --- docs/API/admin_api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 6659b605d..8a3d60187 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1253,7 +1253,7 @@ Loads json generated from `config/descriptions.exs`. - Authentication: required - Params: - - `urls` + - `urls` (array) - Response: From abfb1c756b62c24589d2881d77bf5974a80809d3 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:56:17 +0000 Subject: [PATCH 307/375] Apply suggestion to docs/API/admin_api.md --- docs/API/admin_api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 8a3d60187..c7f56cf5f 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1273,8 +1273,8 @@ Loads json generated from `config/descriptions.exs`. - Authentication: required - Params: - - `urls` - - `ban` + - `urls` (array) + - `ban` (boolean) - Response: From 74fd761637f737822d01aed945b6e0c75ced7008 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:56:30 +0000 Subject: [PATCH 308/375] Apply suggestion to lib/pleroma/web/media_proxy/invalidation.ex --- lib/pleroma/web/media_proxy/invalidation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex index 83ff8589c..6da7eb720 100644 --- a/lib/pleroma/web/media_proxy/invalidation.ex +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -32,6 +32,6 @@ defp do_purge(urls) do def prepare_urls(urls) do urls |> List.wrap() - |> Enum.map(&MediaProxy.url(&1)) + |> Enum.map(&MediaProxy.url/1) end end From 1b45bc7b2ac53e56a4868da7e2b5b198d16306ab Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:58:08 +0000 Subject: [PATCH 309/375] Apply suggestion to test/web/admin_api/controllers/media_proxy_cache_controller_test.exs --- .../admin_api/controllers/media_proxy_cache_controller_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs index 76a96f46f..ddaf39f14 100644 --- a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -14,7 +14,6 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do setup do on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) - :ok end setup do From 793a53f1ec18a42f15f58494e40ed3c37d35f95c Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:58:16 +0000 Subject: [PATCH 310/375] Apply suggestion to test/web/admin_api/controllers/media_proxy_cache_controller_test.exs --- .../admin_api/controllers/media_proxy_cache_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs index ddaf39f14..81e20d001 100644 --- a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -72,7 +72,7 @@ test "shows banned MediaProxy URLs", %{conn: conn} do end end - describe "DELETE /api/pleroma/admin/media_proxy_caches/delete" do + describe "POST /api/pleroma/admin/media_proxy_caches/delete" do test "deleted MediaProxy URLs from banned", %{conn: conn} do MediaProxy.put_in_deleted_urls([ "http://localhost:4001/media/a688346.jpg", From 6d33a3a51bb8ff0afdf7f4f9880f8f5c5f2dfebc Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:58:28 +0000 Subject: [PATCH 311/375] Apply suggestion to test/web/admin_api/controllers/media_proxy_cache_controller_test.exs --- .../admin_api/controllers/media_proxy_cache_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs index 81e20d001..42a3c0dd8 100644 --- a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -93,7 +93,7 @@ test "deleted MediaProxy URLs from banned", %{conn: conn} do end end - describe "PURGE /api/pleroma/admin/media_proxy_caches/purge" do + describe "POST /api/pleroma/admin/media_proxy_caches/purge" do test "perform invalidates cache of MediaProxy", %{conn: conn} do urls = [ "http://example.com/media/a688346.jpg", From 11b22a42293ec2ac0e66897bf4b29b5363913c19 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:58:33 +0000 Subject: [PATCH 312/375] Apply suggestion to test/web/media_proxy/invalidations/http_test.exs --- test/web/media_proxy/invalidations/http_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/media_proxy/invalidations/http_test.exs b/test/web/media_proxy/invalidations/http_test.exs index 09e7ca0fb..9d181dd8b 100644 --- a/test/web/media_proxy/invalidations/http_test.exs +++ b/test/web/media_proxy/invalidations/http_test.exs @@ -7,7 +7,6 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do setup do on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) - :ok end test "logs hasn't error message when request is valid" do From 2991aae4c4e4ae430539c1e6fac53fb8a0c991e9 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:58:38 +0000 Subject: [PATCH 313/375] Apply suggestion to test/web/media_proxy/invalidations/script_test.exs --- test/web/media_proxy/invalidations/script_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/media_proxy/invalidations/script_test.exs b/test/web/media_proxy/invalidations/script_test.exs index c69cec07a..8e155b705 100644 --- a/test/web/media_proxy/invalidations/script_test.exs +++ b/test/web/media_proxy/invalidations/script_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do setup do on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) - :ok end test "it logger error when script not found" do From 078d687e6ed66f921d7f54114f2dc6bf4abbf237 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 12:58:50 +0000 Subject: [PATCH 314/375] Apply suggestion to test/web/media_proxy/media_proxy_controller_test.exs --- test/web/media_proxy/media_proxy_controller_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index 2b6b25221..72da98a6a 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -12,7 +12,6 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do setup do on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) - :ok end test "it returns 404 when MediaProxy disabled", %{conn: conn} do From 44ce97a9c9e90d5906386d1b51dea144cd258c32 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 13:12:32 +0000 Subject: [PATCH 315/375] Apply suggestion to lib/pleroma/web/media_proxy/invalidations/script.ex --- lib/pleroma/web/media_proxy/invalidations/script.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex index 0217b119d..b0f44e8e2 100644 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do require Logger @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, opts \\ %{}) do + def purge(urls, opts \\ []) do args = urls |> List.wrap() From 9a371bf5f6245b0f372bec7af15e2f4fc41d4ab7 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 13:12:38 +0000 Subject: [PATCH 316/375] Apply suggestion to lib/pleroma/web/media_proxy/invalidations/script.ex --- lib/pleroma/web/media_proxy/invalidations/script.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex index b0f44e8e2..d32ffc50b 100644 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -18,7 +18,7 @@ def purge(urls, opts \\ []) do |> Enum.join(" ") opts - |> Keyword.get(:script_path, nil) + |> Keyword.get(:script_path) |> do_purge([args]) |> handle_result(urls) end From 96493da7bdab4ff4a51cbebf18df4127ddc47990 Mon Sep 17 00:00:00 2001 From: Maksim <parallel588@gmail.com> Date: Wed, 17 Jun 2020 13:14:01 +0000 Subject: [PATCH 317/375] Apply suggestion to test/web/media_proxy/invalidation_test.exs --- test/web/media_proxy/invalidation_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/media_proxy/invalidation_test.exs b/test/web/media_proxy/invalidation_test.exs index 3a9fa8c88..bf9af251c 100644 --- a/test/web/media_proxy/invalidation_test.exs +++ b/test/web/media_proxy/invalidation_test.exs @@ -13,7 +13,6 @@ defmodule Pleroma.Web.MediaProxy.InvalidationTest do setup do on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) - :ok end describe "Invalidation.Http" do From d4b5a9730e8fe7adf5a2eca15bf40fafff85f30d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn <egor@kislitsyn.com> Date: Wed, 17 Jun 2020 18:47:59 +0400 Subject: [PATCH 318/375] Remove `poll` from `notification_type` OpenAPI spec --- lib/pleroma/web/api_spec/operations/notification_operation.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index c966b553a..41328b5f2 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -183,7 +183,6 @@ defp notification_type do "favourite", "reblog", "mention", - "poll", "pleroma:emoji_reaction", "pleroma:chat_mention", "move", From 71a5d9bffb33d9424ea28900ea006678617e0096 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 17 Jun 2020 12:54:02 -0500 Subject: [PATCH 319/375] Empty list as default --- lib/pleroma/web/media_proxy/invalidations/http.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex index 3694b56e8..bb81d8888 100644 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do require Logger @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, opts) do + def purge(urls, opts \\ []) do method = Keyword.get(opts, :method, :purge) headers = Keyword.get(opts, :headers, []) options = Keyword.get(opts, :options, []) From c08c9db0c137d36896910194a6dc50a391a8fee2 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 17 Jun 2020 13:02:01 -0500 Subject: [PATCH 320/375] Remove misleading is_ prefix from boolean function --- lib/pleroma/web/media_proxy/media_proxy.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 59ca217ab..3dccd6b7f 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -37,15 +37,15 @@ def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url def url(url) do - if disabled?() or not is_url_proxiable?(url) do + if disabled?() or not url_proxiable?(url) do url else encode_url(url) end end - @spec is_url_proxiable?(String.t()) :: boolean() - def is_url_proxiable?(url) do + @spec url_proxiable?(String.t()) :: boolean() + def url_proxiable?(url) do if local?(url) or whitelisted?(url) do false else From 2731ea1334c2c91315465659a0874829cb9e1e11 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 17 Jun 2020 13:13:55 -0500 Subject: [PATCH 321/375] Change references from "deleted_urls" to "banned_urls" as nothing is handled via media deletions anymore; all actions are manual operations by an admin to ban the url --- lib/pleroma/application.ex | 2 +- lib/pleroma/plugs/uploaded_media.ex | 10 ++++---- .../media_proxy_cache_controller.ex | 6 ++--- lib/pleroma/web/media_proxy/media_proxy.ex | 20 ++++++++-------- .../web/media_proxy/media_proxy_controller.ex | 4 ++-- .../media_proxy_cache_controller_test.exs | 24 +++++++++---------- test/web/media_proxy/invalidation_test.exs | 14 +++++------ .../media_proxy_controller_test.exs | 6 ++--- 8 files changed, 43 insertions(+), 43 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index adebebc7a..4a21bf138 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -149,7 +149,7 @@ defp cachex_children do build_cachex("web_resp", limit: 2500), build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), build_cachex("failed_proxy_url", limit: 2500), - build_cachex("deleted_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000) + build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000) ] end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index 2f3fde002..40984cfc0 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -49,7 +49,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do with uploader <- Keyword.fetch!(config, :uploader), proxy_remote = Keyword.get(config, :proxy_remote, false), {:ok, get_method} <- uploader.get_file(file), - false <- media_is_deleted(conn, get_method) do + false <- media_is_banned(conn, get_method) do get_media(conn, get_method, proxy_remote, opts) else _ -> @@ -61,13 +61,13 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do def call(conn, _opts), do: conn - defp media_is_deleted(%{request_path: path} = _conn, {:static_dir, _}) do - MediaProxy.in_deleted_urls(Pleroma.Web.base_url() <> path) + defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do + MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path) end - defp media_is_deleted(_, {:url, url}), do: MediaProxy.in_deleted_urls(url) + defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url) - defp media_is_deleted(_, _), do: false + defp media_is_banned(_, _), do: false defp get_media(conn, {:static_dir, directory}, _, opts) do static_opts = diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex index e3fa0ac28..e2759d59f 100644 --- a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do def index(%{assigns: %{user: _}} = conn, params) do cursor = - :deleted_urls_cache + :banned_urls_cache |> :ets.table([{:traverse, {:select, Cachex.Query.create(true, :key)}}]) |> :qlc.cursor() @@ -47,7 +47,7 @@ def index(%{assigns: %{user: _}} = conn, params) do end def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do - MediaProxy.remove_from_deleted_urls(urls) + MediaProxy.remove_from_banned_urls(urls) render(conn, "index.json", urls: urls) end @@ -55,7 +55,7 @@ def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _ MediaProxy.Invalidation.purge(urls) if ban do - MediaProxy.put_in_deleted_urls(urls) + MediaProxy.put_in_banned_urls(urls) end render(conn, "index.json", urls: urls) diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 3dccd6b7f..077fabe47 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -10,27 +10,27 @@ defmodule Pleroma.Web.MediaProxy do @base64_opts [padding: false] - @spec in_deleted_urls(String.t()) :: boolean() - def in_deleted_urls(url), do: elem(Cachex.exists?(:deleted_urls_cache, url(url)), 1) + @spec in_banned_urls(String.t()) :: boolean() + def in_banned_urls(url), do: elem(Cachex.exists?(:banned_urls_cache, url(url)), 1) - def remove_from_deleted_urls(urls) when is_list(urls) do - Cachex.execute!(:deleted_urls_cache, fn cache -> + def remove_from_banned_urls(urls) when is_list(urls) do + Cachex.execute!(:banned_urls_cache, fn cache -> Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1)) end) end - def remove_from_deleted_urls(url) when is_binary(url) do - Cachex.del(:deleted_urls_cache, url(url)) + def remove_from_banned_urls(url) when is_binary(url) do + Cachex.del(:banned_urls_cache, url(url)) end - def put_in_deleted_urls(urls) when is_list(urls) do - Cachex.execute!(:deleted_urls_cache, fn cache -> + def put_in_banned_urls(urls) when is_list(urls) do + Cachex.execute!(:banned_urls_cache, fn cache -> Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true)) end) end - def put_in_deleted_urls(url) when is_binary(url) do - Cachex.put(:deleted_urls_cache, url(url), true) + def put_in_banned_urls(url) when is_binary(url) do + Cachex.put(:banned_urls_cache, url(url), true) end def url(url) when is_nil(url) or url == "", do: nil diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index ff0158d83..9a64b0ef3 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -14,11 +14,11 @@ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do with config <- Pleroma.Config.get([:media_proxy], []), true <- Keyword.get(config, :enabled, false), {:ok, url} <- MediaProxy.decode_url(sig64, url64), - {_, false} <- {:in_deleted_urls, MediaProxy.in_deleted_urls(url)}, + {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)}, :ok <- filename_matches(params, conn.request_path, url) do ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) else - error when error in [false, {:in_deleted_urls, true}] -> + error when error in [false, {:in_banned_urls, true}] -> send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) {:error, :invalid_signature} -> diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs index 42a3c0dd8..5ab6cb78a 100644 --- a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do setup do: clear_config([:media_proxy]) setup do - on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end setup do @@ -34,14 +34,14 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do describe "GET /api/pleroma/admin/media_proxy_caches" do test "shows banned MediaProxy URLs", %{conn: conn} do - MediaProxy.put_in_deleted_urls([ + MediaProxy.put_in_banned_urls([ "http://localhost:4001/media/a688346.jpg", "http://localhost:4001/media/fb1f4d.jpg" ]) - MediaProxy.put_in_deleted_urls("http://localhost:4001/media/gb1f44.jpg") - MediaProxy.put_in_deleted_urls("http://localhost:4001/media/tb13f47.jpg") - MediaProxy.put_in_deleted_urls("http://localhost:4001/media/wb1f46.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") response = conn @@ -74,7 +74,7 @@ test "shows banned MediaProxy URLs", %{conn: conn} do describe "POST /api/pleroma/admin/media_proxy_caches/delete" do test "deleted MediaProxy URLs from banned", %{conn: conn} do - MediaProxy.put_in_deleted_urls([ + MediaProxy.put_in_banned_urls([ "http://localhost:4001/media/a688346.jpg", "http://localhost:4001/media/fb1f4d.jpg" ]) @@ -88,8 +88,8 @@ test "deleted MediaProxy URLs from banned", %{conn: conn} do |> json_response_and_validate_schema(200) assert response["urls"] == ["http://localhost:4001/media/a688346.jpg"] - refute MediaProxy.in_deleted_urls("http://localhost:4001/media/a688346.jpg") - assert MediaProxy.in_deleted_urls("http://localhost:4001/media/fb1f4d.jpg") + refute MediaProxy.in_banned_urls("http://localhost:4001/media/a688346.jpg") + assert MediaProxy.in_banned_urls("http://localhost:4001/media/fb1f4d.jpg") end end @@ -114,8 +114,8 @@ test "perform invalidates cache of MediaProxy", %{conn: conn} do assert response["urls"] == urls - refute MediaProxy.in_deleted_urls("http://example.com/media/a688346.jpg") - refute MediaProxy.in_deleted_urls("http://example.com/media/fb1f4d.jpg") + refute MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") + refute MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") end end @@ -137,8 +137,8 @@ test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: c assert response["urls"] == urls - assert MediaProxy.in_deleted_urls("http://example.com/media/a688346.jpg") - assert MediaProxy.in_deleted_urls("http://example.com/media/fb1f4d.jpg") + assert MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") + assert MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") end end end diff --git a/test/web/media_proxy/invalidation_test.exs b/test/web/media_proxy/invalidation_test.exs index bf9af251c..926ae74ca 100644 --- a/test/web/media_proxy/invalidation_test.exs +++ b/test/web/media_proxy/invalidation_test.exs @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MediaProxy.InvalidationTest do setup do: clear_config([:media_proxy]) setup do - on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end describe "Invalidation.Http" do @@ -23,7 +23,7 @@ test "perform request to clear cache" do Config.put([Invalidation.Http], method: :purge, headers: [{"x-refresh", 1}]) image_url = "http://example.com/media/example.jpg" - Pleroma.Web.MediaProxy.put_in_deleted_urls(image_url) + Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) mock(fn %{ @@ -35,9 +35,9 @@ test "perform request to clear cache" do end) assert capture_log(fn -> - assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) assert Invalidation.purge([image_url]) == {:ok, [image_url]} - assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) end) =~ "Running cache purge: [\"#{image_url}\"]" end end @@ -50,13 +50,13 @@ test "run script to clear cache" do Config.put([Invalidation.Script], script_path: "purge-nginx") image_url = "http://example.com/media/example.jpg" - Pleroma.Web.MediaProxy.put_in_deleted_urls(image_url) + Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) with_mocks [{System, [], [cmd: fn _, _ -> {"ok", 0} end]}] do assert capture_log(fn -> - assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) assert Invalidation.purge([image_url]) == {:ok, [image_url]} - assert Pleroma.Web.MediaProxy.in_deleted_urls(image_url) + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) end) =~ "Running cache purge: [\"#{image_url}\"]" 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 72da98a6a..d61cef83b 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do setup do: clear_config([Pleroma.Web.Endpoint, :secret_key_base]) setup do - on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end test "it returns 404 when MediaProxy disabled", %{conn: conn} do @@ -71,11 +71,11 @@ test "it performs ReverseProxy.call when signature valid", %{conn: conn} do end end - test "it returns 404 when url contains in deleted_urls cache", %{conn: conn} do + test "it returns 404 when url contains in banned_urls cache", %{conn: conn} do Config.put([:media_proxy, :enabled], true) Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") - Pleroma.Web.MediaProxy.put_in_deleted_urls("https://google.fn/test.png") + Pleroma.Web.MediaProxy.put_in_banned_urls("https://google.fn/test.png") with_mock Pleroma.ReverseProxy, call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do From 4044f24e2e4935757e038e7f06373ed1c9172560 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Thu, 18 Jun 2020 05:02:33 +0300 Subject: [PATCH 322/375] fix test --- lib/pleroma/web/media_proxy/invalidation.ex | 3 ++- test/web/media_proxy/invalidations/http_test.exs | 2 +- test/web/media_proxy/invalidations/script_test.exs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex index 6da7eb720..5808861e6 100644 --- a/lib/pleroma/web/media_proxy/invalidation.ex +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -26,7 +26,8 @@ def purge(urls) do defp do_purge(urls) do provider = Config.get([:media_proxy, :invalidation, :provider]) - provider.purge(urls, Config.get(provider)) + options = Config.get(provider) + provider.purge(urls, options) end def prepare_urls(urls) do diff --git a/test/web/media_proxy/invalidations/http_test.exs b/test/web/media_proxy/invalidations/http_test.exs index 9d181dd8b..a1bef5237 100644 --- a/test/web/media_proxy/invalidations/http_test.exs +++ b/test/web/media_proxy/invalidations/http_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do import Tesla.Mock setup do - on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end test "logs hasn't error message when request is valid" do diff --git a/test/web/media_proxy/invalidations/script_test.exs b/test/web/media_proxy/invalidations/script_test.exs index 8e155b705..51833ab18 100644 --- a/test/web/media_proxy/invalidations/script_test.exs +++ b/test/web/media_proxy/invalidations/script_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do import ExUnit.CaptureLog setup do - on_exit(fn -> Cachex.clear(:deleted_urls_cache) end) + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end test "it logger error when script not found" do From c9b5e3fedabd0b6ef3bb9e6108385ffa3857af54 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Thu, 18 Jun 2020 05:29:31 +0300 Subject: [PATCH 323/375] revert 'database' option to rejected keys --- lib/pleroma/config/loader.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 76559e70c..0f3ecf1ed 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Config.Loader do Pleroma.Web.Endpoint, :env, :configurable_from_database, + :database, :swarm ] From e4c61f1741f32fec3201f7d9a8403bc1bc329710 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Thu, 18 Jun 2020 05:45:15 +0300 Subject: [PATCH 324/375] added test --- test/fixtures/config/temp.secret.exs | 2 ++ test/tasks/config_test.exs | 1 + 2 files changed, 3 insertions(+) diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs index dc950ca30..fa8c7c7e8 100644 --- a/test/fixtures/config/temp.secret.exs +++ b/test/fixtures/config/temp.secret.exs @@ -9,3 +9,5 @@ config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox config :postgrex, :json_library, Poison + +config :pleroma, :database, rum_enabled: true diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index e1bddfebf..99038e544 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -50,6 +50,7 @@ test "filtered settings are migrated to db" do config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"}) refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"}) + refute ConfigDB.get_by_params(%{group: ":pleroma", key: ":database"}) assert config1.value == [key: "value", key2: [Repo]] assert config2.value == [key: "value2", key2: ["Activity"]] From 3becdafd335f95d9320d287ecf9a55ea1b1765cd Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Thu, 18 Jun 2020 14:32:21 +0300 Subject: [PATCH 325/375] emoji packs pagination --- lib/pleroma/emoji/pack.ex | 19 ++++++++++++---- .../pleroma_emoji_pack_operation.ex | 14 ++++++++++++ .../controllers/emoji_pack_controller.ex | 4 ++-- .../emoji_pack_controller_test.exs | 22 +++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 14a5185be..5660c4c9d 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji - @spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values} + @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} def create(name) do with :ok <- validate_not_empty([name]), dir <- Path.join(emoji_path(), name), @@ -120,8 +120,8 @@ def list_remote(url) do end end - @spec list_local() :: {:ok, map()} - def list_local do + @spec list_local(keyword()) :: {:ok, map()} + def list_local(opts) do with {:ok, results} <- list_packs_dir() do packs = results @@ -132,6 +132,17 @@ def list_local do end end) |> Enum.reject(&is_nil/1) + + packs = + case opts[:page] do + 1 -> + Enum.take(packs, opts[:page_size]) + + _ -> + packs + |> Enum.take(opts[:page] * opts[:page_size]) + |> Enum.take(-opts[:page_size]) + end |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end) {:ok, packs} @@ -146,7 +157,7 @@ def get_archive(name) do end end - @spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()} + @spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()} def download(name, url, as) do uri = url |> String.trim() |> URI.parse() diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 567688ff5..0d842382b 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -33,6 +33,20 @@ def index_operation do tags: ["Emoji Packs"], summary: "Lists local custom emoji packs", operationId: "PleromaAPI.EmojiPackController.index", + parameters: [ + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of statuses to return" + ) + ], responses: %{ 200 => emoji_packs_response() } diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index d1efdeb5d..5654b3fbe 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -37,13 +37,13 @@ def remote(conn, %{url: url}) do end end - def index(conn, _params) do + def index(conn, params) do emoji_path = [:instance, :static_dir] |> Pleroma.Config.get!() |> Path.join("emoji") - with {:ok, packs} <- Pack.list_local() do + with {:ok, packs} <- Pack.list_local(page: params.page, page_size: params.page_size) do json(conn, packs) else {:error, :create_dir, e} -> diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index ee3d281a0..aafca6359 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -39,6 +39,28 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do non_shared = resp["test_pack_nonshared"] assert non_shared["pack"]["share-files"] == false assert non_shared["pack"]["can-download"] == false + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1") + |> json_response_and_validate_schema(200) + + [pack1] = Map.keys(resp) + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=2") + |> json_response_and_validate_schema(200) + + [pack2] = Map.keys(resp) + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=3") + |> json_response_and_validate_schema(200) + + [pack3] = Map.keys(resp) + assert [pack1, pack2, pack3] |> Enum.uniq() |> length() == 3 end describe "GET /api/pleroma/emoji/packs/remote" do From 4975ed86bcca330373a68c9e6c6798a6b2167b14 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Thu, 18 Jun 2020 18:50:03 +0300 Subject: [PATCH 326/375] emoji pagination for pack show action --- docs/API/pleroma_api.md | 12 ++- lib/pleroma/emoji/pack.ex | 37 +++++---- .../pleroma_emoji_pack_operation.ex | 16 +++- .../controllers/emoji_pack_controller.ex | 4 +- .../emoji/test_pack/blank2.png | Bin 0 -> 95 bytes .../instance_static/emoji/test_pack/pack.json | 3 +- .../emoji/test_pack_nonshared/nonshared.zip | Bin 256 -> 548 bytes .../emoji/test_pack_nonshared/pack.json | 2 +- .../emoji_pack_controller_test.exs | 72 +++++++++++++----- 9 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 test/instance_static/emoji/test_pack/blank2.png diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 70d4755b7..d8d3ba85f 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -450,17 +450,25 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Response: JSON, list with updated files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message. ## `GET /api/pleroma/emoji/packs` + ### Lists local custom emoji packs + * Method `GET` * Authentication: not required -* Params: None +* Params: + * `page`: page number for packs (default 1) + * `page_size`: page size for packs (default 50) * Response: JSON, "ok" and 200 status and the JSON hashmap of pack name to pack contents ## `GET /api/pleroma/emoji/packs/:name` + ### Get pack.json for the pack + * Method `GET` * Authentication: not required -* Params: None +* Params: + * `page`: page number for files (default 1) + * `page_size`: page size for files (default 50) * Response: JSON, pack json with `files` and `pack` keys with 200 status or 404 if the pack does not exist ## `GET /api/pleroma/emoji/packs/:name/archive` diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 5660c4c9d..c033572c1 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -26,10 +26,27 @@ def create(name) do end end - @spec show(String.t()) :: {:ok, t()} | {:error, atom()} - def show(name) do + defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size) + + defp paginate(entities, page, page_size) do + entities + |> Enum.take(page * page_size) + |> Enum.take(-page_size) + end + + @spec show(keyword()) :: {:ok, t()} | {:error, atom()} + def show(opts) do + name = opts[:name] + with :ok <- validate_not_empty([name]), {:ok, pack} <- load_pack(name) do + shortcodes = + pack.files + |> Map.keys() + |> paginate(opts[:page], opts[:page_size]) + + pack = Map.put(pack, :files, Map.take(pack.files, shortcodes)) + {:ok, validate_pack(pack)} end end @@ -132,17 +149,7 @@ def list_local(opts) do end end) |> Enum.reject(&is_nil/1) - - packs = - case opts[:page] do - 1 -> - Enum.take(packs, opts[:page_size]) - - _ -> - packs - |> Enum.take(opts[:page] * opts[:page_size]) - |> Enum.take(-opts[:page_size]) - end + |> paginate(opts[:page], opts[:page_size]) |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end) {:ok, packs} @@ -307,7 +314,9 @@ defp downloadable?(pack) do # Otherwise, they'd have to download it from external-src pack.pack["share-files"] && Enum.all?(pack.files, fn {_, file} -> - File.exists?(Path.join(pack.path, file)) + pack.path + |> Path.join(file) + |> File.exists?() end) end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 0d842382b..e8abe654d 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -58,7 +58,21 @@ def show_operation do tags: ["Emoji Packs"], summary: "Show emoji pack", operationId: "PleromaAPI.EmojiPackController.show", - parameters: [name_param()], + parameters: [ + name_param(), + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of statuses to return" + ) + ], responses: %{ 200 => Operation.response("Emoji Pack", "application/json", emoji_pack()), 400 => Operation.response("Bad Request", "application/json", ApiError), diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 5654b3fbe..078fb88dd 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -60,10 +60,10 @@ def index(conn, params) do end end - def show(conn, %{name: name}) do + def show(conn, %{name: name, page: page, page_size: page_size}) do name = String.trim(name) - with {:ok, pack} <- Pack.show(name) do + with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do json(conn, pack) else {:error, :not_found} -> diff --git a/test/instance_static/emoji/test_pack/blank2.png b/test/instance_static/emoji/test_pack/blank2.png new file mode 100644 index 0000000000000000000000000000000000000000..8f50fa02340e7e09e562f86e00b6e4bd6ad1d565 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^4Is=2Bp6=1#-sr$rjj7PU<QV=$!9HqJPA)1$B+uf q<ORkOtcw#wdYS?axZDnEFfed5Ffe}4nR*bYhQZU-&t;ucLK6U?=oVoB literal 0 HcmV?d00001 diff --git a/test/instance_static/emoji/test_pack/pack.json b/test/instance_static/emoji/test_pack/pack.json index 481891b08..5b33fbb32 100644 --- a/test/instance_static/emoji/test_pack/pack.json +++ b/test/instance_static/emoji/test_pack/pack.json @@ -1,6 +1,7 @@ { "files": { - "blank": "blank.png" + "blank": "blank.png", + "blank2": "blank2.png" }, "pack": { "description": "Test description", diff --git a/test/instance_static/emoji/test_pack_nonshared/nonshared.zip b/test/instance_static/emoji/test_pack_nonshared/nonshared.zip index 148446c642ea24b494bc3e25ccd772faaf2f2a13..59bff37f0895f17fecf4285015c5dcd18fcc8b35 100644 GIT binary patch literal 548 zcmWIWW@Zs#-~hstas2)aP!JEKIT;ifl5!IBvh@n`(nCXd8Q6EphQ<a|ypHn$;?fFk z21b^zj0_Aw?F<aBc|H_Be>&+=QbNLmuU`r{nJ)1voZ(QBh}(T^38Ut+NecTD*xELo zOxJC&;q(_jK7s4l6V_uwYr2J9s%A0q?zqF3WnfTXVqj2rerA=xlFOH`o==?{>?F(( z;LXkvAKoY$0ki|;r~sVK<$^ia2*th8K(|H>q<a~eM3@l)jO-Us0K>qNMi7hW;12M{ gZ7e7tU|>n(dYG|91xtW8D;r2J6A*%Q#xsI=0P0(p8vp<R delta 152 zcmZ3&(!k^x;LXe;!oa}5!EiE;-#<RQQ8WU`iw9y(1{sE=oW#6ry@I^-&=5`r=3TO( zv8RBzw1S&~k>v$50|Stl=o%x-$Rx*%%Mgjlb&OIvtPtHOIvE%Oyjj_RHZd>)p+AtG I4dO5W0E3SliU0rr diff --git a/test/instance_static/emoji/test_pack_nonshared/pack.json b/test/instance_static/emoji/test_pack_nonshared/pack.json index 93d643a5f..09f6274d1 100644 --- a/test/instance_static/emoji/test_pack_nonshared/pack.json +++ b/test/instance_static/emoji/test_pack_nonshared/pack.json @@ -4,7 +4,7 @@ "homepage": "https://pleroma.social", "description": "Test description", "fallback-src": "https://nonshared-pack", - "fallback-src-sha256": "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF", + "fallback-src-sha256": "1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D", "share-files": false }, "files": { diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index aafca6359..f6239cae5 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -31,7 +31,7 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) shared = resp["test_pack"] - assert shared["files"] == %{"blank" => "blank.png"} + assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} assert Map.has_key?(shared["pack"], "download-sha256") assert shared["pack"]["can-download"] assert shared["pack"]["share-files"] @@ -354,7 +354,7 @@ test "for a pack with a fallback source", ctx do Map.put( new_data, "fallback-src-sha256", - "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF" + "1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D" ) assert ctx[:admin_conn] @@ -420,7 +420,7 @@ test "don't rewrite old emoji", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", + shortcode: "blank3", filename: "dir/blank.png", file: %Plug.Upload{ filename: "blank.png", @@ -429,7 +429,8 @@ test "don't rewrite old emoji", %{admin_conn: admin_conn} do }) |> json_response_and_validate_schema(200) == %{ "blank" => "blank.png", - "blank2" => "dir/blank.png" + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" } assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") @@ -453,7 +454,7 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", + shortcode: "blank3", filename: "dir/blank.png", file: %Plug.Upload{ filename: "blank.png", @@ -462,7 +463,8 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do }) |> json_response_and_validate_schema(200) == %{ "blank" => "blank.png", - "blank2" => "dir/blank.png" + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" } assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") @@ -470,14 +472,15 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - new_shortcode: "blank3", + shortcode: "blank3", + new_shortcode: "blank4", new_filename: "dir_2/blank_3.png", force: true }) |> json_response_and_validate_schema(200) == %{ "blank" => "blank.png", - "blank3" => "dir_2/blank_3.png" + "blank2" => "blank2.png", + "blank4" => "dir_2/blank_3.png" } assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") @@ -503,7 +506,7 @@ test "add file with not loaded pack", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") |> post("/api/pleroma/emoji/packs/not_loaded/files", %{ - shortcode: "blank2", + shortcode: "blank3", filename: "dir/blank.png", file: %Plug.Upload{ filename: "blank.png", @@ -557,7 +560,8 @@ test "new with shortcode as file with update", %{admin_conn: admin_conn} do }) |> json_response_and_validate_schema(200) == %{ "blank" => "blank.png", - "blank4" => "dir/blank.png" + "blank4" => "dir/blank.png", + "blank2" => "blank2.png" } assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") @@ -571,7 +575,8 @@ test "new with shortcode as file with update", %{admin_conn: admin_conn} do }) |> json_response_and_validate_schema(200) == %{ "blank3" => "dir_2/blank_3.png", - "blank" => "blank.png" + "blank" => "blank.png", + "blank2" => "blank2.png" } refute File.exists?("#{@emoji_path}/test_pack/dir/") @@ -579,7 +584,10 @@ test "new with shortcode as file with update", %{admin_conn: admin_conn} do assert admin_conn |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") - |> json_response_and_validate_schema(200) == %{"blank" => "blank.png"} + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png" + } refute File.exists?("#{@emoji_path}/test_pack/dir_2/") @@ -603,7 +611,8 @@ test "new with shortcode from url", %{admin_conn: admin_conn} do }) |> json_response_and_validate_schema(200) == %{ "blank_url" => "blank_url.png", - "blank" => "blank.png" + "blank" => "blank.png", + "blank2" => "blank2.png" } assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") @@ -624,15 +633,16 @@ test "new without shortcode", %{admin_conn: admin_conn} do }) |> json_response_and_validate_schema(200) == %{ "shortcode" => "shortcode.png", - "blank" => "blank.png" + "blank" => "blank.png", + "blank2" => "blank2.png" } end test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank2") + |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank2\" does not exist" + "error" => "Emoji \"blank3\" does not exist" } end @@ -640,12 +650,12 @@ test "update non existing emoji", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - new_shortcode: "blank3", + shortcode: "blank3", + new_shortcode: "blank4", new_filename: "dir_2/blank_3.png" }) |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank2\" does not exist" + "error" => "Emoji \"blank3\" does not exist" } end @@ -768,7 +778,7 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do describe "GET /api/pleroma/emoji/packs/:name" do test "shows pack.json", %{conn: conn} do assert %{ - "files" => %{"blank" => "blank.png"}, + "files" => files, "pack" => %{ "can-download" => true, "description" => "Test description", @@ -781,6 +791,26 @@ test "shows pack.json", %{conn: conn} do conn |> get("/api/pleroma/emoji/packs/test_pack") |> json_response_and_validate_schema(200) + + assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} + + assert %{ + "files" => files + } = + conn + |> get("/api/pleroma/emoji/packs/test_pack?page_size=1") + |> json_response_and_validate_schema(200) + + assert files |> Map.keys() |> length() == 1 + + assert %{ + "files" => files + } = + conn + |> get("/api/pleroma/emoji/packs/test_pack?page_size=1&page=2") + |> json_response_and_validate_schema(200) + + assert files |> Map.keys() |> length() == 1 end test "non existing pack", %{conn: conn} do From 3e3f9253e6db17b691c7393ad7a5f89df84348ea Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 19 Jun 2020 10:17:24 +0300 Subject: [PATCH 327/375] adding overall count for packs and files --- docs/API/pleroma_api.md | 22 ++++++++++++-- lib/pleroma/emoji/pack.ex | 20 +++++++++---- .../controllers/emoji_pack_controller.ex | 4 +-- .../emoji_pack_controller_test.exs | 30 ++++++++++++------- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index d8d3ba85f..e5bc29eb2 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -458,7 +458,17 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Params: * `page`: page number for packs (default 1) * `page_size`: page size for packs (default 50) -* Response: JSON, "ok" and 200 status and the JSON hashmap of pack name to pack contents +* Response: `packs` key with JSON hashmap of pack name to pack contents and `count` key for count of packs. + +```json +{ + "packs": { + "pack_name": {...}, // pack contents + ... + }, + "count": 0 // packs count +} +``` ## `GET /api/pleroma/emoji/packs/:name` @@ -469,7 +479,15 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Params: * `page`: page number for files (default 1) * `page_size`: page size for files (default 50) -* Response: JSON, pack json with `files` and `pack` keys with 200 status or 404 if the pack does not exist +* Response: JSON, pack json with `files`, `files_count` and `pack` keys with 200 status or 404 if the pack does not exist. + +```json +{ + "files": {...}, + "files_count": 0, // emoji count in pack + "pack": {...} +} +``` ## `GET /api/pleroma/emoji/packs/:name/archive` ### Requests a local pack archive from the instance diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index c033572c1..2dca21c93 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -1,6 +1,7 @@ defmodule Pleroma.Emoji.Pack do - @derive {Jason.Encoder, only: [:files, :pack]} + @derive {Jason.Encoder, only: [:files, :pack, :files_count]} defstruct files: %{}, + files_count: 0, pack_file: nil, path: nil, pack: %{}, @@ -8,6 +9,7 @@ defmodule Pleroma.Emoji.Pack do @type t() :: %__MODULE__{ files: %{String.t() => Path.t()}, + files_count: non_neg_integer(), pack_file: Path.t(), path: Path.t(), pack: map(), @@ -137,10 +139,10 @@ def list_remote(url) do end end - @spec list_local(keyword()) :: {:ok, map()} + @spec list_local(keyword()) :: {:ok, map(), non_neg_integer()} def list_local(opts) do with {:ok, results} <- list_packs_dir() do - packs = + all_packs = results |> Enum.map(fn name -> case load_pack(name) do @@ -149,10 +151,13 @@ def list_local(opts) do end end) |> Enum.reject(&is_nil/1) + + packs = + all_packs |> paginate(opts[:page], opts[:page_size]) |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end) - {:ok, packs} + {:ok, packs, length(all_packs)} end end @@ -215,7 +220,12 @@ def load_pack(name) do |> Map.put(:path, Path.dirname(pack_file)) |> Map.put(:name, name) - {:ok, pack} + files_count = + pack.files + |> Map.keys() + |> length() + + {:ok, Map.put(pack, :files_count, files_count)} else {:error, :not_found} end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 078fb88dd..33ecd1f70 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -43,8 +43,8 @@ def index(conn, params) do |> Pleroma.Config.get!() |> Path.join("emoji") - with {:ok, packs} <- Pack.list_local(page: params.page, page_size: params.page_size) do - json(conn, packs) + with {:ok, packs, count} <- Pack.list_local(page: params.page, page_size: params.page_size) do + json(conn, %{packs: packs, count: count}) else {:error, :create_dir, e} -> conn diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index f6239cae5..91312c832 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -30,13 +30,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do test "GET /api/pleroma/emoji/packs", %{conn: conn} do resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - shared = resp["test_pack"] + assert resp["count"] == 3 + shared = resp["packs"]["test_pack"] assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} assert Map.has_key?(shared["pack"], "download-sha256") assert shared["pack"]["can-download"] assert shared["pack"]["share-files"] - non_shared = resp["test_pack_nonshared"] + non_shared = resp["packs"]["test_pack_nonshared"] assert non_shared["pack"]["share-files"] == false assert non_shared["pack"]["can-download"] == false @@ -45,21 +46,24 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> get("/api/pleroma/emoji/packs?page_size=1") |> json_response_and_validate_schema(200) - [pack1] = Map.keys(resp) + assert resp["count"] == 3 + [pack1] = Map.keys(resp["packs"]) resp = conn |> get("/api/pleroma/emoji/packs?page_size=1&page=2") |> json_response_and_validate_schema(200) - [pack2] = Map.keys(resp) + assert resp["count"] == 3 + [pack2] = Map.keys(resp["packs"]) resp = conn |> get("/api/pleroma/emoji/packs?page_size=1&page=3") |> json_response_and_validate_schema(200) - [pack3] = Map.keys(resp) + assert resp["count"] == 3 + [pack3] = Map.keys(resp["packs"]) assert [pack1, pack2, pack3] |> Enum.uniq() |> length() == 3 end @@ -683,7 +687,8 @@ test "creating and deleting a pack", %{admin_conn: admin_conn} do assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ "pack" => %{}, - "files" => %{} + "files" => %{}, + "files_count" => 0 } assert admin_conn @@ -741,14 +746,14 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - refute Map.has_key?(resp, "test_pack_for_import") + refute Map.has_key?(resp["packs"], "test_pack_for_import") assert admin_conn |> get("/api/pleroma/emoji/packs/import") |> json_response_and_validate_schema(200) == ["test_pack_for_import"] resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} + assert resp["packs"]["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") @@ -768,7 +773,7 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - assert resp["test_pack_for_import"]["files"] == %{ + assert resp["packs"]["test_pack_for_import"]["files"] == %{ "blank" => "blank.png", "blank2" => "blank.png", "foo" => "blank.png" @@ -779,6 +784,7 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do test "shows pack.json", %{conn: conn} do assert %{ "files" => files, + "files_count" => 2, "pack" => %{ "can-download" => true, "description" => "Test description", @@ -795,7 +801,8 @@ test "shows pack.json", %{conn: conn} do assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} assert %{ - "files" => files + "files" => files, + "files_count" => 2 } = conn |> get("/api/pleroma/emoji/packs/test_pack?page_size=1") @@ -804,7 +811,8 @@ test "shows pack.json", %{conn: conn} do assert files |> Map.keys() |> length() == 1 assert %{ - "files" => files + "files" => files, + "files_count" => 2 } = conn |> get("/api/pleroma/emoji/packs/test_pack?page_size=1&page=2") From 0c739b423aad4cc6baa3a59308200ca5a5060716 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 19 Jun 2020 12:31:55 +0300 Subject: [PATCH 328/375] changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee13904f..ee657de13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** removed `with_move` parameter from notifications timeline. ### Added + - Chats: Added support for federated chats. For details, see the docs. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - Instance: Add `background_image` to configuration and `/api/v1/instance` @@ -34,6 +35,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Notifications: Added `follow_request` notification type. - Added `:reject_deletes` group to SimplePolicy - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances +- Support pagination in emoji packs API (for packs and for files in pack) + <details> <summary>API Changes</summary> - Mastodon API: Extended `/api/v1/instance`. From 02ca8a363f738ece7b605940690f6a538f6c2fa8 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 19 Jun 2020 14:46:38 +0300 Subject: [PATCH 329/375] default page size for files --- docs/API/pleroma_api.md | 2 +- .../web/api_spec/operations/pleroma_emoji_pack_operation.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index e5bc29eb2..b7eee5192 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -478,7 +478,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Authentication: not required * Params: * `page`: page number for files (default 1) - * `page_size`: page size for files (default 50) + * `page_size`: page size for files (default 30) * Response: JSON, pack json with `files`, `files_count` and `pack` keys with 200 status or 404 if the pack does not exist. ```json diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index e8abe654d..da7cc5154 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -69,7 +69,7 @@ def show_operation do Operation.parameter( :page_size, :query, - %Schema{type: :integer, default: 50}, + %Schema{type: :integer, default: 30}, "Number of statuses to return" ) ], From 5237a2df9f123f661de30a53193b7d9fec69ecae Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Fri, 19 Jun 2020 16:14:06 +0300 Subject: [PATCH 330/375] [#1873] Fixes missing :offset pagination param support. Added pagination support for hashtags search. --- lib/pleroma/pagination.ex | 6 ++++ lib/pleroma/web/api_spec/helpers.ex | 6 ++++ .../controllers/search_controller.ex | 31 ++++++++++++------- .../controllers/search_controller_test.exs | 16 ++++++++++ .../controllers/status_controller_test.exs | 2 +- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index 1b99e44f9..9a3795769 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -64,6 +64,12 @@ def fetch_paginated(query, params, :offset, table_binding) do @spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] def paginate(query, options, method \\ :keyset, table_binding \\ nil) + def paginate(list, options, _method, _table_binding) when is_list(list) do + offset = options[:offset] || 0 + limit = options[:limit] || 0 + Enum.slice(list, offset, limit) + end + def paginate(query, options, :keyset, table_binding) do query |> restrict(:min_id, options, table_binding) diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index a9cfe0fed..a258e8421 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -39,6 +39,12 @@ def pagination_params do :string, "Return the newest items newer than this ID" ), + Operation.parameter( + :offset, + :query, + %Schema{type: :integer, default: 0}, + "Return items past this number of items" + ), Operation.parameter( :limit, :query, diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 3be0ca095..e50980122 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -107,21 +107,21 @@ defp resource_search(_, "statuses", query, options) do ) end - defp resource_search(:v2, "hashtags", query, _options) do + defp resource_search(:v2, "hashtags", query, options) do tags_path = Web.base_url() <> "/tag/" query - |> prepare_tags() + |> prepare_tags(options) |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) end - defp resource_search(:v1, "hashtags", query, _options) do - prepare_tags(query) + defp resource_search(:v1, "hashtags", query, options) do + prepare_tags(query, options) end - defp prepare_tags(query, add_joined_tag \\ true) do + defp prepare_tags(query, options) do tags = query |> preprocess_uri_query() @@ -139,13 +139,20 @@ defp prepare_tags(query, add_joined_tag \\ true) do tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end) - if Enum.empty?(explicit_tags) && add_joined_tag do - tags - |> Kernel.++([joined_tag(tags)]) - |> Enum.uniq_by(&String.downcase/1) - else - tags - end + tags = + if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do + add_joined_tag(tags) + else + tags + end + + Pleroma.Pagination.paginate(tags, options) + end + + defp add_joined_tag(tags) do + tags + |> Kernel.++([joined_tag(tags)]) + |> Enum.uniq_by(&String.downcase/1) end # If `query` is a URI, returns last component of its path, otherwise returns `query` diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index c605957b1..826f37fbc 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -151,6 +151,22 @@ test "constructs hashtags from search query", %{conn: conn} do ] end + test "supports pagination of hashtags search results", %{conn: conn} do + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1}) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "text", "url" => "#{Web.base_url()}/tag/text"}, + %{"name" => "with", "url" => "#{Web.base_url()}/tag/with"} + ] + end + test "excludes a blocked users from search results", %{conn: conn} do user = insert(:user) user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 648e6f2ce..a98e939e8 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -1561,7 +1561,7 @@ test "favorites paginate correctly" do # Using the header for pagination works correctly [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") - [_, max_id] = Regex.run(~r/max_id=(.*)>;/, next) + [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) assert max_id == third_favorite.id From abdb540d450b5e68ea452f78d865d63bca764a49 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Fri, 19 Jun 2020 15:30:30 +0200 Subject: [PATCH 331/375] ObjectValidators: Add basic UpdateValidator. --- lib/pleroma/web/activity_pub/builder.ex | 15 +++++++ .../web/activity_pub/object_validator.ex | 11 +++++ .../object_validators/update_validator.ex | 43 +++++++++++++++++++ .../activity_pub/object_validator_test.exs | 20 +++++++++ 4 files changed, 89 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/object_validators/update_validator.ex diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 1aac62c69..135a5c431 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -123,6 +123,21 @@ def like(actor, object) do end end + # Retricted to user updates for now, always public + @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()} + def update(actor, object) do + to = [Pleroma.Constants.as_public(), actor.follower_address] + + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "type" => "Update", + "actor" => actor.ap_id, + "object" => object, + "to" => to + }, []} + end + @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} def announce(actor, object, options \\ []) do public? = Keyword.get(options, :public, false) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 6a83a2c33..804a9d06e 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -19,10 +19,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) + def validate(%{"type" => "Update"} = object, meta) do + with {:ok, object} <- + object + |> UpdateValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => "Undo"} = object, meta) do with {:ok, object} <- object diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex new file mode 100644 index 000000000..94d72491b --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:type, :string) + field(:actor, ObjectValidators.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + # In this case, we save the full object in this activity instead of just a + # reference, so we can always see what was actually changed by this. + field(:object, :map) + end + + def cast_data(data) do + %__MODULE__{} + |> cast(data, __schema__(:fields)) + end + + def validate_data(cng) do + cng + |> validate_required([:id, :type, :actor, :to, :cc, :object]) + |> validate_inclusion(:type, ["Update"]) + |> validate_actor_presence() + end + + def cast_and_validate(data) do + data + |> cast_data + |> validate_data + end +end diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 31224abe0..adb56092d 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -622,4 +622,24 @@ test "returns an error if the actor can't announce the object", %{ assert {:actor, {"can not announce this object publicly", []}} in cng.errors end end + + describe "updates" do + setup do + user = insert(:user) + + object = %{ + "id" => user.ap_id, + "name" => "A new name", + "summary" => "A new bio" + } + + {:ok, valid_update, []} = Builder.update(user, object) + + %{user: user, valid_update: valid_update} + end + + test "validates a basic object", %{valid_update: valid_update} do + assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) + end + end end From d54b0432eae74a830a0294cf48f23933a16382aa Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Fri, 19 Jun 2020 15:49:34 +0200 Subject: [PATCH 332/375] README: Add some troubleshooting info for compilation issues. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 7fc1fd381..6ca3118fb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,16 @@ Currently Pleroma is not packaged by any OS/Distros, but if you want to package ### Docker While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>. +### Compilation Troubleshooting +If you ever encounter compilation issues during the updating of Pleroma, you can try these commands and see if they fix things: + +- `mix deps.clean --all` +- `mix local.rebar` +- `mix local.hex` +- `rm -r _build` + +If you are not developing Pleroma, it is better to use the OTP release, which comes with everything precompiled. + ## Documentation - Latest Released revision: <https://docs.pleroma.social> - Latest Git revision: <https://docs-develop.pleroma.social> From 75670a99e46a09f9bddc0959c680c2cb173e1f3b Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Fri, 19 Jun 2020 16:38:57 +0200 Subject: [PATCH 333/375] UpdateValidator: Only allow updates from the user themselves. --- .../object_validators/update_validator.ex | 16 ++++++++++++++++ test/web/activity_pub/object_validator_test.exs | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index 94d72491b..b4ba5ede0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -33,6 +33,7 @@ def validate_data(cng) do |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Update"]) |> validate_actor_presence() + |> validate_updating_rights() end def cast_and_validate(data) do @@ -40,4 +41,19 @@ def cast_and_validate(data) do |> cast_data |> validate_data end + + # For now we only support updating users, and here the rule is easy: + # object id == actor id + def validate_updating_rights(cng) do + with actor = get_field(cng, :actor), + object = get_field(cng, :object), + {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), + true <- actor == object_id do + cng + else + _e -> + cng + |> add_error(:object, "Can't be updated by this actor") + end + end end diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index adb56092d..770a8dcf8 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -641,5 +641,17 @@ test "returns an error if the actor can't announce the object", %{ test "validates a basic object", %{valid_update: valid_update} do assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) end + + test "returns an error if the object can't be updated by the actor", %{ + valid_update: valid_update + } do + other_user = insert(:user) + + update = + valid_update + |> Map.put("actor", other_user.ap_id) + + assert {:error, _cng} = ObjectValidator.validate(update, []) + end end end From b63646169dbed68814bdb867c4b8b3c88a3d2360 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko <suprunenko.s@gmail.com> Date: Fri, 19 Jun 2020 21:18:07 +0200 Subject: [PATCH 334/375] Add support for bot field in update_credentials --- CHANGELOG.md | 1 + lib/pleroma/user.ex | 1 + .../controllers/account_controller.ex | 3 + .../update_credentials_test.exs | 67 +++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee13904f..8a8798e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Add support for filtering replies in public and home timelines +- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials` - Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoint for status view. - OTP: Add command to reload emoji packs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f0ccc7c79..ae4f96aac 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -465,6 +465,7 @@ 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) + |> validate_inclusion(:actor_type, ["Person", "Service"]) |> put_fields() |> put_emoji() |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index c38c2b895..adbbac624 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -177,6 +177,9 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store]) |> Maps.put_if_present(:default_scope, params[:default_scope]) |> Maps.put_if_present(:default_scope, params["source"]["privacy"]) + |> Maps.put_if_present(:actor_type, params[:bot], fn bot -> + if bot, do: {:ok, "Service"}, else: {:ok, "Person"} + end) |> Maps.put_if_present(:actor_type, params[:actor_type]) changeset = User.update_changeset(user, user_params) 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 76e6d603a..f67d294ba 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 @@ -400,4 +400,71 @@ test "update fields when invalid request", %{conn: conn} do |> json_response_and_validate_schema(403) end end + + describe "Mark account as bot" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "changing actor_type to Service makes account a bot", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Service"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Service" + end + + test "changing actor_type to Person makes account a human", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Person"}) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + + test "changing actor_type to Application causes error", %{conn: conn} do + response = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Application"}) + |> json_response_and_validate_schema(403) + + assert %{"error" => "Invalid request"} == response + end + + test "changing bot field to true changes actor_type to Service", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{bot: "true"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Service" + end + + test "changing bot field to false changes actor_type to Person", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{bot: "false"}) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + + test "actor_type field has a higher priority than bot", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{ + actor_type: "Person", + bot: "true" + }) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + end end From ac0344dd24d520ab61e835b9caea97529f4c1dad Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko <suprunenko.s@gmail.com> Date: Fri, 19 Jun 2020 21:19:00 +0200 Subject: [PATCH 335/375] Only accounts with Service actor_type are considered as bots --- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 68beb69b8..6c40b8ccd 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -179,7 +179,7 @@ defp do_render("show.json", %{user: user} = opts) do 0 end - bot = user.actor_type in ["Application", "Service"] + bot = user.actor_type == "Service" emojis = Enum.map(user.emoji, fn {shortcode, raw_url} -> From 3d4cfc9c5f3969e08c32781385c86f310eba70a2 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Thu, 18 Jun 2020 19:32:03 +0200 Subject: [PATCH 336/375] Stop filling conversation field on incoming objects (legacy, unused) conversation field is still set for outgoing federation for compatibility. --- .../web/activity_pub/object_validators/note_validator.ex | 1 - lib/pleroma/web/activity_pub/transmogrifier.ex | 6 +++--- test/web/activity_pub/transmogrifier_test.exs | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index a10728ac6..56b93dde8 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -41,7 +41,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:announcements, {:array, :string}, default: []) # see if needed - field(:conversation, :string) field(:context_id, :string) end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 851f474b8..1c60ef8f5 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -172,8 +172,8 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) object |> Map.put("inReplyTo", replied_object.data["id"]) |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) - |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) |> Map.put("context", replied_object.data["context"] || object["conversation"]) + |> Map.drop(["conversation"]) else e -> Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") @@ -207,7 +207,7 @@ def fix_context(object) do object |> Map.put("context", context) - |> Map.put("conversation", context) + |> Map.drop(["conversation"]) end def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do @@ -458,7 +458,7 @@ def handle_incoming( to: data["to"], object: object, actor: user, - context: object["conversation"], + context: object["context"], local: false, published: data["published"], additional: diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 94d8552e8..47d6e843a 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1571,9 +1571,6 @@ test "returns modified object when allowed incoming reply", %{data: data} do assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" - assert modified_object["conversation"] == - "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26" - assert modified_object["context"] == "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26" end From 1a704e1f1e0acb73cbfb49acc4f614dd01799c46 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 20 Jun 2020 10:56:28 +0300 Subject: [PATCH 337/375] fix for packs pagination --- lib/pleroma/emoji/pack.ex | 6 +++--- .../emoji_pack_controller_test.exs | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 2dca21c93..787ff8141 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -32,8 +32,8 @@ defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size) defp paginate(entities, page, page_size) do entities - |> Enum.take(page * page_size) - |> Enum.take(-page_size) + |> Enum.chunk_every(page_size) + |> Enum.at(page - 1) end @spec show(keyword()) :: {:ok, t()} | {:error, atom()} @@ -470,7 +470,7 @@ defp list_packs_dir do # with the API so it should be sufficient with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do - {:ok, results} + {:ok, Enum.sort(results)} else {:create_dir, {:error, e}} -> {:error, :create_dir, e} {:ls, {:error, e}} -> {:error, :ls, e} diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index 91312c832..df58a5eb6 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -31,6 +31,11 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) assert resp["count"] == 3 + + assert resp["packs"] + |> Map.keys() + |> length() == 3 + shared = resp["packs"]["test_pack"] assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} assert Map.has_key?(shared["pack"], "download-sha256") @@ -47,7 +52,12 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> json_response_and_validate_schema(200) assert resp["count"] == 3 - [pack1] = Map.keys(resp["packs"]) + + packs = Map.keys(resp["packs"]) + + assert length(packs) == 1 + + [pack1] = packs resp = conn @@ -55,7 +65,9 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> json_response_and_validate_schema(200) assert resp["count"] == 3 - [pack2] = Map.keys(resp["packs"]) + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack2] = packs resp = conn @@ -63,7 +75,9 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> json_response_and_validate_schema(200) assert resp["count"] == 3 - [pack3] = Map.keys(resp["packs"]) + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack3] = packs assert [pack1, pack2, pack3] |> Enum.uniq() |> length() == 3 end From 4cb7b1ebc6b255faae635f6138bf90264e84e1fb Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Sat, 20 Jun 2020 09:34:34 +0000 Subject: [PATCH 338/375] Apply suggestion to lib/mix/tasks/pleroma/config.ex --- lib/mix/tasks/pleroma/config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 65691f9c1..d5129d410 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -52,7 +52,7 @@ def migrate_to_db(file_path \\ nil) do defp do_migrate_to_db(config_file) do if File.exists?(config_file) do - shell_info("Running migrate settings from file: #{Path.expand(config_file)}") + shell_info("Migrating settings from file: #{Path.expand(config_file)}") Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") From 15ba5392584a2d4e8129a99e825f5025e57e6ebd Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Sat, 20 Jun 2020 11:39:06 +0200 Subject: [PATCH 339/375] =?UTF-8?q?cheatsheet.md:=20no=5Fattachment=5Flink?= =?UTF-8?q?s=20=E2=86=92=20attachment=5Flinks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 6ebdab546..7e5f1cd29 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -60,7 +60,7 @@ To add configuration to your config file, you can copy it from the base config. older software for theses nicknames. * `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature. * `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow. -* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses. +* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses. * `welcome_message`: A message that will be send to a newly registered users as a direct message. * `welcome_user_nickname`: The nickname of the local user that sends the welcome message. * `max_report_comment_size`: The maximum size of the report comment (Default: `1000`). From 0e789bc55fed24fd913d6bf1a5c6be135320b0c9 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Sat, 20 Jun 2020 09:39:50 +0000 Subject: [PATCH 340/375] Apply suggestion to lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex --- .../web/api_spec/operations/pleroma_emoji_pack_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index da7cc5154..caa849721 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -44,7 +44,7 @@ def index_operation do :page_size, :query, %Schema{type: :integer, default: 50}, - "Number of statuses to return" + "Number of emoji packs to return" ) ], responses: %{ From c5863438ba9079a01a832fe48e203907fe5b37cd Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 20 Jun 2020 13:53:57 +0300 Subject: [PATCH 341/375] proper error codes for error in adminFE --- docs/API/admin_api.md | 54 ++++++++++--------- .../controllers/admin_api_controller.ex | 29 +++++----- .../controllers/fallback_controller.ex | 6 +++ .../controllers/admin_api_controller_test.exs | 4 +- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index c7f56cf5f..b6fb43dcb 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -488,35 +488,39 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ### 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` +* 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: +* Responses: + +Status: 200 ```json {"status": "success"} ``` +Status: 400 + ```json {"errors": {"actor_type": "is invalid"}, @@ -525,8 +529,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` +Status: 404 + ```json -{"error": "Unable to update user."} +{"error": "Not found"} ``` ## `GET /api/pleroma/admin/reports` diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 5cbf0dd4f..db2413dfe 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -111,8 +111,7 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) action: "delete" }) - conn - |> json(nicknames) + json(conn, nicknames) end def user_follow(%{assigns: %{user: admin}} = conn, %{ @@ -131,8 +130,7 @@ def user_follow(%{assigns: %{user: admin}} = conn, %{ }) end - conn - |> json("ok") + json(conn, "ok") end def user_unfollow(%{assigns: %{user: admin}} = conn, %{ @@ -151,8 +149,7 @@ def user_unfollow(%{assigns: %{user: admin}} = conn, %{ }) end - conn - |> json("ok") + json(conn, "ok") end def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do @@ -191,8 +188,7 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do action: "create" }) - conn - |> json(res) + json(conn, res) {:error, id, changeset, _} -> res = @@ -363,8 +359,8 @@ defp maybe_parse_filters(filters) do filters |> String.split(",") |> Enum.filter(&Enum.member?(@filters, &1)) - |> Enum.map(&String.to_atom(&1)) - |> Enum.into(%{}, &{&1, true}) + |> Enum.map(&String.to_atom/1) + |> Map.new(&{&1, true}) end def right_add_multiple(%{assigns: %{user: admin}} = conn, %{ @@ -568,10 +564,10 @@ def update_user_credentials( {:error, changeset} -> errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end) - json(conn, %{errors: errors}) + {:errors, errors} _ -> - json(conn, %{error: "Unable to update user."}) + {:error, :not_found} end end @@ -616,7 +612,7 @@ defp configurable_from_database do def reload_emoji(conn, _params) do Pleroma.Emoji.reload() - conn |> json("ok") + json(conn, "ok") end def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do @@ -630,7 +626,7 @@ def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames} action: "confirm_email" }) - conn |> json("") + json(conn, "") end def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do @@ -644,14 +640,13 @@ def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" = action: "resend_confirmation_email" }) - conn |> json("") + json(conn, "") end def stats(conn, _) do count = Stats.get_status_visibility_count() - conn - |> json(%{"status_visibility" => count}) + json(conn, %{"status_visibility" => count}) end defp page_params(params) do diff --git a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex index 82965936d..34d90db07 100644 --- a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex @@ -17,6 +17,12 @@ def call(conn, {:error, reason}) do |> json(%{error: reason}) end + def call(conn, {:errors, errors}) do + conn + |> put_status(:bad_request) + |> json(%{errors: errors}) + end + def call(conn, {:param_cast, _}) do conn |> put_status(:bad_request) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index e3d3ccb8d..3a3eb822d 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1599,14 +1599,14 @@ test "changes actor type from permitted list", %{conn: conn, user: user} do assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ "actor_type" => "Application" }) - |> json_response(200) == %{"errors" => %{"actor_type" => "is invalid"}} + |> json_response(400) == %{"errors" => %{"actor_type" => "is invalid"}} end test "update non existing user", %{conn: conn} do assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{ "password" => "new_password" }) - |> json_response(200) == %{"error" => "Unable to update user."} + |> json_response(404) == %{"error" => "Not found"} end end From b5f13af7ba66924f6aed448bd519f6becc269922 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 20 Jun 2020 10:59:08 +0000 Subject: [PATCH 342/375] Apply suggestion to lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex --- .../web/api_spec/operations/pleroma_emoji_pack_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index caa849721..b2b4f8713 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -70,7 +70,7 @@ def show_operation do :page_size, :query, %Schema{type: :integer, default: 30}, - "Number of statuses to return" + "Number of emoji to return" ) ], responses: %{ From 35e9282ffdafd8a04d1c09ec5eff3f176bb389de Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 22 Jun 2020 10:35:11 +0200 Subject: [PATCH 343/375] HellthreadPolicy: Restrict to Notes and Articles. --- .../web/activity_pub/mrf/hellthread_policy.ex | 7 +++++-- .../mrf/hellthread_policy_test.exs | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index 1764bc789..f6b2c4415 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -13,8 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do defp delist_message(message, threshold) when threshold > 0 do follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address + to = message["to"] || [] + cc = message["cc"] || [] - follower_collection? = Enum.member?(message["to"] ++ message["cc"], follower_collection) + follower_collection? = Enum.member?(to ++ cc, follower_collection) message = case get_recipient_count(message) do @@ -71,7 +73,8 @@ defp get_recipient_count(message) do end @impl true - def filter(%{"type" => "Create"} = message) do + def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message) + when object_type in ~w{Note Article} do reject_threshold = Pleroma.Config.get( [:mrf_hellthread, :reject_threshold], diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs index 95ef0b168..6e9daa7f9 100644 --- a/test/web/activity_pub/mrf/hellthread_policy_test.exs +++ b/test/web/activity_pub/mrf/hellthread_policy_test.exs @@ -8,6 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy + alias Pleroma.Web.CommonAPI + setup do user = insert(:user) @@ -20,7 +22,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do "https://instance.tld/users/user1", "https://instance.tld/users/user2", "https://instance.tld/users/user3" - ] + ], + "object" => %{ + "type" => "Note" + } } [user: user, message: message] @@ -28,6 +33,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do setup do: clear_config(:mrf_hellthread) + test "doesn't die on chat messages" do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) + + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post_chat_message(user, other_user, "moin") + + assert {:ok, _} = filter(activity.data) + end + describe "reject" do test "rejects the message if the recipient count is above reject_threshold", %{ message: message From 9f7ee5dfa283f8db9a5fcb006630674263425ac8 Mon Sep 17 00:00:00 2001 From: Ilja <domainepublic@spectraltheorem.be> Date: Mon, 22 Jun 2020 11:41:22 +0200 Subject: [PATCH 344/375] Add include for the "Further reading" section * I added an include and use this include for the installation guides that already had this section * I added the "Further reading" section as well as te "Questions" section to the English guides that didn't have it yet * I added a first point "How Federation Works/Why is my Federated Timeline empty?" to link to lains blogpost about this because we still get this question a lot in the #pleroma support channel * I reordered the list a bit --- docs/installation/alpine_linux_en.md | 5 +---- docs/installation/arch_linux_en.md | 5 +---- docs/installation/debian_based_en.md | 5 +---- docs/installation/debian_based_jp.md | 5 +---- docs/installation/further_reading.include | 5 +++++ docs/installation/gentoo_en.md | 5 +---- docs/installation/netbsd_en.md | 8 ++++++++ docs/installation/openbsd_en.md | 8 ++++++++ docs/installation/otp_en.md | 5 +---- 9 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 docs/installation/further_reading.include diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 2a9b8f6ff..c726d559f 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -225,10 +225,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress #### Further reading -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) +{! backend/installation/further_reading.include !} ## Questions diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 8370986ad..bf9cfb488 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -200,10 +200,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress #### Further reading -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) +{! backend/installation/further_reading.include !} ## Questions diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 2c20d521a..8ae5044b5 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -186,10 +186,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress #### Further reading -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) +{! backend/installation/further_reading.include !} ## Questions diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 1e5a9be91..42e91cda7 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -175,10 +175,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress #### その他の設定とカスタマイズ -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) +{! backend/installation/further_reading.include !} ## 質問ある? diff --git a/docs/installation/further_reading.include b/docs/installation/further_reading.include new file mode 100644 index 000000000..46752c722 --- /dev/null +++ b/docs/installation/further_reading.include @@ -0,0 +1,5 @@ +* [How Federation Works/Why is my Federated Timeline empty?](https://blog.soykaf.com/post/how-federation-works/) +* [Backup your instance](../administration/backup.md) +* [Updating your instance](../administration/updating.md) +* [Hardening your instance](../configuration/hardening.md) +* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 1e61373cc..32152aea7 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -283,10 +283,7 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a #### Further reading -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) +{! backend/installation/further_reading.include !} ## Questions diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index 6a922a27e..3626acc69 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -196,3 +196,11 @@ incorrect timestamps. You should have ntpd running. ## Instances running NetBSD * <https://catgirl.science> + +#### Further reading + +{! backend/installation/further_reading.include !} + +## Questions + +Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index e8c5d844c..5dbe24f75 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -242,3 +242,11 @@ If your instance is up and running, you can create your first user with administ ``` LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin ``` + +#### Further reading + +{! backend/installation/further_reading.include !} + +## Questions + +Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 86135cd20..e4f822d1c 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -270,10 +270,7 @@ This will create an account withe the username of 'joeuser' with the email addre ## Further reading -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) +{! backend/installation/further_reading.include !} ## Questions From 31a4d42ce0470d74417279a855192294650cff97 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 22 Jun 2020 13:15:37 +0200 Subject: [PATCH 345/375] SideEffects: Handle user updating. --- lib/pleroma/web/activity_pub/side_effects.ex | 12 ++++++++++++ test/web/activity_pub/side_effects_test.exs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 1a1cc675c..09fd7d7c9 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -20,6 +20,18 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do def handle(object, meta \\ []) + # Tasks this handles: + # Update the user + def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do + {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) + + User.get_by_ap_id(updated_object["id"]) + |> User.remote_user_changeset(new_user_data) + |> User.update_and_set_cache() + + {:ok, object, meta} + end + # Tasks this handles: # - Add like to object # - Set up notification diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 6bbbaae87..1d7c2736b 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -64,6 +64,22 @@ test "it streams out notifications and streams" do end end + describe "update users" do + setup do + user = insert(:user) + {:ok, update_data, []} = Builder.update(user, %{"id" => user.ap_id, "name" => "new name!"}) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + %{user: user, update_data: update_data, update: update} + end + + test "it updates the user", %{user: user, update: update} do + {:ok, _, _} = SideEffects.handle(update) + user = User.get_by_id(user.id) + assert user.name == "new name!" + end + end + describe "delete objects" do setup do user = insert(:user) From 9438f83f83305f101b9fed65f68a5b9fd622bcbb Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 22 Jun 2020 13:16:05 +0200 Subject: [PATCH 346/375] Transmogrifier: Handle `Update` with the pipeline. --- .../web/activity_pub/transmogrifier.ex | 33 +++---------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 851f474b8..8165218ee 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -684,35 +684,12 @@ def handle_incoming(%{"type" => type} = data, _options) end def handle_incoming( - %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} = - data, + %{"type" => "Update"} = data, _options - ) - when object_type in [ - "Person", - "Application", - "Service", - "Organization" - ] do - with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do - {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) - - actor - |> User.remote_user_changeset(new_user_data) - |> User.update_and_set_cache() - - ActivityPub.update(%{ - local: false, - to: data["to"] || [], - cc: data["cc"] || [], - object: object, - actor: actor_id, - activity_id: data["id"] - }) - else - e -> - Logger.error(e) - :error + ) do + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), + {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity} end end From 1e7ca2443011f65aa766c3ddd5cd1203e79db50b Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 22 Jun 2020 13:23:21 +0200 Subject: [PATCH 347/375] Update Handling Test: Fix for re-used update ids. --- .../transmogrifier/user_update_handling_test.exs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs index 8e5d3b883..64636656c 100644 --- a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -106,11 +106,13 @@ test "it works with custom profile fields" do Pleroma.Config.put([:instance, :max_remote_account_fields], 2) update_data = - put_in(update_data, ["object", "attachment"], [ + update_data + |> put_in(["object", "attachment"], [ %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} ]) + |> Map.put("id", update_data["id"] <> ".") {:ok, _} = Transmogrifier.handle_incoming(update_data) @@ -121,7 +123,10 @@ test "it works with custom profile fields" do %{"name" => "foo1", "value" => "updated"} ] - update_data = put_in(update_data, ["object", "attachment"], []) + update_data = + update_data + |> put_in(["object", "attachment"], []) + |> Map.put("id", update_data["id"] <> ".") {:ok, _} = Transmogrifier.handle_incoming(update_data) From 4f5af68b3e96c5b5b62185f86af39fc2f8955e10 Mon Sep 17 00:00:00 2001 From: Ben Is <srsbzns@cock.li> Date: Fri, 19 Jun 2020 14:33:58 +0000 Subject: [PATCH 348/375] Added translation using Weblate (Italian) --- priv/gettext/it/LC_MESSAGES/errors.po | 578 ++++++++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 priv/gettext/it/LC_MESSAGES/errors.po diff --git a/priv/gettext/it/LC_MESSAGES/errors.po b/priv/gettext/it/LC_MESSAGES/errors.po new file mode 100644 index 000000000..18ec03c83 --- /dev/null +++ b/priv/gettext/it/LC_MESSAGES/errors.po @@ -0,0 +1,578 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-19 14:33+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 2.5.1\n" + +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:421 +#, elixir-format +msgid "Account not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:249 +#, elixir-format +msgid "Already voted" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:360 +#, elixir-format +msgid "Bad request" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 +#, elixir-format +msgid "Can't delete object" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196 +#, elixir-format +msgid "Can't delete this post" +msgstr "" + +#: lib/pleroma/web/controller_helper.ex:95 +#: lib/pleroma/web/controller_helper.ex:101 +#, elixir-format +msgid "Can't display this activity" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254 +#, elixir-format +msgid "Can't find user" +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114 +#, elixir-format +msgid "Can't get favorites" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437 +#, elixir-format +msgid "Can't like object" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:556 +#, elixir-format +msgid "Cannot post an empty status without attachments" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:504 +#, elixir-format +msgid "Comment must be up to %{max_size} characters" +msgstr "" + +#: lib/pleroma/config/config_db.ex:222 +#, elixir-format +msgid "Config with params %{params} not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:95 +#, elixir-format +msgid "Could not delete" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:141 +#, elixir-format +msgid "Could not favorite" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:370 +#, elixir-format +msgid "Could not pin" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:112 +#, elixir-format +msgid "Could not repeat" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:188 +#, elixir-format +msgid "Could not unfavorite" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:380 +#, elixir-format +msgid "Could not unpin" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:126 +#, elixir-format +msgid "Could not unrepeat" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:428 +#: lib/pleroma/web/common_api/common_api.ex:437 +#, elixir-format +msgid "Could not update state" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202 +#, elixir-format +msgid "Error." +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:106 +#, elixir-format +msgid "Invalid CAPTCHA" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117 +#: lib/pleroma/web/oauth/oauth_controller.ex:569 +#, elixir-format +msgid "Invalid credentials" +msgstr "" + +#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 +#, elixir-format +msgid "Invalid credentials." +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:265 +#, elixir-format +msgid "Invalid indices" +msgstr "" + +#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147 +#, elixir-format +msgid "Invalid parameters" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:411 +#, elixir-format +msgid "Invalid password." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187 +#, elixir-format +msgid "Invalid request" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:109 +#, elixir-format +msgid "Kocaptcha service unavailable" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113 +#, elixir-format +msgid "Missing parameters" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:540 +#, elixir-format +msgid "No such conversation" +msgstr "" + +#: lib/pleroma/web/admin_api/admin_api_controller.ex:439 +#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507 +#, elixir-format +msgid "No such permission_group" +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:74 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135 +#: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143 +#, elixir-format +msgid "Not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:241 +#, elixir-format +msgid "Poll's author can't vote" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:290 +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71 +#, elixir-format +msgid "Record not found" +msgstr "" + +#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153 +#: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32 +#: lib/pleroma/web/ostatus/ostatus_controller.ex:149 +#, elixir-format +msgid "Something went wrong" +msgstr "" + +#: lib/pleroma/web/common_api/activity_draft.ex:107 +#, elixir-format +msgid "The message visibility must be direct" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:566 +#, elixir-format +msgid "The status is over the character limit" +msgstr "" + +#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31 +#, elixir-format +msgid "This resource requires authentication." +msgstr "" + +#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 +#, elixir-format +msgid "Throttled" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:266 +#, elixir-format +msgid "Too many choices" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442 +#, elixir-format +msgid "Unhandled activity type" +msgstr "" + +#: lib/pleroma/web/admin_api/admin_api_controller.ex:536 +#, elixir-format +msgid "You can't revoke your own admin status." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:218 +#: lib/pleroma/web/oauth/oauth_controller.ex:309 +#, elixir-format +msgid "Your account is currently disabled" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:180 +#: lib/pleroma/web/oauth/oauth_controller.ex:332 +#, elixir-format +msgid "Your login is missing a confirmed e-mail address" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389 +#, elixir-format +msgid "can't read inbox of %{nickname} as %{as_nickname}" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472 +#, elixir-format +msgid "can't update outbox of %{nickname} as %{as_nickname}" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:388 +#, elixir-format +msgid "conversation is already muted" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491 +#, elixir-format +msgid "error" +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29 +#, elixir-format +msgid "mascots can only be images" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60 +#, elixir-format +msgid "not found" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:395 +#, elixir-format +msgid "Bad OAuth request." +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:115 +#, elixir-format +msgid "CAPTCHA already used" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:112 +#, elixir-format +msgid "CAPTCHA expired" +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:55 +#, elixir-format +msgid "Failed" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:411 +#, elixir-format +msgid "Failed to authenticate: %{message}." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:442 +#, elixir-format +msgid "Failed to set up user account." +msgstr "" + +#: lib/pleroma/plugs/oauth_scopes_plug.ex:38 +#, elixir-format +msgid "Insufficient permissions: %{permissions}." +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:94 +#, elixir-format +msgid "Internal Error" +msgstr "" + +#: lib/pleroma/web/oauth/fallback_controller.ex:22 +#: lib/pleroma/web/oauth/fallback_controller.ex:29 +#, elixir-format +msgid "Invalid Username/Password" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:118 +#, elixir-format +msgid "Invalid answer data" +msgstr "" + +#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128 +#, elixir-format +msgid "Nodeinfo schema version not handled" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:169 +#, elixir-format +msgid "This action is outside the authorized scopes" +msgstr "" + +#: lib/pleroma/web/oauth/fallback_controller.ex:14 +#, elixir-format +msgid "Unknown error, please check the details and try again." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:116 +#: lib/pleroma/web/oauth/oauth_controller.ex:155 +#, elixir-format +msgid "Unlisted redirect_uri." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:391 +#, elixir-format +msgid "Unsupported OAuth provider: %{provider}." +msgstr "" + +#: lib/pleroma/uploaders/uploader.ex:72 +#, elixir-format +msgid "Uploader callback timeout" +msgstr "" + +#: lib/pleroma/web/uploader_controller.ex:23 +#, elixir-format +msgid "bad request" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:103 +#, elixir-format +msgid "CAPTCHA Error" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:200 +#, elixir-format +msgid "Could not add reaction emoji" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:211 +#, elixir-format +msgid "Could not remove reaction emoji" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:129 +#, elixir-format +msgid "Invalid CAPTCHA (Missing parameter: %{name})" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 +#, elixir-format +msgid "List not found" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124 +#, elixir-format +msgid "Missing parameter: %{name}" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:207 +#: lib/pleroma/web/oauth/oauth_controller.ex:322 +#, elixir-format +msgid "Password reset is required" +msgstr "" + +#: lib/pleroma/tests/auth_test_controller.ex:9 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6 +#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/fallback_redirect_controller.ex:6 +#: lib/pleroma/web/feed/tag_controller.ex:6 lib/pleroma/web/feed/user_controller.ex:6 +#: lib/pleroma/web/mailer/subscription_controller.ex:2 lib/pleroma/web/masto_fe_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 +#: lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 +#: lib/pleroma/web/oauth/fallback_controller.ex:6 lib/pleroma/web/oauth/mfa_controller.ex:10 +#: lib/pleroma/web/oauth/oauth_controller.ex:6 lib/pleroma/web/ostatus/ostatus_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:2 +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6 +#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6 +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6 +#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6 +#, elixir-format +msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." +msgstr "" + +#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 +#, elixir-format +msgid "Two-factor authentication enabled, you must use a access token." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210 +#, elixir-format +msgid "Unexpected error occurred while adding file to pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138 +#, elixir-format +msgid "Unexpected error occurred while creating pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278 +#, elixir-format +msgid "Unexpected error occurred while removing file from pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250 +#, elixir-format +msgid "Unexpected error occurred while updating file in pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179 +#, elixir-format +msgid "Unexpected error occurred while updating pack metadata." +msgstr "" + +#: lib/pleroma/plugs/user_is_admin_plug.ex:40 +#, elixir-format +msgid "User is not an admin or OAuth admin scope is not granted." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 +#, elixir-format +msgid "Web push subscription is disabled on this Pleroma instance" +msgstr "" + +#: lib/pleroma/web/admin_api/admin_api_controller.ex:502 +#, elixir-format +msgid "You can't revoke your own admin/moderator status." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105 +#, elixir-format +msgid "authorization required for timeline view" +msgstr "" From 68c812eb2ecbfb1d582925c15d90bd1bd4e62b4b Mon Sep 17 00:00:00 2001 From: Ben Is <srsbzns@cock.li> Date: Fri, 19 Jun 2020 14:35:01 +0000 Subject: [PATCH 349/375] Translated using Weblate (Italian) Currently translated at 0.9% (1 of 106 strings) Translation: Pleroma/Pleroma backend Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma/it/ --- priv/gettext/it/LC_MESSAGES/errors.po | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/priv/gettext/it/LC_MESSAGES/errors.po b/priv/gettext/it/LC_MESSAGES/errors.po index 18ec03c83..726be628b 100644 --- a/priv/gettext/it/LC_MESSAGES/errors.po +++ b/priv/gettext/it/LC_MESSAGES/errors.po @@ -3,14 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-19 14:33+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" +"PO-Revision-Date: 2020-06-19 20:38+0000\n" +"Last-Translator: Ben Is <srsbzns@cock.li>\n" +"Language-Team: Italian <https://translate.pleroma.social/projects/pleroma/" +"pleroma/it/>\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Translate Toolkit 2.5.1\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.0.4\n" ## This file is a PO Template file. ## @@ -23,7 +25,7 @@ msgstr "" ## effect: edit them in PO (`.po`) files instead. ## From Ecto.Changeset.cast/4 msgid "can't be blank" -msgstr "" +msgstr "non può essere nullo" ## From Ecto.Changeset.unique_constraint/3 msgid "has already been taken" From e785cd5caeab2c610f12a9071cade31a6b4549a4 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 22 Jun 2020 13:59:45 +0200 Subject: [PATCH 350/375] ActivityPub: Remove `update` and switch to pipeline. --- lib/pleroma/web/activity_pub/activity_pub.ex | 22 -------- lib/pleroma/web/activity_pub/side_effects.ex | 18 +++++-- .../controllers/account_controller.ex | 53 +++++++++++-------- test/web/activity_pub/activity_pub_test.exs | 46 ---------------- test/web/activity_pub/side_effects_test.exs | 9 ++++ 5 files changed, 52 insertions(+), 96 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 3e4f3ad30..4cc9fe16c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -321,28 +321,6 @@ defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do end end - @spec update(map()) :: {:ok, Activity.t()} | {:error, any()} - def update(%{to: to, cc: cc, actor: actor, object: object} = params) do - local = !(params[:local] == false) - activity_id = params[:activity_id] - - data = - %{ - "to" => to, - "cc" => cc, - "type" => "Update", - "actor" => actor, - "object" => object - } - |> Maps.put_if_present("id", activity_id) - - with {:ok, activity} <- insert(data, local), - _ <- notify_and_stream(activity), - :ok <- maybe_federate(activity) do - {:ok, activity} - end - end - @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: {:ok, Activity.t()} | {:error, any()} def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 09fd7d7c9..de143b8f0 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -21,13 +21,21 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do def handle(object, meta \\ []) # Tasks this handles: - # Update the user + # - Update the user + # + # For a local user, we also get a changeset with the full information, so we + # can update non-federating, non-activitypub settings as well. def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do - {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) + if changeset = Keyword.get(meta, :user_update_changeset) do + changeset + |> User.update_and_set_cache() + else + {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) - User.get_by_ap_id(updated_object["id"]) - |> User.remote_user_changeset(new_user_data) - |> User.update_and_set_cache() + User.get_by_ap_id(updated_object["id"]) + |> User.remote_user_changeset(new_user_data) + |> User.update_and_set_cache() + end {:ok, object, meta} end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index c38c2b895..f0499621a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -20,6 +20,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -179,34 +181,39 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p |> Maps.put_if_present(:default_scope, params["source"]["privacy"]) |> Maps.put_if_present(:actor_type, params[:actor_type]) - changeset = User.update_changeset(user, user_params) - - with {:ok, user} <- User.update_and_set_cache(changeset) do - user - |> build_update_activity_params() - |> ActivityPub.update() - - render(conn, "show.json", user: user, for: user, with_pleroma_settings: true) + # What happens here: + # + # We want to update the user through the pipeline, but the ActivityPub + # update information is not quite enough for this, because this also + # contains local settings that don't federate and don't even appear + # in the Update activity. + # + # So we first build the normal local changeset, then apply it to the + # user data, but don't persist it. With this, we generate the object + # data for our update activity. We feed this and the changeset as meta + # inforation into the pipeline, where they will be properly updated and + # federated. + with changeset <- User.update_changeset(user, user_params), + {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update), + updated_object <- + Pleroma.Web.ActivityPub.UserView.render("user.json", user: user) + |> Map.delete("@context"), + {:ok, update_data, []} <- Builder.update(user, updated_object), + {:ok, _update, _} <- + Pipeline.common_pipeline(update_data, + local: true, + user_update_changeset: changeset + ) do + render(conn, "show.json", + user: unpersisted_user, + for: unpersisted_user, + with_pleroma_settings: true + ) else _e -> render_error(conn, :forbidden, "Invalid request") end end - # Hotfix, handling will be redone with the pipeline - defp build_update_activity_params(user) do - object = - Pleroma.Web.ActivityPub.UserView.render("user.json", user: user) - |> Map.delete("@context") - - %{ - local: true, - to: [user.follower_address], - cc: [], - object: object, - actor: user.ap_id - } - end - defp normalize_fields_attributes(fields) do if Enum.all?(fields, &is_tuple/1) do Enum.map(fields, fn {_, v} -> v end) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 7693f6400..ce35c9605 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1092,52 +1092,6 @@ test "it filters broken threads" do end end - describe "update" do - setup do: clear_config([:instance, :max_pinned_statuses]) - - test "it creates an update activity with the new user data" do - user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) - user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) - - {:ok, update} = - ActivityPub.update(%{ - actor: user_data["id"], - to: [user.follower_address], - cc: [], - object: user_data - }) - - assert update.data["actor"] == user.ap_id - assert update.data["to"] == [user.follower_address] - assert embedded_object = update.data["object"] - assert embedded_object["id"] == user_data["id"] - assert embedded_object["type"] == user_data["type"] - end - end - - test "returned pinned statuses" do - Config.put([:instance, :max_pinned_statuses], 3) - user = insert(:user) - - {:ok, activity_one} = CommonAPI.post(user, %{status: "HI!!!"}) - {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) - {:ok, activity_three} = CommonAPI.post(user, %{status: "HI!!!"}) - - CommonAPI.pin(activity_one.id, user) - user = refresh_record(user) - - CommonAPI.pin(activity_two.id, user) - user = refresh_record(user) - - CommonAPI.pin(activity_three.id, user) - user = refresh_record(user) - - activities = ActivityPub.fetch_user_activities(user, nil, %{pinned: true}) - - assert 3 = length(activities) - end - describe "flag/1" do setup do reporter = insert(:user) diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 1d7c2736b..12c9ef1da 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -78,6 +78,15 @@ test "it updates the user", %{user: user, update: update} do user = User.get_by_id(user.id) assert user.name == "new name!" end + + test "it uses a given changeset to update", %{user: user, update: update} do + changeset = Ecto.Changeset.change(user, %{default_scope: "direct"}) + + assert user.default_scope == "public" + {:ok, _, _} = SideEffects.handle(update, user_update_changeset: changeset) + user = User.get_by_id(user.id) + assert user.default_scope == "direct" + end end describe "delete objects" do From b05f795326b77edd881ffea2c004d7ca0ddd7df9 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Mon, 22 Jun 2020 14:02:29 +0200 Subject: [PATCH 351/375] Credo fixes --- .../web/mastodon_api/controllers/account_controller.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index f0499621a..d4605c518 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -20,8 +20,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -186,8 +186,8 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p # We want to update the user through the pipeline, but the ActivityPub # update information is not quite enough for this, because this also # contains local settings that don't federate and don't even appear - # in the Update activity. - # + # in the Update activity. + # # So we first build the normal local changeset, then apply it to the # user data, but don't persist it. With this, we generate the object # data for our update activity. We feed this and the changeset as meta From a3b10a4f643d574b84ecee51fb891e26e7f0dbc2 Mon Sep 17 00:00:00 2001 From: Ilja <domainepublic@spectraltheorem.be> Date: Mon, 22 Jun 2020 14:13:30 +0200 Subject: [PATCH 352/375] Fix 1586 Docs: provide a index.md * I renamed the introduction.md to index.md * I moved over the FE parts to an index file in the FE repo (will do an MR in the FE repo to actually add it) * While I was at it, I also fixed some broken links --- docs/index.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..fb9e32816 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +# Introduction to Pleroma +## What is Pleroma? +Pleroma is a federated social networking platform, compatible with Mastodon and other ActivityPub implementations. It is free software licensed under the AGPLv3. +It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. +It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. +One account on an instance is enough to talk to the entire fediverse! + +## How can I use it? + +Pleroma instances are already widely deployed, a list can be found at <https://the-federation.info/pleroma> and <https://fediverse.network/pleroma>. + +If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too! +Installation instructions can be found in the installation section of these docs. + +## I got an account, now what? +Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. <https://pleroma.soykaf.com>) and login with your username and password. (If you don't have an account yet, click on Register) + +### Pleroma-FE +The default front-end used by Pleroma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](../frontend). + +### Mastodon interface +If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! +Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! +The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation. + +Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. From 1e089cdf2905309a5450e2acb32aa6b35a928c29 Mon Sep 17 00:00:00 2001 From: Ilja <domainepublic@spectraltheorem.be> Date: Mon, 22 Jun 2020 14:18:55 +0200 Subject: [PATCH 353/375] I forgot to git add some files, oops (should be squashed with MR) --- .../howto_theming_your_instance.md | 2 +- docs/dev.md | 2 +- docs/introduction.md | 65 ------------------- 3 files changed, 2 insertions(+), 67 deletions(-) delete mode 100644 docs/introduction.md diff --git a/docs/configuration/howto_theming_your_instance.md b/docs/configuration/howto_theming_your_instance.md index d0daf5b25..cfa00f538 100644 --- a/docs/configuration/howto_theming_your_instance.md +++ b/docs/configuration/howto_theming_your_instance.md @@ -60,7 +60,7 @@ Example of `my-awesome-theme.json` where we add the name "My Awesome Theme" ### Set as default theme -Now we can set the new theme as default in the [Pleroma FE configuration](General-tips-for-customizing-Pleroma-FE.md). +Now we can set the new theme as default in the [Pleroma FE configuration](../../../frontend/CONFIGURATION). Example of adding the new theme in the back-end config files ```elixir diff --git a/docs/dev.md b/docs/dev.md index f1b4cbf8b..9c749c17c 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -20,4 +20,4 @@ This document contains notes and guidelines for Pleroma developers. ## Auth-related configuration, OAuth consumer mode etc. -See `Authentication` section of [`docs/configuration/cheatsheet.md`](docs/configuration/cheatsheet.md#authentication). +See `Authentication` section of [the configuration cheatsheet](configuration/cheatsheet.md#authentication). diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100644 index a915c143c..000000000 --- a/docs/introduction.md +++ /dev/null @@ -1,65 +0,0 @@ -# Introduction to Pleroma -## What is Pleroma? -Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3. -It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. -It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. -One account on an instance is enough to talk to the entire fediverse! - -## How can I use it? - -Pleroma instances are already widely deployed, a list can be found at <http://distsn.org/pleroma-instances.html>. Information on all existing fediverse instances can be found at <https://fediverse.network/>. - -If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too! -Installation instructions can be found in the installation section of these docs. - -## I got an account, now what? -Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. <https://pleroma.soykaf.com>) and login with your username and password. (If you don't have an account yet, click on Register) - -At this point you will have two columns in front of you. - -### Left column - -- first block: here you can see your avatar, your nickname and statistics (Statuses, Following, Followers). Clicking your profile pic will open your profile. -Under that you have a text form which allows you to post new statuses. The number on the bottom of the text form is a character counter, every instance can have a different character limit (the default is 5000). -If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. -Under the text form there are also several visibility options and there is the option to use rich text. -Under that the icon on the left is for uploading media files and attach them to your post. There is also an emoji-picker and an option to post a poll. -To post your status, simply press Submit. -On the top right you will also see a wrench icon. This opens your personal settings. - -- second block: Here you can switch between the different timelines: - - Timeline: all the people that you follow - - Interactions: here you can switch between different timelines where there was interaction with your account. There is Mentions, Repeats and Favorites, and New follows - - Direct Messages: these are the Direct Messages sent to you - - Public Timeline: all the statutes from the local instance - - The Whole Known Network: all public posts the instance knows about, both local and remote! - - About: This isn't a Timeline but shows relevant info about the instance. You can find a list of the moderators and admins, Terms of Service, MRF policies and enabled features. -- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable and by default has links to the pleroma-fe and Mastodon-fe. -- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses. - -### Right column -This is where the interesting stuff happens! -Depending on the timeline you will see different statuses, but each status has a standard structure: - -- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile. -- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime! -- An arrow icon allows you to open the status on the instance where it's originating from. -- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person. -- Three buttons (left to right): Reply, Repeat, Favorite. There is also a forth button, this is a dropdown menu for simple moderation like muting the conversation or, if you have moderation rights, delete the status from the server. - -### Top right - -- The magnifier icon opens the search screen where you can search for statuses, people and hashtags. It's also possible to import statusses from remote servers by pasting the url to the post in the search field. -- The gear icon gives you general settings -- If you have admin rights, you'll see an icon that opens the admin interface -- The last icon is to log out - -### Bottom right -On the bottom right you have a chatbox. Here you can communicate with people on the same instance in realtime. It is local-only, for now, but there are plans to make it extendable to the entire fediverse! - -### Mastodon interface -If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! -Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! -The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation. - -Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. From 499324f7bee55de4e08647f71fd4adbfd4bd039f Mon Sep 17 00:00:00 2001 From: Ilja <domainepublic@spectraltheorem.be> Date: Mon, 22 Jun 2020 14:22:23 +0200 Subject: [PATCH 354/375] Removed a space that was too much --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index fb9e32816..1a90d0a8d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,4 +23,4 @@ If the Pleroma interface isn't your thing, or you're just trying something new b Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation. -Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. +Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. From b0a40fc2e42a186fc6bb383621f291411b2a81be Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Mon, 22 Jun 2020 17:27:49 +0300 Subject: [PATCH 355/375] added verify RUM settings before start app --- lib/pleroma/application.ex | 2 +- lib/pleroma/application_requirements.ex | 103 ++++++++++++++++++ lib/pleroma/repo.ex | 37 +------ ...510135645_add_fts_index_to_objects_two.exs | 39 +++---- test/application_requirements_test.exs | 67 ++++++++++++ test/repo_test.exs | 34 ------ 6 files changed, 189 insertions(+), 93 deletions(-) create mode 100644 lib/pleroma/application_requirements.ex create mode 100644 test/application_requirements_test.exs diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9d3d92b38..c30e5aadf 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -39,7 +39,7 @@ def start(_type, _args) do Pleroma.HTML.compile_scrubbers() Config.DeprecationWarnings.warn() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() - Pleroma.Repo.check_migrations_applied!() + Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex new file mode 100644 index 000000000..3bba70b7b --- /dev/null +++ b/lib/pleroma/application_requirements.ex @@ -0,0 +1,103 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ApplicationRequirements do + @moduledoc """ + The module represents the collection of validations to runs before start server. + """ + + defmodule VerifyError, do: defexception([:message]) + + import Ecto.Query + + require Logger + + @spec verify!() :: :ok | VerifyError.t() + def verify! do + :ok + |> check_migrations_applied!() + |> check_rum!() + |> handle_result() + end + + defp handle_result(:ok), do: :ok + defp handle_result({:error, message}), do: raise(VerifyError, message: message) + + defp check_migrations_applied!(:ok) do + unless Pleroma.Config.get( + [:i_am_aware_this_may_cause_data_loss, :disable_migration_check], + false + ) do + {_, res, _} = + Ecto.Migrator.with_repo(Pleroma.Repo, fn repo -> + down_migrations = + Ecto.Migrator.migrations(repo) + |> Enum.reject(fn + {:up, _, _} -> true + {:down, _, _} -> false + end) + + if length(down_migrations) > 0 do + down_migrations_text = + Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end) + + Logger.error( + "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" + ) + + {:error, "Unapplied Migrations detected"} + else + :ok + end + end) + + res + else + :ok + end + end + + defp check_migrations_applied!(result), do: result + + defp check_rum!(:ok) do + {_, res, _} = + Ecto.Migrator.with_repo(Pleroma.Repo, fn repo -> + migrate = + from(o in "columns", + where: o.table_name == "objects", + where: o.column_name == "fts_content" + ) + |> repo.exists?(prefix: "information_schema") + + setting = Pleroma.Config.get([:database, :rum_enabled], false) + + do_check_rum!(setting, migrate) + end) + + res + end + + defp check_rum!(result), do: result + + defp do_check_rum!(setting, migrate) do + case {setting, migrate} do + {true, false} -> + Logger.error( + "Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`" + ) + + {:error, "Unapplied RUM Migrations detected"} + + {false, true} -> + Logger.error( + "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`" + ) + + {:error, "RUM Migrations detected"} + + _ -> + :ok + end + end +end diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index 6d85d70bc..f317e4d58 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -11,9 +11,7 @@ defmodule Pleroma.Repo do import Ecto.Query require Logger - defmodule Instrumenter do - use Prometheus.EctoInstrumenter - end + defmodule Instrumenter, do: use(Prometheus.EctoInstrumenter) @doc """ Dynamically loads the repository url from the @@ -51,35 +49,6 @@ def get_assoc(resource, association) do end end - def check_migrations_applied!() do - unless Pleroma.Config.get( - [:i_am_aware_this_may_cause_data_loss, :disable_migration_check], - false - ) do - Ecto.Migrator.with_repo(__MODULE__, fn repo -> - down_migrations = - Ecto.Migrator.migrations(repo) - |> Enum.reject(fn - {:up, _, _} -> true - {:down, _, _} -> false - end) - - if length(down_migrations) > 0 do - down_migrations_text = - Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end) - - Logger.error( - "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" - ) - - raise Pleroma.Repo.UnappliedMigrationsError - end - end) - else - :ok - end - end - def chunk_stream(query, chunk_size) do # We don't actually need start and end funcitons of resource streaming, # but it seems to be the only way to not fetch records one-by-one and @@ -107,7 +76,3 @@ def chunk_stream(query, chunk_size) do ) end end - -defmodule Pleroma.Repo.UnappliedMigrationsError do - defexception message: "Unapplied Migrations detected" -end diff --git a/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs b/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs index 79bde163d..757afa129 100644 --- a/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs +++ b/priv/repo/optional_migrations/rum_indexing/20190510135645_add_fts_index_to_objects_two.exs @@ -2,29 +2,24 @@ defmodule Pleroma.Repo.Migrations.AddFtsIndexToObjectsTwo do use Ecto.Migration def up do - if Pleroma.Config.get([:database, :rum_enabled]) do - execute("create extension if not exists rum") - drop_if_exists index(:objects, ["(to_tsvector('english', data->>'content'))"], using: :gin, name: :objects_fts) - alter table(:objects) do - add(:fts_content, :tsvector) - end - - execute("CREATE FUNCTION objects_fts_update() RETURNS trigger AS $$ - begin - new.fts_content := to_tsvector('english', new.data->>'content'); - return new; - end - $$ LANGUAGE plpgsql") - execute("create index if not exists objects_fts on objects using RUM (fts_content rum_tsvector_addon_ops, inserted_at) with (attach = 'inserted_at', to = 'fts_content');") - - execute("CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON objects - FOR EACH ROW EXECUTE PROCEDURE objects_fts_update()") - - execute("UPDATE objects SET updated_at = NOW()") - else - raise Ecto.MigrationError, - message: "Migration is not allowed. You can change this behavior by setting `database/rum_enabled` to true." + execute("create extension if not exists rum") + drop_if_exists index(:objects, ["(to_tsvector('english', data->>'content'))"], using: :gin, name: :objects_fts) + alter table(:objects) do + add(:fts_content, :tsvector) end + + execute("CREATE FUNCTION objects_fts_update() RETURNS trigger AS $$ + begin + new.fts_content := to_tsvector('english', new.data->>'content'); + return new; + end + $$ LANGUAGE plpgsql") + execute("create index if not exists objects_fts on objects using RUM (fts_content rum_tsvector_addon_ops, inserted_at) with (attach = 'inserted_at', to = 'fts_content');") + + execute("CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON objects + FOR EACH ROW EXECUTE PROCEDURE objects_fts_update()") + + execute("UPDATE objects SET updated_at = NOW()") end def down do diff --git a/test/application_requirements_test.exs b/test/application_requirements_test.exs new file mode 100644 index 000000000..0981fcdeb --- /dev/null +++ b/test/application_requirements_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RepoTest do + use Pleroma.DataCase + import ExUnit.CaptureLog + import Mock + + describe "check_rum!" do + setup_with_mocks([ + {Ecto.Migrator, [], + [ + with_repo: fn repo, fun -> passthrough([repo, fun]) end, + migrations: fn Pleroma.Repo -> [] end + ]} + ]) do + :ok + end + + setup do: clear_config([:database, :rum_enabled]) + + test "raises if rum is enabled and detects unapplied rum migrations" do + Pleroma.Config.put([:database, :rum_enabled], true) + + assert_raise Pleroma.ApplicationRequirements.VerifyError, + "Unapplied RUM Migrations detected", + fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + end + + describe "check_migrations_applied!" do + setup_with_mocks([ + {Ecto.Migrator, [], + [ + with_repo: fn repo, fun -> passthrough([repo, fun]) end, + migrations: fn Pleroma.Repo -> + [ + {:up, 20_191_128_153_944, "fix_missing_following_count"}, + {:up, 20_191_203_043_610, "create_report_notes"}, + {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} + ] + end + ]} + ]) do + :ok + end + + 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.ApplicationRequirements.VerifyError, + "Unapplied Migrations detected", + fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + + test "doesn't do anything if disabled" do + Pleroma.Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) + + assert :ok == Pleroma.ApplicationRequirements.verify!() + end + end +end diff --git a/test/repo_test.exs b/test/repo_test.exs index daffc6542..92e827c95 100644 --- a/test/repo_test.exs +++ b/test/repo_test.exs @@ -4,9 +4,7 @@ defmodule Pleroma.RepoTest do use Pleroma.DataCase - import ExUnit.CaptureLog import Pleroma.Factory - import Mock alias Pleroma.User @@ -49,36 +47,4 @@ test "return error if has not assoc " do assert Repo.get_assoc(token, :user) == {:error, :not_found} end end - - describe "check_migrations_applied!" do - setup_with_mocks([ - {Ecto.Migrator, [], - [ - with_repo: fn repo, fun -> passthrough([repo, fun]) end, - migrations: fn Pleroma.Repo -> - [ - {:up, 20_191_128_153_944, "fix_missing_following_count"}, - {:up, 20_191_203_043_610, "create_report_notes"}, - {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} - ] - end - ]} - ]) do - :ok - end - - 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 -> - capture_log(&Repo.check_migrations_applied!/0) - end - end - - test "doesn't do anything if disabled" do - Pleroma.Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) - - assert :ok == Repo.check_migrations_applied!() - end - end end From 7e6f43c0d7c625a03ee0216c2d9474253ef87b5a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn <egor@kislitsyn.com> Date: Mon, 22 Jun 2020 19:03:04 +0400 Subject: [PATCH 356/375] Add `is_muted` to notifications --- .../mastodon_api/views/notification_view.ex | 8 ++--- .../views/notification_view_test.exs | 36 +++++++++++++++---- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 3865be280..c97e6d32f 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -84,12 +84,7 @@ def render( # Note: :relationships contain user mutes (needed for :muted flag in :status) status_render_opts = %{relationships: opts[:relationships]} - - account = - AccountView.render( - "show.json", - %{user: actor, for: reading_user} - ) + account = AccountView.render("show.json", %{user: actor, for: reading_user}) response = %{ id: to_string(notification.id), @@ -97,6 +92,7 @@ def render( created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), account: account, pleroma: %{ + is_muted: User.mutes?(reading_user, actor), is_seen: notification.seen } } diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index 9c399b2df..8e0e58538 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -49,7 +49,7 @@ test "ChatMessage notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "pleroma:chat_mention", account: AccountView.render("show.json", %{user: user, for: recipient}), chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), @@ -68,7 +68,7 @@ test "Mention notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "mention", account: AccountView.render("show.json", %{ @@ -92,7 +92,7 @@ test "Favourite notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "favourite", account: AccountView.render("show.json", %{user: another_user, for: user}), status: StatusView.render("show.json", %{activity: create_activity, for: user}), @@ -112,7 +112,7 @@ test "Reblog notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "reblog", account: AccountView.render("show.json", %{user: another_user, for: user}), status: StatusView.render("show.json", %{activity: reblog_activity, for: user}), @@ -130,7 +130,7 @@ test "Follow notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "follow", account: AccountView.render("show.json", %{user: follower, for: followed}), created_at: Utils.to_masto_date(notification.inserted_at) @@ -171,7 +171,7 @@ test "Move notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "move", account: AccountView.render("show.json", %{user: old_user, for: follower}), target: AccountView.render("show.json", %{user: new_user, for: follower}), @@ -196,7 +196,7 @@ test "EmojiReact notification" do expected = %{ id: to_string(notification.id), - pleroma: %{is_seen: false}, + pleroma: %{is_seen: false, is_muted: false}, type: "pleroma:emoji_reaction", emoji: "☕", account: AccountView.render("show.json", %{user: other_user, for: user}), @@ -206,4 +206,26 @@ test "EmojiReact notification" do test_notifications_rendering([notification], user, [expected]) end + + test "muted notification" do + user = insert(:user) + another_user = insert(:user) + + {:ok, _} = Pleroma.UserRelationship.create_mute(user, another_user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: true}, + type: "favourite", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end end From b3a549e916c2a721da16f60e7665b6eb64d756dd Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn <egor@kislitsyn.com> Date: Mon, 22 Jun 2020 19:18:33 +0400 Subject: [PATCH 357/375] Update NotificationOperation spec --- .../web/api_spec/operations/notification_operation.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 41328b5f2..f09be64cb 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -163,6 +163,13 @@ def notification do description: "Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.", nullable: true + }, + pleroma: %Schema{ + type: :object, + properties: %{ + is_seen: %Schema{type: :boolean}, + is_muted: %Schema{type: :boolean} + } } }, example: %{ @@ -170,7 +177,8 @@ def notification do "type" => "mention", "created_at" => "2019-11-23T07:49:02.064Z", "account" => Account.schema().example, - "status" => Status.schema().example + "status" => Status.schema().example, + "pleroma" => %{"is_seen" => false, "is_muted" => false} } } end From 8f6ba4b22f48dcd0256d6a9cf7259aa475895b84 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Mon, 22 Jun 2020 23:45:29 +0200 Subject: [PATCH 358/375] Add warning against parsing/reusing MastoFE settings blob --- lib/pleroma/web/masto_fe_controller.ex | 2 +- lib/pleroma/web/router.ex | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index d0d8bc8eb..43ec70021 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -49,7 +49,7 @@ def manifest(conn, _params) do |> render("manifest.json") end - @doc "PUT /api/web/settings" + @doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere" def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do with {:ok, _} <- User.mastodon_settings_update(user, settings) do json(conn, %{}) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index eda74a171..419aa55e4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -467,6 +467,7 @@ defmodule Pleroma.Web.Router do scope "/api/web", Pleroma.Web do pipe_through(:authenticated_api) + # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere put("/settings", MastoFEController, :put_settings) end From bf8310f3802c46e6305fcb3832bca297582990d9 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Mon, 22 Jun 2020 17:35:02 -0500 Subject: [PATCH 359/375] Add missing default config value for :instance, instance_thumbnail Follows up on b7fc61e17b --- config/config.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.exs b/config/config.exs index 4bf31f3fc..e0888fa9a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -186,6 +186,7 @@ notify_email: "noreply@example.com", description: "Pleroma: An efficient and flexible fediverse server", background_image: "/images/city.jpg", + instance_thumbnail: "/instance/thumbnail.jpeg", limit: 5_000, chat_limit: 5_000, remote_limit: 100_000, From df5e048cbb7d349b34203ccba49a8f646e4d93a3 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Mon, 22 Jun 2020 17:39:02 -0500 Subject: [PATCH 360/375] Do not need a function to provide fallback value with default defined in config.exs --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index c498fe632..c6b54e570 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -23,7 +23,7 @@ def render("show.json", _) do streaming_api: Pleroma.Web.Endpoint.websocket_url() }, stats: Pleroma.Stats.get_stats(), - thumbnail: instance_thumbnail(), + thumbnail: Keyword.get(instance, :instance_thumbnail), languages: ["en"], registrations: Keyword.get(instance, :registrations_open), # Extra (not present in Mastodon): @@ -88,9 +88,4 @@ def federation do end |> Map.put(:enabled, Config.get([:instance, :federating])) end - - defp instance_thumbnail do - Pleroma.Config.get([:instance, :instance_thumbnail]) || - "#{Pleroma.Web.base_url()}/instance/thumbnail.jpeg" - end end From c116b6d6d6e4b12d9d751481926183f19cdb5248 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Tue, 23 Jun 2020 04:42:44 +0200 Subject: [PATCH 361/375] ActivityPubController: Update upload_media @doc Small cherry-pick from https://git.pleroma.social/pleroma/pleroma/-/merge_requests/1810 --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index f0b5c6e93..220c4fe52 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -514,7 +514,6 @@ defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {new_user, for_user} end - # TODO: Add support for "object" field @doc """ Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload> @@ -525,6 +524,8 @@ defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do Response: - HTTP Code: 201 Created - HTTP Body: ActivityPub object to be inserted into another's `attachment` field + + Note: Will not point to a URL with a `Location` header because no standalone Activity has been created. """ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do with {:ok, object} <- From 2715c40e1d36cc844be1dd7d41a0c6a16ca5f7b7 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Tue, 23 Jun 2020 06:56:17 +0300 Subject: [PATCH 362/375] added tests --- lib/pleroma/application_requirements.ex | 8 +++-- test/application_requirements_test.exs | 48 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex index 3bba70b7b..88575a498 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -24,7 +24,9 @@ def verify! do defp handle_result(:ok), do: :ok defp handle_result({:error, message}), do: raise(VerifyError, message: message) - defp check_migrations_applied!(:ok) do + # Checks for pending migrations. + # + def check_migrations_applied!(:ok) do unless Pleroma.Config.get( [:i_am_aware_this_may_cause_data_loss, :disable_migration_check], false @@ -58,8 +60,10 @@ defp check_migrations_applied!(:ok) do end end - defp check_migrations_applied!(result), do: result + def check_migrations_applied!(result), do: result + # Checks for settings of RUM indexes. + # defp check_rum!(:ok) do {_, res, _} = Ecto.Migrator.with_repo(Pleroma.Repo, fn repo -> diff --git a/test/application_requirements_test.exs b/test/application_requirements_test.exs index 0981fcdeb..b8d073e11 100644 --- a/test/application_requirements_test.exs +++ b/test/application_requirements_test.exs @@ -2,25 +2,22 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.RepoTest do +defmodule Pleroma.ApplicationRequirementsTest do use Pleroma.DataCase import ExUnit.CaptureLog import Mock describe "check_rum!" do setup_with_mocks([ - {Ecto.Migrator, [], - [ - with_repo: fn repo, fun -> passthrough([repo, fun]) end, - migrations: fn Pleroma.Repo -> [] end - ]} + {Pleroma.ApplicationRequirements, [:passthrough], + [check_migrations_applied!: fn _ -> :ok end]} ]) do :ok end setup do: clear_config([:database, :rum_enabled]) - test "raises if rum is enabled and detects unapplied rum migrations" do + test "raises if rum is enabled and detects unapplied rum migrations" do Pleroma.Config.put([:database, :rum_enabled], true) assert_raise Pleroma.ApplicationRequirements.VerifyError, @@ -29,6 +26,43 @@ test "raises if rum is enabled and detects unapplied rum migrations" do capture_log(&Pleroma.ApplicationRequirements.verify!/0) end end + + test "raises if rum is disabled and detects rum migrations" do + Pleroma.Config.put([:database, :rum_enabled], false) + + with_mocks([ + { + Pleroma.Repo, + [:passthrough], + [exists?: fn _, _ -> true end] + } + ]) do + assert_raise Pleroma.ApplicationRequirements.VerifyError, + "RUM Migrations detected", + fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + end + + test "doesn't do anything if rum enabled and applied migrations" do + Pleroma.Config.put([:database, :rum_enabled], true) + + with_mocks([ + { + Pleroma.Repo, + [:passthrough], + [exists?: fn _, _ -> true end] + } + ]) do + assert Pleroma.ApplicationRequirements.verify!() == :ok + end + end + + test "doesn't do anything if rum disabled" do + Pleroma.Config.put([:database, :rum_enabled], false) + assert Pleroma.ApplicationRequirements.verify!() == :ok + end end describe "check_migrations_applied!" do From 84aa9c78dd314e93a5153e3584af38b8c218caed Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Tue, 23 Jun 2020 09:08:24 +0300 Subject: [PATCH 363/375] fix tests --- test/application_requirements_test.exs | 37 +++++++++++--------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/test/application_requirements_test.exs b/test/application_requirements_test.exs index b8d073e11..481cdfd73 100644 --- a/test/application_requirements_test.exs +++ b/test/application_requirements_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.ApplicationRequirementsTest do import ExUnit.CaptureLog import Mock + alias Pleroma.Repo + describe "check_rum!" do setup_with_mocks([ {Pleroma.ApplicationRequirements, [:passthrough], @@ -20,23 +22,19 @@ defmodule Pleroma.ApplicationRequirementsTest do test "raises if rum is enabled and detects unapplied rum migrations" do Pleroma.Config.put([:database, :rum_enabled], true) - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Unapplied RUM Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do + assert_raise Pleroma.ApplicationRequirements.VerifyError, + "Unapplied RUM Migrations detected", + fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end end test "raises if rum is disabled and detects rum migrations" do Pleroma.Config.put([:database, :rum_enabled], false) - with_mocks([ - { - Pleroma.Repo, - [:passthrough], - [exists?: fn _, _ -> true end] - } - ]) do + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do assert_raise Pleroma.ApplicationRequirements.VerifyError, "RUM Migrations detected", fn -> @@ -48,20 +46,17 @@ test "raises if rum is disabled and detects rum migrations" do test "doesn't do anything if rum enabled and applied migrations" do Pleroma.Config.put([:database, :rum_enabled], true) - with_mocks([ - { - Pleroma.Repo, - [:passthrough], - [exists?: fn _, _ -> true end] - } - ]) do + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do assert Pleroma.ApplicationRequirements.verify!() == :ok end end test "doesn't do anything if rum disabled" do Pleroma.Config.put([:database, :rum_enabled], false) - assert Pleroma.ApplicationRequirements.verify!() == :ok + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do + assert Pleroma.ApplicationRequirements.verify!() == :ok + end end end @@ -70,7 +65,7 @@ test "doesn't do anything if rum disabled" do {Ecto.Migrator, [], [ with_repo: fn repo, fun -> passthrough([repo, fun]) end, - migrations: fn Pleroma.Repo -> + migrations: fn Repo -> [ {:up, 20_191_128_153_944, "fix_missing_following_count"}, {:up, 20_191_203_043_610, "create_report_notes"}, From 2737809bbf249696d06d4a351837a405d79d47e3 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 23 Jun 2020 11:03:32 +0200 Subject: [PATCH 364/375] An act of desperation. --- test/web/activity_pub/activity_pub_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index e490a5744..6ea50fd96 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -665,7 +665,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform_all() %Activity{} = activity = Activity.get_by_ap_id(data["id"]) assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients end From d93e01137b0682dd97b95b848f7b8656de89e3cf Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 23 Jun 2020 11:43:20 +0200 Subject: [PATCH 365/375] ActivityPubControllerTest: Testing changes. --- .../web/activity_pub/activity_pub_controller_test.exs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 6ea50fd96..e5f801b22 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -648,11 +648,14 @@ test "it accepts messages with bcc as string instead of array", %{conn: conn, da test "it accepts announces with to as string instead of array", %{conn: conn} do user = insert(:user) + {:ok, post} = CommonAPI.post(user, %{status: "hey"}) + announcer = insert(:user, local: false) + data = %{ "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "http://mastodon.example.org/users/admin", - "id" => "http://mastodon.example.org/users/admin/statuses/19512778738411822/activity", - "object" => "https://mastodon.social/users/emelie/statuses/101849165031453009", + "actor" => announcer.ap_id, + "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity", + "object" => post.data["object"], "to" => "https://www.w3.org/ns/activitystreams#Public", "cc" => [user.ap_id], "type" => "Announce" @@ -665,7 +668,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) - ObanHelpers.perform_all() + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) %Activity{} = activity = Activity.get_by_ap_id(data["id"]) assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients end From adc199c6a8932f893bc1098acbf222e64cdb07d9 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 23 Jun 2020 12:04:51 +0200 Subject: [PATCH 366/375] ActivityPubControllerTest: Capture error log --- test/web/activity_pub/activity_pub_controller_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index e5f801b22..e722f7c04 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -536,6 +536,7 @@ test "accept follow activity", %{conn: conn} do assert_receive {:mix_shell, :info, ["relay.mastodon.host"]} end + @tag capture_log: true test "without valid signature, " <> "it only accepts Create activities and requires enabled federation", %{conn: conn} do From aee815b478aea5d74959c5a445c6c5d87f25168e Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Tue, 23 Jun 2020 12:37:05 +0200 Subject: [PATCH 367/375] ObjectValidator: Clarify type of object. --- lib/pleroma/web/activity_pub/object_validator.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 804a9d06e..2c657b467 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -24,13 +24,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) - def validate(%{"type" => "Update"} = object, meta) do - with {:ok, object} <- - object + def validate(%{"type" => "Update"} = update_activity, meta) do + with {:ok, update_activity} <- + update_activity |> UpdateValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object) - {:ok, object, meta} + update_activity = stringify_keys(update_activity) + {:ok, update_activity, meta} end end From 54039100fe05e8daf03274ea5c56ca8dab341e9b Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 23 Jun 2020 11:17:26 -0500 Subject: [PATCH 368/375] Remove reference to defunct distsn.org --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index f9523936a..bc781e4c8 100644 --- a/config/description.exs +++ b/config/description.exs @@ -979,7 +979,7 @@ key: :instance_thumbnail, type: :string, description: - "The instance thumbnail image. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html)", + "The instance thumbnail is the Mastodon landing page image and used by some apps to identify the instance.", suggestions: ["/instance/thumbnail.jpeg"] } ] From cb96c82f70e94e24bdf71e832db4548086f4e7c5 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 20:18:27 +0300 Subject: [PATCH 369/375] moving to mrf namespace migration fix --- ...rf_config_move_from_instance_namespace.exs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs b/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs index 6f6094613..ef36c4eb7 100644 --- a/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs +++ b/priv/repo/migrations/20200323122421_mrf_config_move_from_instance_namespace.exs @@ -5,13 +5,11 @@ defmodule Pleroma.Repo.Migrations.MrfConfigMoveFromInstanceNamespace do @old_keys [:rewrite_policy, :mrf_transparency, :mrf_transparency_exclusions] def change do - config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":instance"}) + config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) if config do - old_instance = ConfigDB.from_binary(config.value) - mrf = - old_instance + config.value |> Keyword.take(@old_keys) |> Keyword.new(fn {:rewrite_policy, policies} -> {:policies, policies} @@ -21,15 +19,17 @@ def change do if mrf != [] do {:ok, _} = - ConfigDB.create( - %{group: ":pleroma", key: ":mrf", value: ConfigDB.to_binary(mrf)}, - false - ) + %ConfigDB{} + |> ConfigDB.changeset(%{group: :pleroma, key: :mrf, value: mrf}) + |> Pleroma.Repo.insert() - new_instance = Keyword.drop(old_instance, @old_keys) + new_instance = Keyword.drop(config.value, @old_keys) if new_instance != [] do - {:ok, _} = ConfigDB.update(config, %{value: ConfigDB.to_binary(new_instance)}, false) + {:ok, _} = + config + |> ConfigDB.changeset(%{value: new_instance}) + |> Pleroma.Repo.update() else {:ok, _} = ConfigDB.delete(config) end From 721fc7c554425ccc7df693776c282c30e95ae2bb Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Wed, 24 Jun 2020 09:12:32 +0300 Subject: [PATCH 370/375] added wrapper Pleroma.HTTP for Tzdata.HTTPClient --- config/config.exs | 2 ++ lib/pleroma/http/http.ex | 8 ++++++-- lib/pleroma/http/tzdata.ex | 25 +++++++++++++++++++++++++ mix.exs | 2 +- mix.lock | 2 +- test/http/tzdata_test.exs | 35 +++++++++++++++++++++++++++++++++++ test/http_test.exs | 9 +++++++++ 7 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/http/tzdata.ex create mode 100644 test/http/tzdata_test.exs diff --git a/config/config.exs b/config/config.exs index a81ffcd3b..bd559c835 100644 --- a/config/config.exs +++ b/config/config.exs @@ -695,6 +695,8 @@ transparency: true, transparency_exclusions: [] +config :tzdata, :http_client, Pleroma.HTTP.Tzdata + # 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/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 583b56484..66ca75367 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -16,6 +16,7 @@ defmodule Pleroma.HTTP do require Logger @type t :: __MODULE__ + @type method() :: :get | :post | :put | :delete | :head @doc """ Performs GET request. @@ -28,6 +29,9 @@ def get(url, headers \\ [], options \\ []) def get(nil, _, _), do: nil def get(url, headers, options), do: request(:get, url, "", headers, options) + @spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()} + def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options) + @doc """ Performs POST request. @@ -42,7 +46,7 @@ def post(url, body, headers \\ [], options \\ []), Builds and performs http request. # Arguments: - `method` - :get, :post, :put, :delete + `method` - :get, :post, :put, :delete, :head `url` - full url `body` - request body `headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]` @@ -52,7 +56,7 @@ def post(url, body, headers \\ [], options \\ []), `{:ok, %Tesla.Env{}}` or `{:error, error}` """ - @spec request(atom(), Request.url(), String.t(), Request.headers(), keyword()) :: + @spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()} def request(method, url, body, headers, options) when is_binary(url) do uri = URI.parse(url) diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex new file mode 100644 index 000000000..34bb253a7 --- /dev/null +++ b/lib/pleroma/http/tzdata.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.Tzdata do + @moduledoc false + + @behaviour Tzdata.HTTPClient + + alias Pleroma.HTTP + + @impl true + def get(url, headers, options) do + with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do + {:ok, {env.status, env.headers, env.body}} + end + end + + @impl true + def head(url, headers, options) do + with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do + {:ok, {env.status, env.headers}} + end + end +end diff --git a/mix.exs b/mix.exs index 4d13e95d7..b638be541 100644 --- a/mix.exs +++ b/mix.exs @@ -117,7 +117,7 @@ defp oauth_deps do defp deps do [ {:phoenix, "~> 1.4.8"}, - {:tzdata, "~> 0.5.21"}, + {:tzdata, "~> 1.0.3"}, {:plug_cowboy, "~> 2.0"}, {:phoenix_pubsub, "~> 1.1"}, {:phoenix_ecto, "~> 4.0"}, diff --git a/mix.lock b/mix.lock index 5383c2c6e..5ad49391d 100644 --- a/mix.lock +++ b/mix.lock @@ -110,7 +110,7 @@ "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, - "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"}, + "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, "ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "db9fbfb5ac707bc4f85a297758406340bf0358b4af737a88113c1a9eee120ac7"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, diff --git a/test/http/tzdata_test.exs b/test/http/tzdata_test.exs new file mode 100644 index 000000000..4b37299cd --- /dev/null +++ b/test/http/tzdata_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.TzdaraTest do + use ExUnit.Case + + import Tesla.Mock + alias Pleroma.HTTP + @url "https://data.iana.org/time-zones/tzdata-latest.tar.gz" + + setup do + mock(fn + %{method: :head, url: @url} -> + %Tesla.Env{status: 200, body: ""} + + %{method: :get, url: @url} -> + %Tesla.Env{status: 200, body: "hello"} + end) + + :ok + end + + describe "head/1" do + test "returns successfully result" do + assert HTTP.Tzdata.head(@url, [], []) == {:ok, {200, []}} + end + end + + describe "get/1" do + test "returns successfully result" do + assert HTTP.Tzdata.get(@url, [], []) == {:ok, {200, [], "hello"}} + end + end +end diff --git a/test/http_test.exs b/test/http_test.exs index 618485b55..d394bb942 100644 --- a/test/http_test.exs +++ b/test/http_test.exs @@ -17,6 +17,9 @@ defmodule Pleroma.HTTPTest do } -> json(%{"my" => "data"}) + %{method: :head, url: "http://example.com/hello"} -> + %Tesla.Env{status: 200, body: ""} + %{method: :get, url: "http://example.com/hello"} -> %Tesla.Env{status: 200, body: "hello"} @@ -27,6 +30,12 @@ defmodule Pleroma.HTTPTest do :ok end + describe "head/1" do + test "returns successfully result" do + assert HTTP.head("http://example.com/hello") == {:ok, %Tesla.Env{status: 200, body: ""}} + end + end + describe "get/1" do test "returns successfully result" do assert HTTP.get("http://example.com/hello") == { From 65f3eb333b001586771247ea9949e40bfec0a947 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Wed, 24 Jun 2020 08:50:33 +0000 Subject: [PATCH 371/375] Apply suggestion to test/http/tzdata_test.exs --- test/http/tzdata_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/http/tzdata_test.exs b/test/http/tzdata_test.exs index 4b37299cd..3e605d33b 100644 --- a/test/http/tzdata_test.exs +++ b/test/http/tzdata_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.HTTP.TzdaraTest do +defmodule Pleroma.HTTP.TzdataTest do use ExUnit.Case import Tesla.Mock From aae1af8cf1d860243eb8c8a642682441294967e1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 18:06:30 +0300 Subject: [PATCH 372/375] fix for emoji pagination in pack show --- lib/pleroma/emoji/pack.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 787ff8141..d076ae312 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -45,6 +45,7 @@ def show(opts) do shortcodes = pack.files |> Map.keys() + |> Enum.sort() |> paginate(opts[:page], opts[:page_size]) pack = Map.put(pack, :files, Map.take(pack.files, shortcodes)) From 67ab5805536ed64ca842998bfd4b3b0e63d13dd3 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 24 Jun 2020 17:18:53 -0500 Subject: [PATCH 373/375] Filter outstanding follower requests from deactivated accounts --- lib/pleroma/following_relationship.ex | 1 + test/user_test.exs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 093b1f405..c2020d30a 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -124,6 +124,7 @@ def get_follow_requests(%User{id: id}) do |> join(:inner, [r], f in assoc(r, :follower)) |> where([r], r.state == ^:follow_pending) |> where([r], r.following_id == ^id) + |> where([r, f], f.deactivated != true) |> select([r, f], f) |> Repo.all() end diff --git a/test/user_test.exs b/test/user_test.exs index 311b6c683..9b66f3f51 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -199,6 +199,16 @@ test "doesn't return already accepted or duplicate follow requests" do assert [^pending_follower] = User.get_follow_requests(locked) end + test "doesn't return follow requests for deactivated accounts" do + locked = insert(:user, locked: true) + pending_follower = insert(:user, %{deactivated: true}) + + CommonAPI.follow(pending_follower, locked) + + assert true == pending_follower.deactivated + assert [] = User.get_follow_requests(locked) + end + test "clears follow requests when requester is blocked" do followed = insert(:user, locked: true) follower = insert(:user) From 439a1a0218fe032ac35bb2e84516a8a4bf8563b4 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Thu, 25 Jun 2020 07:12:29 +0300 Subject: [PATCH 374/375] added wrapper Pleroma.HTTP for ExAws.S3 --- config/config.exs | 2 ++ lib/pleroma/http/ex_aws.ex | 22 ++++++++++++++++ test/http/ex_aws_test.exs | 54 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 lib/pleroma/http/ex_aws.ex create mode 100644 test/http/ex_aws_test.exs diff --git a/config/config.exs b/config/config.exs index bd559c835..5aad26e95 100644 --- a/config/config.exs +++ b/config/config.exs @@ -697,6 +697,8 @@ config :tzdata, :http_client, Pleroma.HTTP.Tzdata +config :ex_aws, http_client: Pleroma.HTTP.ExAws + # 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/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex new file mode 100644 index 000000000..e53e64077 --- /dev/null +++ b/lib/pleroma/http/ex_aws.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.ExAws do + @moduledoc false + + @behaviour ExAws.Request.HttpClient + + alias Pleroma.HTTP + + @impl true + def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do + case HTTP.request(method, url, body, headers, http_opts) do + {:ok, env} -> + {:ok, %{status_code: env.status, headers: env.headers, body: env.body}} + + {:error, reason} -> + {:error, %{reason: reason}} + end + end +end diff --git a/test/http/ex_aws_test.exs b/test/http/ex_aws_test.exs new file mode 100644 index 000000000..d0b00ca26 --- /dev/null +++ b/test/http/ex_aws_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.ExAwsTest do + use ExUnit.Case + + import Tesla.Mock + alias Pleroma.HTTP + + @url "https://s3.amazonaws.com/test_bucket/test_image.jpg" + + setup do + mock(fn + %{method: :get, url: @url, headers: [{"x-amz-bucket-region", "us-east-1"}]} -> + %Tesla.Env{ + status: 200, + body: "image-content", + headers: [{"x-amz-bucket-region", "us-east-1"}] + } + + %{method: :post, url: @url, body: "image-content-2"} -> + %Tesla.Env{status: 200, body: "image-content-2"} + end) + + :ok + end + + describe "request" do + test "get" do + assert HTTP.ExAws.request(:get, @url, "", [{"x-amz-bucket-region", "us-east-1"}]) == { + :ok, + %{ + body: "image-content", + headers: [{"x-amz-bucket-region", "us-east-1"}], + status_code: 200 + } + } + end + + test "post" do + assert HTTP.ExAws.request(:post, @url, "image-content-2", [ + {"x-amz-bucket-region", "us-east-1"} + ]) == { + :ok, + %{ + body: "image-content-2", + headers: [], + status_code: 200 + } + } + end + end +end From f585622f852f4204f8f8dcaa6626ed4cd025edfe Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Thu, 25 Jun 2020 10:17:16 +0000 Subject: [PATCH 375/375] Apply suggestion to config/description.exs --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index bc781e4c8..5ed7f753e 100644 --- a/config/description.exs +++ b/config/description.exs @@ -979,7 +979,7 @@ key: :instance_thumbnail, type: :string, description: - "The instance thumbnail is the Mastodon landing page image and used by some apps to identify the instance.", + "The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance.", suggestions: ["/instance/thumbnail.jpeg"] } ]