From 63477d07adb614413a382a87f06af2bc2495b432 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 12 May 2020 06:44:33 +0300 Subject: [PATCH 1/8] unsubscribes of friends when user deactivated --- lib/mix/tasks/pleroma/user.ex | 14 ++------------ lib/pleroma/user.ex | 20 +++++++++++++++----- test/tasks/user_test.exs | 8 ++++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index da140ac86..93ecb4631 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -150,22 +150,12 @@ def run(["unsubscribe", nickname]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do shell_info("Deactivating #{user.nickname}") User.deactivate(user) - - user - |> User.get_friends() - |> Enum.each(fn friend -> - user = User.get_cached_by_id(user.id) - - shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}") - User.unfollow(user, friend) - end) - :timer.sleep(500) user = User.get_cached_by_id(user.id) - if Enum.empty?(User.get_friends(user)) do - shell_info("Successfully unsubscribed all followers from #{user.nickname}") + if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do + shell_info("Successfully unsubscribed all local followers from #{user.nickname}") end else _ -> diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index a86cc3202..1c456b27c 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -750,7 +750,19 @@ def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do {:error, "Not subscribed!"} end + @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()} def unfollow(%User{} = follower, %User{} = followed) do + case do_unfollow(follower, followed) do + {:ok, follower, followed} -> + {:ok, follower, Utils.fetch_latest_follow(follower, followed)} + + error -> + error + end + end + + @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()} + defp do_unfollow(%User{} = follower, %User{} = followed) do case get_follow_state(follower, followed) do state when state in [:follow_pending, :follow_accept] -> FollowingRelationship.unfollow(follower, followed) @@ -761,7 +773,7 @@ def unfollow(%User{} = follower, %User{} = followed) do |> update_following_count() |> set_cache() - {:ok, follower, Utils.fetch_latest_follow(follower, followed)} + {:ok, follower, followed} nil -> {:error, "Not subscribed!"} @@ -1401,15 +1413,13 @@ def deactivate(%User{} = user, status) do user |> get_followers() |> Enum.filter(& &1.local) - |> Enum.each(fn follower -> - follower |> update_following_count() |> set_cache() - end) + |> Enum.each(&set_cache(update_following_count(&1))) # Only update local user counts, remote will be update during the next pull. user |> get_friends() |> Enum.filter(& &1.local) - |> Enum.each(&update_follower_count/1) + |> Enum.each(&do_unfollow(user, &1)) {:ok, user} end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index b4f68d494..4b3ab5a87 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -172,23 +172,23 @@ test "no user to toggle" do describe "running unsubscribe" do test "user is unsubscribed" do followed = insert(:user) + remote_followed = insert(:user, local: false) user = insert(:user) + User.follow(user, followed, :follow_accept) + User.follow(user, remote_followed, :follow_accept) Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname]) assert_received {:mix_shell, :info, [message]} assert message =~ "Deactivating" - assert_received {:mix_shell, :info, [message]} - assert message =~ "Unsubscribing" - # Note that the task has delay :timer.sleep(500) assert_received {:mix_shell, :info, [message]} assert message =~ "Successfully unsubscribed" user = User.get_cached_by_nickname(user.nickname) - assert Enum.empty?(User.get_friends(user)) + assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) assert user.deactivated end From e688d4ee69dfbda0f8fd3a5544720a566b3946c5 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 14 Apr 2020 18:59:04 +0200 Subject: [PATCH 2/8] MRF.StealEmojiPolicy: New Policy Inspired by https://git.pleroma.social/moonman/emoji-stealer-mrf/-/blob/master/steal_emoji_policy.ex --- CHANGELOG.md | 1 + docs/configuration/cheatsheet.md | 5 + .../activity_pub/mrf/steal_emoji_policy.ex | 97 +++++++++++++++++++ test/support/http_request_mock.ex | 4 + .../mrf/steal_emoji_policy_test.exs | 64 ++++++++++++ 5 files changed, 171 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex create mode 100644 test/web/activity_pub/mrf/steal_emoji_policy_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7fb603d..acd0fe171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix task to create trusted OAuth App. - 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
API Changes - Mastodon API: Extended `/api/v1/instance`. diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 707d7fdbd..1b2d72087 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -149,6 +149,11 @@ 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_steal_emoji +* `hosts`: List of hosts to steal emojis from +* `rejected_shortcodes`: Regex-list of shortcodes to reject +* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk + ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed * `outgoing_blocks`: Whether to federate blocks to other instances diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex new file mode 100644 index 000000000..2858af9eb --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -0,0 +1,97 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do + require Logger + + alias Pleroma.Config + + @moduledoc "Detect new emojis by their shortcode and steals them" + @behaviour Pleroma.Web.ActivityPub.MRF + + defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host]) + + defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], []) + + defp steal_emoji({shortcode, url}) do + url = Pleroma.Web.MediaProxy.url(url) + {:ok, response} = Pleroma.HTTP.get(url) + size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000) + + if byte_size(response.body) <= size_limit do + emoji_dir_path = + Config.get( + [:mrf_steal_emoji, :path], + Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") + ) + + extension = + url + |> URI.parse() + |> Map.get(:path) + |> Path.basename() + |> Path.extname() + + file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")]) + + try do + :ok = File.write(file_path, response.body) + + shortcode + rescue + e -> + Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}") + nil + end + else + Logger.debug( + "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{ + size_limit + } B)" + ) + + nil + end + rescue + e -> + Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}") + nil + end + + @impl true + def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do + host = URI.parse(actor).host + + if remote_host?(host) and accept_host?(host) do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + + new_emojis = + foreign_emojis + |> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end) + |> Enum.filter(fn {shortcode, _url} -> + reject_emoji? = + Config.get([:mrf_steal_emoji, :rejected_shortcodes], []) + |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end) + + !reject_emoji? + end) + |> Enum.map(&steal_emoji(&1)) + |> Enum.filter(& &1) + + if !Enum.empty?(new_emojis) do + Logger.info("Stole new emojis: #{inspect(new_emojis)}") + Pleroma.Emoji.reload() + end + end + + {:ok, message} + end + + def filter(message), do: {:ok, message} + + @impl true + def describe do + {:ok, %{}} + end +end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 3a95e92da..3d5128835 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1291,6 +1291,10 @@ def get("https://skippers-bin.com/notes/7x9tmrp97i", _, _, _) do }} end + def get("https://example.org/emoji/firedfox.png", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}} + end + def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}} end diff --git a/test/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/web/activity_pub/mrf/steal_emoji_policy_test.exs new file mode 100644 index 000000000..8882c8c13 --- /dev/null +++ b/test/web/activity_pub/mrf/steal_emoji_policy_test.exs @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do + clear_config(:mrf_steal_emoji) + + emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") + File.rm_rf!(emoji_path) + File.mkdir!(emoji_path) + + Pleroma.Emoji.reload() + end + + test "does nothing by default" do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + assert {:ok, message} == StealEmojiPolicy.filter(message) + + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + end + + test "Steals emoji on unknown shortcode from allowed remote host" do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + Config.put([:mrf_steal_emoji, :hosts], ["example.org"]) + Config.put([:mrf_steal_emoji, :size_limit], 284_468) + + assert {:ok, message} == StealEmojiPolicy.filter(message) + + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + assert "firedfox" in installed_emoji + end +end From 490a3a34b63fa10e9151e9a385920c10615a1a3c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 19 May 2020 21:52:26 +0400 Subject: [PATCH 3/8] Add OpenAPI spec for PleromaAPI.PleromaAPIController --- docs/API/pleroma_api.md | 4 +- .../operations/notification_operation.ex | 2 +- .../api_spec/operations/pleroma_operation.ex | 223 ++++++++++++++++++ .../controllers/pleroma_api_controller.ex | 28 ++- test/support/api_spec_helpers.ex | 2 +- .../pleroma_api_controller_test.exs | 44 ++-- 6 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_operation.ex diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 867f59919..d6dbafc06 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -358,7 +358,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though. * Response: JSON, statuses (200 - healthy, 503 unhealthy) -## `GET /api/v1/pleroma/conversations/read` +## `POST /api/v1/pleroma/conversations/read` ### Marks all user's conversations as read. * Method `POST` * Authentication: required @@ -536,7 +536,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to ``` ## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji` -### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji` +### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji * Method: `GET` * Authentication: optional * Params: None diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 64adc5319..46e72f8bf 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -145,7 +145,7 @@ def destroy_multiple_operation do } end - defp notification do + def notification do %Schema{ title: "Notification", description: "Response schema for a notification", diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex new file mode 100644 index 000000000..c6df5c854 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex @@ -0,0 +1,223 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaOperation 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.Conversation + alias Pleroma.Web.ApiSpec.StatusOperation + alias Pleroma.Web.ApiSpec.NotificationOperation + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def emoji_reactions_by_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: + "Get an object of emoji to account mappings with accounts that reacted to the post", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji", + required: false + ) + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaController.emoji_reactions_by", + responses: %{ + 200 => array_of_reactions_response() + } + } + end + + def react_with_emoji_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: "React to a post with a unicode emoji", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", + required: true + ) + ], + security: [%{"oAuth" => ["write:statuses"]}], + operationId: "PleromaController.react_with_emoji", + responses: %{ + 200 => Operation.response("Status", "application/json", Status) + } + } + end + + def unreact_with_emoji_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: "Remove a reaction to a post with a unicode emoji", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", + required: true + ) + ], + security: [%{"oAuth" => ["write:statuses"]}], + operationId: "PleromaController.unreact_with_emoji", + responses: %{ + 200 => Operation.response("Status", "application/json", Status) + } + } + end + + defp array_of_reactions_response do + Operation.response("Array of Emoji Reactions", "application/json", %Schema{ + type: :array, + items: emoji_reaction(), + example: [emoji_reaction().example] + }) + end + + defp emoji_reaction do + %Schema{ + title: "EmojiReaction", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Emoji"}, + count: %Schema{type: :integer, description: "Count of reactions with this emoji"}, + me: %Schema{type: :boolean, description: "Did I react with this emoji?"}, + accounts: %Schema{ + type: :array, + items: Account, + description: "Array of accounts reacted with this emoji" + } + }, + example: %{ + "name" => "😱", + "count" => 1, + "me" => false, + "accounts" => [Account.schema().example] + } + } + end + + def conversation_operation do + %Operation{ + tags: ["Conversations"], + summary: "The conversation with the given ID", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaController.conversation", + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end + + def conversation_statuses_operation do + %Operation{ + tags: ["Conversations"], + summary: "Timeline for a given conversation", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + | pagination_params() + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaController.conversation_statuses", + responses: %{ + 200 => + Operation.response( + "Array of Statuses", + "application/json", + StatusOperation.array_of_statuses() + ) + } + } + end + + def update_conversation_operation do + %Operation{ + tags: ["Conversations"], + summary: "Update a conversation. Used to change the set of recipients.", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ), + Operation.parameter( + :recipients, + :query, + %Schema{type: :array, items: FlakeID}, + "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.", + required: true + ) + ], + security: [%{"oAuth" => ["write:conversations"]}], + operationId: "PleromaController.update_conversation", + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end + + def mark_conversations_as_read_operation do + %Operation{ + tags: ["Conversations"], + summary: "Marks all user's conversations as read", + security: [%{"oAuth" => ["write:conversations"]}], + operationId: "PleromaController.mark_conversations_as_read", + responses: %{ + 200 => + Operation.response( + "Array of Conversations that were marked as read", + "application/json", + %Schema{ + type: :array, + items: Conversation, + example: [Conversation.schema().example] + } + ) + } + } + end + + def mark_notifications_as_read_operation do + %Operation{ + tags: ["Notifications"], + summary: "Mark notifications as read. Query parameters are mutually exclusive.", + parameters: [ + Operation.parameter(:id, :query, :string, "A single notification ID to read"), + Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id") + ], + security: [%{"oAuth" => ["write:notifications"]}], + operationId: "PleromaController.mark_notifications_as_read", + responses: %{ + 200 => + Operation.response( + "A Notification or array of Motifications", + "application/json", + %Schema{ + anyOf: [ + %Schema{type: :array, items: NotificationOperation.notification()}, + NotificationOperation.notification() + ] + } + ), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index e834133b2..8220d13bc 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -20,6 +20,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( OAuthScopesPlug, %{scopes: ["read:statuses"]} @@ -49,14 +51,16 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read ) - def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation + + def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <- Object.normalize(activity) do reactions = emoji_reactions |> Enum.map(fn [emoji, user_ap_ids] -> - if params["emoji"] && params["emoji"] != emoji do + if params[:emoji] && params[:emoji] != emoji do nil else users = @@ -79,7 +83,7 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} } end end) - |> Enum.filter(& &1) + |> Enum.reject(&is_nil/1) conn |> json(reactions) @@ -90,7 +94,7 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} end end - def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do + def react_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji), activity <- Activity.get_by_id(activity_id) do conn @@ -99,10 +103,7 @@ def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "e end end - def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{ - "id" => activity_id, - "emoji" => emoji - }) do + def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji), activity <- Activity.get_by_id(activity_id) do @@ -112,7 +113,7 @@ def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{ end end - def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + def conversation(%{assigns: %{user: user}} = conn, %{id: participation_id}) do with %Participation{} = participation <- Participation.get(participation_id), true <- user.id == participation.user_id do conn @@ -128,12 +129,13 @@ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) def conversation_statuses( %{assigns: %{user: %{id: user_id} = user}} = conn, - %{"id" => participation_id} = params + %{id: participation_id} = params ) do with %Participation{user_id: ^user_id} = participation <- 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) @@ -162,7 +164,7 @@ def conversation_statuses( def update_conversation( %{assigns: %{user: user}} = conn, - %{"id" => participation_id, "recipients" => recipients} + %{id: participation_id, recipients: recipients} ) do with %Participation{} = participation <- Participation.get(participation_id), true <- user.id == participation.user_id, @@ -192,7 +194,7 @@ def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do end end - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do + def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do with {:ok, notification} <- Notification.read_one(user, notification_id) do conn |> put_view(NotificationView) @@ -205,7 +207,7 @@ def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notif end end - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do + def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do with notifications <- Notification.set_read_up_to(user, max_id) do notifications = Enum.take(notifications, 80) diff --git a/test/support/api_spec_helpers.ex b/test/support/api_spec_helpers.ex index 80c69c788..46388f92c 100644 --- a/test/support/api_spec_helpers.ex +++ b/test/support/api_spec_helpers.ex @@ -51,7 +51,7 @@ def api_operations do |> Map.take([:delete, :get, :head, :options, :patch, :post, :put, :trace]) |> Map.values() |> Enum.reject(&is_nil/1) - |> Enum.uniq() end) + |> Enum.uniq() end end diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index cfd1dbd24..f0cdc2f08 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -27,7 +27,7 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do |> assign(:user, other_user) |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") - |> json_response(200) + |> json_response_and_validate_schema(200) # We return the status, but this our implementation detail. assert %{"id" => id} = result @@ -53,7 +53,7 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") - assert %{"id" => id} = json_response(result, 200) + assert %{"id" => id} = json_response_and_validate_schema(result, 200) assert to_string(activity.id) == id ObanHelpers.perform_all() @@ -73,7 +73,7 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do result = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response(200) + |> json_response_and_validate_schema(200) assert result == [] @@ -85,7 +85,7 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do result = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response(200) + |> json_response_and_validate_schema(200) [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result @@ -96,7 +96,7 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do |> assign(:user, other_user) |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response(200) + |> json_response_and_validate_schema(200) assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = result @@ -111,7 +111,7 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do result = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response(200) + |> json_response_and_validate_schema(200) assert result == [] @@ -121,7 +121,7 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do result = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response(200) + |> json_response_and_validate_schema(200) [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result @@ -140,7 +140,7 @@ test "/api/v1/pleroma/conversations/:id" do result = conn |> get("/api/v1/pleroma/conversations/#{participation.id}") - |> json_response(200) + |> json_response_and_validate_schema(200) assert result["id"] == participation.id |> to_string() end @@ -168,7 +168,7 @@ test "/api/v1/pleroma/conversations/:id/statuses" do result = conn |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") - |> json_response(200) + |> json_response_and_validate_schema(200) assert length(result) == 2 @@ -186,12 +186,12 @@ test "/api/v1/pleroma/conversations/:id/statuses" do assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = conn |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) assert [%{"id" => ^id_three}] = conn |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) end test "PATCH /api/v1/pleroma/conversations/:id" do @@ -208,12 +208,12 @@ test "PATCH /api/v1/pleroma/conversations/:id" do assert [user] == participation.recipients assert other_user not in participation.recipients + query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" + result = conn - |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{ - "recipients" => [user.id, other_user.id] - }) - |> json_response(200) + |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") + |> json_response_and_validate_schema(200) assert result["id"] == participation.id |> to_string @@ -242,7 +242,7 @@ test "POST /api/v1/pleroma/conversations/read" do [%{"unread" => false}, %{"unread" => false}] = conn |> post("/api/v1/pleroma/conversations/read", %{}) - |> json_response(200) + |> json_response_and_validate_schema(200) [participation2, participation1] = Participation.for_user(other_user) assert Participation.get(participation2.id).read == true @@ -262,8 +262,8 @@ test "it marks a single notification as read", %{user: user1, conn: conn} do response = conn - |> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"}) - |> json_response(:ok) + |> post("/api/v1/pleroma/notifications/read?id=#{notification1.id}") + |> json_response_and_validate_schema(:ok) assert %{"pleroma" => %{"is_seen" => true}} = response assert Repo.get(Notification, notification1.id).seen @@ -280,8 +280,8 @@ test "it marks multiple notifications as read", %{user: user1, conn: conn} do [response1, response2] = conn - |> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"}) - |> json_response(:ok) + |> post("/api/v1/pleroma/notifications/read?max_id=#{notification2.id}") + |> json_response_and_validate_schema(:ok) assert %{"pleroma" => %{"is_seen" => true}} = response1 assert %{"pleroma" => %{"is_seen" => true}} = response2 @@ -293,8 +293,8 @@ test "it marks multiple notifications as read", %{user: user1, conn: conn} do test "it returns error when notification not found", %{conn: conn} do response = conn - |> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"}) - |> json_response(:bad_request) + |> post("/api/v1/pleroma/notifications/read?id=22222222222222") + |> json_response_and_validate_schema(:bad_request) assert response == %{"error" => "Cannot get notification"} end From 9a5de0f4548cfe6b62265596bbe3cef2d639b978 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 19 May 2020 23:50:49 +0400 Subject: [PATCH 4/8] Move reaction actions to EmojiReactionController --- .../operations/emoji_reaction_operation.ex | 102 ++++++++++++++ .../api_spec/operations/pleroma_operation.ex | 96 +------------- .../controllers/emoji_reaction_controller.ex | 61 +++++++++ .../controllers/pleroma_api_controller.ex | 77 ----------- .../pleroma_api/views/emoji_reaction_view.ex | 33 +++++ lib/pleroma/web/router.ex | 8 +- .../emoji_reaction_controller_test.exs | 125 ++++++++++++++++++ .../pleroma_api_controller_test.exs | 115 ---------------- 8 files changed, 329 insertions(+), 288 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex create mode 100644 lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex create mode 100644 test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex new file mode 100644 index 000000000..7c08fbaa7 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex @@ -0,0 +1,102 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Status + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: + "Get an object of emoji to account mappings with accounts that reacted to the post", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji", + required: false + ) + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "EmojiReactionController.index", + responses: %{ + 200 => array_of_reactions_response() + } + } + end + + def create_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: "React to a post with a unicode emoji", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", + required: true + ) + ], + security: [%{"oAuth" => ["write:statuses"]}], + operationId: "EmojiReactionController.create", + responses: %{ + 200 => Operation.response("Status", "application/json", Status) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: "Remove a reaction to a post with a unicode emoji", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", + required: true + ) + ], + security: [%{"oAuth" => ["write:statuses"]}], + operationId: "EmojiReactionController.delete", + responses: %{ + 200 => Operation.response("Status", "application/json", Status) + } + } + end + + defp array_of_reactions_response do + Operation.response("Array of Emoji Reactions", "application/json", %Schema{ + type: :array, + items: emoji_reaction(), + example: [emoji_reaction().example] + }) + end + + defp emoji_reaction do + %Schema{ + title: "EmojiReaction", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Emoji"}, + count: %Schema{type: :integer, description: "Count of reactions with this emoji"}, + me: %Schema{type: :boolean, description: "Did I react with this emoji?"}, + accounts: %Schema{ + type: :array, + items: Account, + description: "Array of accounts reacted with this emoji" + } + }, + example: %{ + "name" => "😱", + "count" => 1, + "me" => false, + "accounts" => [Account.schema().example] + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex index c6df5c854..7e46ba553 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex @@ -5,13 +5,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaOperation 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.Conversation - alias Pleroma.Web.ApiSpec.StatusOperation alias Pleroma.Web.ApiSpec.NotificationOperation + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Conversation + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.StatusOperation import Pleroma.Web.ApiSpec.Helpers @@ -20,92 +18,6 @@ def open_api_operation(action) do apply(__MODULE__, operation, []) end - def emoji_reactions_by_operation do - %Operation{ - tags: ["Emoji Reactions"], - summary: - "Get an object of emoji to account mappings with accounts that reacted to the post", - parameters: [ - Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), - Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji", - required: false - ) - ], - security: [%{"oAuth" => ["read:statuses"]}], - operationId: "PleromaController.emoji_reactions_by", - responses: %{ - 200 => array_of_reactions_response() - } - } - end - - def react_with_emoji_operation do - %Operation{ - tags: ["Emoji Reactions"], - summary: "React to a post with a unicode emoji", - parameters: [ - Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), - Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", - required: true - ) - ], - security: [%{"oAuth" => ["write:statuses"]}], - operationId: "PleromaController.react_with_emoji", - responses: %{ - 200 => Operation.response("Status", "application/json", Status) - } - } - end - - def unreact_with_emoji_operation do - %Operation{ - tags: ["Emoji Reactions"], - summary: "Remove a reaction to a post with a unicode emoji", - parameters: [ - Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), - Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", - required: true - ) - ], - security: [%{"oAuth" => ["write:statuses"]}], - operationId: "PleromaController.unreact_with_emoji", - responses: %{ - 200 => Operation.response("Status", "application/json", Status) - } - } - end - - defp array_of_reactions_response do - Operation.response("Array of Emoji Reactions", "application/json", %Schema{ - type: :array, - items: emoji_reaction(), - example: [emoji_reaction().example] - }) - end - - defp emoji_reaction do - %Schema{ - title: "EmojiReaction", - type: :object, - properties: %{ - name: %Schema{type: :string, description: "Emoji"}, - count: %Schema{type: :integer, description: "Count of reactions with this emoji"}, - me: %Schema{type: :boolean, description: "Did I react with this emoji?"}, - accounts: %Schema{ - type: :array, - items: Account, - description: "Array of accounts reacted with this emoji" - } - }, - example: %{ - "name" => "😱", - "count" => 1, - "me" => false, - "accounts" => [Account.schema().example] - } - } - end - def conversation_operation do %Operation{ tags: ["Conversations"], diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex new file mode 100644 index 000000000..a002912f3 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete]) + + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} + when action == :index + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation + + def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do + with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), + %Object{data: %{"reactions" => reactions}} when is_list(reactions) <- + Object.normalize(activity) do + reactions = filter(reactions, params) + render(conn, "index.json", emoji_reactions: reactions, user: user) + else + _e -> json(conn, []) + end + end + + defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do + Enum.filter(reactions, fn [e, _] -> e == emoji end) + end + + defp filter(reactions, _), do: reactions + + def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do + with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do + activity = Activity.get_by_id(activity_id) + + conn + |> put_view(StatusView) + |> render("show.json", activity: activity, for: user, as: :activity) + end + end + + def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do + with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do + activity = Activity.get_by_id(activity_id) + + conn + |> put_view(StatusView) + |> render("show.json", activity: activity, for: user, as: :activity) + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 8220d13bc..61273f7ee 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -7,15 +7,10 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Activity alias Pleroma.Conversation.Participation alias Pleroma.Notification - alias Pleroma.Object alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView @@ -28,18 +23,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do when action in [:conversation, :conversation_statuses] ) - plug( - OAuthScopesPlug, - %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} - when action == :emoji_reactions_by - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:statuses"]} - when action in [:react_with_emoji, :unreact_with_emoji] - ) - plug( OAuthScopesPlug, %{scopes: ["write:conversations"]} @@ -53,66 +36,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation - def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do - with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), - %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <- - Object.normalize(activity) do - reactions = - emoji_reactions - |> Enum.map(fn [emoji, user_ap_ids] -> - if params[:emoji] && params[:emoji] != emoji do - nil - else - users = - Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) - |> Enum.filter(fn - %{deactivated: false} -> true - _ -> false - end) - - %{ - name: emoji, - count: length(users), - accounts: - AccountView.render("index.json", %{ - users: users, - for: user, - as: :user - }), - me: !!(user && user.ap_id in user_ap_ids) - } - end - end) - |> Enum.reject(&is_nil/1) - - conn - |> json(reactions) - else - _e -> - conn - |> json([]) - end - end - - def react_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do - with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji), - activity <- Activity.get_by_id(activity_id) do - conn - |> put_view(StatusView) - |> render("show.json", %{activity: activity, for: user, as: :activity}) - end - end - - def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do - with {:ok, _activity} <- - CommonAPI.unreact_with_emoji(activity_id, user, emoji), - activity <- Activity.get_by_id(activity_id) do - conn - |> put_view(StatusView) - |> render("show.json", %{activity: activity, for: user, as: :activity}) - end - end - def conversation(%{assigns: %{user: user}} = conn, %{id: participation_id}) do with %Participation{} = participation <- Participation.get(participation_id), true <- user.id == participation.user_id do diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex new file mode 100644 index 000000000..84d2d303d --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do + use Pleroma.Web, :view + + alias Pleroma.Web.MastodonAPI.AccountView + + def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do + render_many(emoji_reactions, __MODULE__, "show.json", opts) + end + + def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do + users = fetch_users(user_ap_ids) + + %{ + name: emoji, + count: length(users), + accounts: render(AccountView, "index.json", users: users, for: user, as: :user), + me: !!(user && user.ap_id in user_ap_ids) + } + end + + defp fetch_users(user_ap_ids) do + user_ap_ids + |> Enum.map(&Pleroma.User.get_cached_by_ap_id/1) + |> Enum.filter(fn + %{deactivated: false} -> true + _ -> false + end) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 369c54cf4..12381511e 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -297,8 +297,8 @@ defmodule Pleroma.Web.Router do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:api) - get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by) - get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by) + get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index) + get("/statuses/:id/reactions", EmojiReactionController, :index) end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do @@ -314,8 +314,8 @@ defmodule Pleroma.Web.Router do pipe_through(:authenticated_api) patch("/conversations/:id", PleromaAPIController, :update_conversation) - put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji) - delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji) + put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create) + delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) post("/notifications/read", PleromaAPIController, :mark_notifications_as_read) patch("/accounts/update_avatar", AccountController, :update_avatar) diff --git a/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs new file mode 100644 index 000000000..ee66ebf87 --- /dev/null +++ b/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -0,0 +1,125 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") + |> json_response_and_validate_schema(200) + + # We return the status, but this our implementation detail. + assert %{"id" => id} = result + assert to_string(activity.id) == id + + assert result["pleroma"]["emoji_reactions"] == [ + %{"name" => "☕", "count" => 1, "me" => true} + ] + end + + test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + ObanHelpers.perform_all() + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") + + assert %{"id" => id} = json_response_and_validate_schema(result, 200) + assert to_string(activity.id) == id + + ObanHelpers.perform_all() + + object = Object.get_by_ap_id(activity.data["object"]) + + assert object.data["reaction_count"] == 0 + end + + test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + doomed_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert result == [] + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") + + User.perform(:delete, doomed_user) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result + + assert represented_user["id"] == other_user.id + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = + result + end + + test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") + |> json_response_and_validate_schema(200) + + assert result == [] + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") + |> json_response_and_validate_schema(200) + + assert represented_user["id"] == other_user.id + end +end diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index f0cdc2f08..6f4f01e6f 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -3,131 +3,16 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do - use Oban.Testing, repo: Pleroma.Repo use Pleroma.Web.ConnCase alias Pleroma.Conversation.Participation alias Pleroma.Notification - alias Pleroma.Object alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.CommonAPI import Pleroma.Factory - test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") - |> json_response_and_validate_schema(200) - - # We return the status, but this our implementation detail. - assert %{"id" => id} = result - assert to_string(activity.id) == id - - assert result["pleroma"]["emoji_reactions"] == [ - %{"name" => "☕", "count" => 1, "me" => true} - ] - end - - test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - ObanHelpers.perform_all() - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") - - assert %{"id" => id} = json_response_and_validate_schema(result, 200) - assert to_string(activity.id) == id - - ObanHelpers.perform_all() - - object = Object.get_by_ap_id(activity.data["object"]) - - assert object.data["reaction_count"] == 0 - end - - test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - doomed_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert result == [] - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") - - User.perform(:delete, doomed_user) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result - - assert represented_user["id"] == other_user.id - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = - result - end - - test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response_and_validate_schema(200) - - assert result == [] - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response_and_validate_schema(200) - - [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result - - assert represented_user["id"] == other_user.id - end - test "/api/v1/pleroma/conversations/:id" do user = insert(:user) %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) From f3fc8b22b1dca8d432d066417e2bb9b62a3f1520 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 20 May 2020 15:00:11 +0400 Subject: [PATCH 5/8] Move conversation actions to PleromaAPI.ConversationController --- .../pleroma_conversation_operation.ex | 106 ++++++++++++++ .../api_spec/operations/pleroma_operation.ex | 93 ------------ .../controllers/conversation_controller.ex | 95 ++++++++++++ .../controllers/pleroma_api_controller.ex | 99 ------------- lib/pleroma/web/router.ex | 12 +- .../conversation_controller_test.exs | 136 ++++++++++++++++++ .../pleroma_api_controller_test.exs | 124 ---------------- 7 files changed, 341 insertions(+), 324 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex create mode 100644 test/web/pleroma_api/controllers/conversation_controller_test.exs diff --git a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex new file mode 100644 index 000000000..e885eab20 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Conversation + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.StatusOperation + + 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: ["Conversations"], + summary: "The conversation with the given ID", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaAPI.ConversationController.show", + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end + + def statuses_operation do + %Operation{ + tags: ["Conversations"], + summary: "Timeline for a given conversation", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + | pagination_params() + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaAPI.ConversationController.statuses", + responses: %{ + 200 => + Operation.response( + "Array of Statuses", + "application/json", + StatusOperation.array_of_statuses() + ) + } + } + end + + def update_operation do + %Operation{ + tags: ["Conversations"], + summary: "Update a conversation. Used to change the set of recipients.", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ), + Operation.parameter( + :recipients, + :query, + %Schema{type: :array, items: FlakeID}, + "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.", + required: true + ) + ], + security: [%{"oAuth" => ["write:conversations"]}], + operationId: "PleromaAPI.ConversationController.update", + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end + + def mark_as_read_operation do + %Operation{ + tags: ["Conversations"], + summary: "Marks all user's conversations as read", + security: [%{"oAuth" => ["write:conversations"]}], + operationId: "PleromaAPI.ConversationController.mark_as_read", + responses: %{ + 200 => + Operation.response( + "Array of Conversations that were marked as read", + "application/json", + %Schema{ + type: :array, + items: Conversation, + example: [Conversation.schema().example] + } + ) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex index 7e46ba553..d28451933 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex @@ -7,105 +7,12 @@ defmodule Pleroma.Web.ApiSpec.PleromaOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.NotificationOperation alias Pleroma.Web.ApiSpec.Schemas.ApiError - alias Pleroma.Web.ApiSpec.Schemas.Conversation - alias Pleroma.Web.ApiSpec.Schemas.FlakeID - alias Pleroma.Web.ApiSpec.StatusOperation - - import Pleroma.Web.ApiSpec.Helpers def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") apply(__MODULE__, operation, []) end - def conversation_operation do - %Operation{ - tags: ["Conversations"], - summary: "The conversation with the given ID", - parameters: [ - Operation.parameter(:id, :path, :string, "Conversation ID", - example: "123", - required: true - ) - ], - security: [%{"oAuth" => ["read:statuses"]}], - operationId: "PleromaController.conversation", - responses: %{ - 200 => Operation.response("Conversation", "application/json", Conversation) - } - } - end - - def conversation_statuses_operation do - %Operation{ - tags: ["Conversations"], - summary: "Timeline for a given conversation", - parameters: [ - Operation.parameter(:id, :path, :string, "Conversation ID", - example: "123", - required: true - ) - | pagination_params() - ], - security: [%{"oAuth" => ["read:statuses"]}], - operationId: "PleromaController.conversation_statuses", - responses: %{ - 200 => - Operation.response( - "Array of Statuses", - "application/json", - StatusOperation.array_of_statuses() - ) - } - } - end - - def update_conversation_operation do - %Operation{ - tags: ["Conversations"], - summary: "Update a conversation. Used to change the set of recipients.", - parameters: [ - Operation.parameter(:id, :path, :string, "Conversation ID", - example: "123", - required: true - ), - Operation.parameter( - :recipients, - :query, - %Schema{type: :array, items: FlakeID}, - "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.", - required: true - ) - ], - security: [%{"oAuth" => ["write:conversations"]}], - operationId: "PleromaController.update_conversation", - responses: %{ - 200 => Operation.response("Conversation", "application/json", Conversation) - } - } - end - - def mark_conversations_as_read_operation do - %Operation{ - tags: ["Conversations"], - summary: "Marks all user's conversations as read", - security: [%{"oAuth" => ["write:conversations"]}], - operationId: "PleromaController.mark_conversations_as_read", - responses: %{ - 200 => - Operation.response( - "Array of Conversations that were marked as read", - "application/json", - %Schema{ - type: :array, - items: Conversation, - example: [Conversation.schema().example] - } - ) - } - } - end - def mark_notifications_as_read_operation do %Operation{ tags: ["Notifications"], diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex new file mode 100644 index 000000000..21d5eb8d5 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ConversationController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + + alias Pleroma.Conversation.Participation + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.StatusView + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView) + plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses]) + + plug( + OAuthScopesPlug, + %{scopes: ["write:conversations"]} when action in [:update, :mark_as_read] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation + + def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do + with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do + render(conn, "participation.json", participation: participation, for: user) + else + _error -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Unknown conversation id"}) + end + end + + def statuses( + %{assigns: %{user: %{id: user_id} = user}} = conn, + %{id: participation_id} = params + ) do + with %Participation{user_id: ^user_id} = participation <- + 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) + + activities = + participation.conversation.ap_id + |> ActivityPub.fetch_activities_for_context_query(params) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) + |> Enum.reverse() + + conn + |> add_link_headers(activities) + |> put_view(StatusView) + |> render("index.json", activities: activities, for: user, as: :activity) + else + _error -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Unknown conversation id"}) + end + end + + def update( + %{assigns: %{user: %{id: user_id} = user}} = conn, + %{id: participation_id, recipients: recipients} + ) do + with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id), + {:ok, participation} <- Participation.set_recipients(participation, recipients) do + render(conn, "participation.json", participation: participation, for: user) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(%{"error" => message}) + + _error -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Unknown conversation id"}) + end + end + + def mark_as_read(%{assigns: %{user: user}} = conn, _params) do + with {:ok, _, participations} <- Participation.mark_all_as_read(user) do + conn + |> add_link_headers(participations) + |> render("participations.json", participations: participations, for: user) + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 61273f7ee..a58665abe 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -5,30 +5,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - - alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.MastodonAPI.StatusView plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug( - OAuthScopesPlug, - %{scopes: ["read:statuses"]} - when action in [:conversation, :conversation_statuses] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:conversations"]} - when action in [:update_conversation, :mark_conversations_as_read] - ) - plug( OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read @@ -36,87 +18,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation - def conversation(%{assigns: %{user: user}} = conn, %{id: participation_id}) do - with %Participation{} = participation <- Participation.get(participation_id), - true <- user.id == participation.user_id do - conn - |> put_view(ConversationView) - |> render("participation.json", %{participation: participation, for: user}) - else - _error -> - conn - |> put_status(404) - |> json(%{"error" => "Unknown conversation id"}) - end - end - - def conversation_statuses( - %{assigns: %{user: %{id: user_id} = user}} = conn, - %{id: participation_id} = params - ) do - with %Participation{user_id: ^user_id} = participation <- - 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) - - activities = - participation.conversation.ap_id - |> ActivityPub.fetch_activities_for_context_query(params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) - |> Enum.reverse() - - conn - |> add_link_headers(activities) - |> put_view(StatusView) - |> render("index.json", - activities: activities, - for: user, - as: :activity - ) - else - _error -> - conn - |> put_status(404) - |> json(%{"error" => "Unknown conversation id"}) - end - end - - def update_conversation( - %{assigns: %{user: user}} = conn, - %{id: participation_id, recipients: recipients} - ) do - with %Participation{} = participation <- Participation.get(participation_id), - true <- user.id == participation.user_id, - {:ok, participation} <- Participation.set_recipients(participation, recipients) do - conn - |> put_view(ConversationView) - |> render("participation.json", %{participation: participation, for: user}) - else - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(%{"error" => message}) - - _error -> - conn - |> put_status(404) - |> json(%{"error" => "Unknown conversation id"}) - end - end - - def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do - with {:ok, _, participations} <- Participation.mark_all_as_read(user) do - conn - |> add_link_headers(participations) - |> put_view(ConversationView) - |> render("participations.json", participations: participations, for: user) - end - end - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do with {:ok, notification} <- Notification.read_one(user, notification_id) do conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 12381511e..78da4a871 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -305,15 +305,11 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:authenticated_api) - get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) - get("/conversations/:id", PleromaAPIController, :conversation) - post("/conversations/read", PleromaAPIController, :mark_conversations_as_read) - end + get("/conversations/:id/statuses", ConversationController, :statuses) + get("/conversations/:id", ConversationController, :show) + post("/conversations/read", ConversationController, :mark_as_read) + patch("/conversations/:id", ConversationController, :update) - scope [] do - pipe_through(:authenticated_api) - - patch("/conversations/:id", PleromaAPIController, :update_conversation) put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create) delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) post("/notifications/read", PleromaAPIController, :mark_notifications_as_read) diff --git a/test/web/pleroma_api/controllers/conversation_controller_test.exs b/test/web/pleroma_api/controllers/conversation_controller_test.exs new file mode 100644 index 000000000..e6d0b3e37 --- /dev/null +++ b/test/web/pleroma_api/controllers/conversation_controller_test.exs @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "/api/v1/pleroma/conversations/:id" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) + + [participation] = Participation.for_user(other_user) + + result = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == participation.id |> to_string() + end + + test "/api/v1/pleroma/conversations/:id/statuses" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) + third_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"}) + + {:ok, activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) + + [participation] = Participation.for_user(other_user) + + {:ok, activity_two} = + CommonAPI.post(other_user, %{ + status: "Hi!", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + result = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") + |> json_response_and_validate_schema(200) + + assert length(result) == 2 + + id_one = activity.id + id_two = activity_two.id + assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result + + {:ok, %{id: id_three}} = + CommonAPI.post(other_user, %{ + status: "Bye!", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^id_three}] = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") + |> json_response_and_validate_schema(:ok) + end + + test "PATCH /api/v1/pleroma/conversations/:id" do + %{user: user, conn: conn} = oauth_access(["write:conversations"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"}) + + [participation] = Participation.for_user(user) + + participation = Repo.preload(participation, :recipients) + + user = User.get_cached_by_id(user.id) + assert [user] == participation.recipients + assert other_user not in participation.recipients + + query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" + + result = + conn + |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") + |> json_response_and_validate_schema(200) + + assert result["id"] == participation.id |> to_string + + [participation] = Participation.for_user(user) + participation = Repo.preload(participation, :recipients) + + assert user in participation.recipients + assert other_user in participation.recipients + end + + test "POST /api/v1/pleroma/conversations/read" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["write:conversations"]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == false + assert Participation.get(participation1.id).read == false + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 + + [%{"unread" => false}, %{"unread" => false}] = + conn + |> post("/api/v1/pleroma/conversations/read", %{}) + |> json_response_and_validate_schema(200) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == true + assert Participation.get(participation1.id).read == true + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 + end +end diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 6f4f01e6f..c4c661266 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -5,136 +5,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Repo - alias Pleroma.User alias Pleroma.Web.CommonAPI import Pleroma.Factory - test "/api/v1/pleroma/conversations/:id" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) - - [participation] = Participation.for_user(other_user) - - result = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == participation.id |> to_string() - end - - test "/api/v1/pleroma/conversations/:id/statuses" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) - third_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"}) - - {:ok, activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) - - [participation] = Participation.for_user(other_user) - - {:ok, activity_two} = - CommonAPI.post(other_user, %{ - status: "Hi!", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - result = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") - |> json_response_and_validate_schema(200) - - assert length(result) == 2 - - id_one = activity.id - id_two = activity_two.id - assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result - - {:ok, %{id: id_three}} = - CommonAPI.post(other_user, %{ - status: "Bye!", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^id_three}] = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") - |> json_response_and_validate_schema(:ok) - end - - test "PATCH /api/v1/pleroma/conversations/:id" do - %{user: user, conn: conn} = oauth_access(["write:conversations"]) - other_user = insert(:user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"}) - - [participation] = Participation.for_user(user) - - participation = Repo.preload(participation, :recipients) - - user = User.get_cached_by_id(user.id) - assert [user] == participation.recipients - assert other_user not in participation.recipients - - query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" - - result = - conn - |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") - |> json_response_and_validate_schema(200) - - assert result["id"] == participation.id |> to_string - - [participation] = Participation.for_user(user) - participation = Repo.preload(participation, :recipients) - - assert user in participation.recipients - assert other_user in participation.recipients - end - - test "POST /api/v1/pleroma/conversations/read" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["write:conversations"]) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) - - [participation2, participation1] = Participation.for_user(other_user) - assert Participation.get(participation2.id).read == false - assert Participation.get(participation1.id).read == false - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 - - [%{"unread" => false}, %{"unread" => false}] = - conn - |> post("/api/v1/pleroma/conversations/read", %{}) - |> json_response_and_validate_schema(200) - - [participation2, participation1] = Participation.for_user(other_user) - assert Participation.get(participation2.id).read == true - assert Participation.get(participation1.id).read == true - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 - end - describe "POST /api/v1/pleroma/notifications/read" do setup do: oauth_access(["write:notifications"]) From 5ba6e1c322c0937849eca53fc816f348659fb34c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 20 May 2020 15:14:11 +0400 Subject: [PATCH 6/8] Move notification actions to PleromaAPI.NotificationController --- ...n.ex => pleroma_notification_operation.ex} | 6 +-- .../controllers/notification_controller.ex | 36 +++++++++++++++ .../controllers/pleroma_api_controller.ex | 46 ------------------- lib/pleroma/web/router.ex | 2 +- ...t.exs => notification_controller_test.exs} | 2 +- 5 files changed, 41 insertions(+), 51 deletions(-) rename lib/pleroma/web/api_spec/operations/{pleroma_operation.ex => pleroma_notification_operation.ex} (89%) create mode 100644 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex delete mode 100644 lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex rename test/web/pleroma_api/controllers/{pleroma_api_controller_test.exs => notification_controller_test.exs} (97%) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex similarity index 89% rename from lib/pleroma/web/api_spec/operations/pleroma_operation.ex rename to lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex index d28451933..636c39a15 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.PleromaOperation do +defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.NotificationOperation @@ -13,7 +13,7 @@ def open_api_operation(action) do apply(__MODULE__, operation, []) end - def mark_notifications_as_read_operation do + def mark_as_read_operation do %Operation{ tags: ["Notifications"], summary: "Mark notifications as read. Query parameters are mutually exclusive.", @@ -22,7 +22,7 @@ def mark_notifications_as_read_operation do Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id") ], security: [%{"oAuth" => ["write:notifications"]}], - operationId: "PleromaController.mark_notifications_as_read", + operationId: "PleromaAPI.NotificationController.mark_as_read", responses: %{ 200 => Operation.response( diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex new file mode 100644 index 000000000..0b2f678c5 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.NotificationController do + use Pleroma.Web, :controller + + alias Pleroma.Notification + alias Pleroma.Plugs.OAuthScopesPlug + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read) + plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation + + def mark_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do + with {:ok, notification} <- Notification.read_one(user, notification_id) do + render(conn, "show.json", notification: notification, for: user) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(%{"error" => message}) + end + end + + def mark_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do + notifications = + user + |> Notification.set_read_up_to(max_id) + |> Enum.take(80) + + render(conn, "index.json", notifications: notifications, for: user) + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex deleted file mode 100644 index a58665abe..000000000 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do - use Pleroma.Web, :controller - - alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Web.MastodonAPI.NotificationView - - plug(Pleroma.Web.ApiSpec.CastAndValidate) - - plug( - OAuthScopesPlug, - %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read - ) - - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation - - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do - with {:ok, notification} <- Notification.read_one(user, notification_id) do - conn - |> put_view(NotificationView) - |> render("show.json", %{notification: notification, for: user}) - else - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(%{"error" => message}) - end - end - - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do - with notifications <- Notification.set_read_up_to(user, max_id) do - notifications = Enum.take(notifications, 80) - - conn - |> put_view(NotificationView) - |> render("index.json", - notifications: notifications, - for: user - ) - end - end -end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 78da4a871..0e29e5645 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -312,7 +312,7 @@ defmodule Pleroma.Web.Router do put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create) delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) - post("/notifications/read", PleromaAPIController, :mark_notifications_as_read) + post("/notifications/read", NotificationController, :mark_as_read) patch("/accounts/update_avatar", AccountController, :update_avatar) patch("/accounts/update_banner", AccountController, :update_banner) diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/notification_controller_test.exs similarity index 97% rename from test/web/pleroma_api/controllers/pleroma_api_controller_test.exs rename to test/web/pleroma_api/controllers/notification_controller_test.exs index c4c661266..7c5ace804 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/notification_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do +defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Notification From 9bc5e18adeef2c68c5fae2435ed01555f1b29c93 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 21 May 2020 08:06:57 +0300 Subject: [PATCH 7/8] rename mix task: `pleroma.user unsubscribe` -> `pleroma.user deactivate` --- docs/administration/CLI_tasks/user.md | 19 +++++++++---------- lib/mix/tasks/pleroma/user.ex | 6 +++--- test/tasks/user_test.exs | 8 ++++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index f535dad82..797641898 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -95,33 +95,33 @@ mix pleroma.user sign_out ``` -## Deactivate or activate a user +## Deactivate or activate a user ```sh tab="OTP" - ./bin/pleroma_ctl user toggle_activated + ./bin/pleroma_ctl user toggle_activated ``` ```sh tab="From Source" -mix pleroma.user toggle_activated +mix pleroma.user toggle_activated ``` -## Unsubscribe local users from a user and deactivate the user +## Deactivate a user and unsubscribes local users from the user ```sh tab="OTP" - ./bin/pleroma_ctl user unsubscribe NICKNAME + ./bin/pleroma_ctl user deactivate NICKNAME ``` ```sh tab="From Source" -mix pleroma.user unsubscribe NICKNAME +mix pleroma.user deactivate NICKNAME ``` -## Unsubscribe local users from an instance and deactivate all accounts on it +## Deactivate all accounts from an instance and unsubscribe local users on it ```sh tab="OTP" - ./bin/pleroma_ctl user unsubscribe_all_from_instance + ./bin/pleroma_ctl user deacitivate_all_from_instance ``` ```sh tab="From Source" -mix pleroma.user unsubscribe_all_from_instance +mix pleroma.user deactivate_all_from_instance ``` @@ -177,4 +177,3 @@ mix pleroma.user untag ```sh tab="From Source" mix pleroma.user toggle_confirmed ``` - diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 93ecb4631..3635c02bc 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -144,7 +144,7 @@ def run(["reset_password", nickname]) do end end - def run(["unsubscribe", nickname]) do + def run(["deactivate", nickname]) do start_pleroma() with %User{} = user <- User.get_cached_by_nickname(nickname) do @@ -163,7 +163,7 @@ def run(["unsubscribe", nickname]) do end end - def run(["unsubscribe_all_from_instance", instance]) do + def run(["deactivate_all_from_instance", instance]) do start_pleroma() Pleroma.User.Query.build(%{nickname: "@#{instance}"}) @@ -171,7 +171,7 @@ def run(["unsubscribe_all_from_instance", instance]) do |> Stream.each(fn users -> users |> Enum.each(fn user -> - run(["unsubscribe", user.nickname]) + run(["deactivate", user.nickname]) end) end) |> Stream.run() diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index 4b3ab5a87..ab7637511 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -169,7 +169,7 @@ test "no user to toggle" do end end - describe "running unsubscribe" do + describe "running deactivate" do test "user is unsubscribed" do followed = insert(:user) remote_followed = insert(:user, local: false) @@ -178,7 +178,7 @@ test "user is unsubscribed" do User.follow(user, followed, :follow_accept) User.follow(user, remote_followed, :follow_accept) - Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname]) + Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname]) assert_received {:mix_shell, :info, [message]} assert message =~ "Deactivating" @@ -192,8 +192,8 @@ test "user is unsubscribed" do assert user.deactivated end - test "no user to unsubscribe" do - Mix.Tasks.Pleroma.User.run(["unsubscribe", "nonexistent"]) + test "no user to deactivate" do + Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"]) assert_received {:mix_shell, :error, [message]} assert message =~ "No user" From 4ae2f75c3e5d293c24fac978b1ae10fdfa7a3c00 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 21 May 2020 10:27:06 +0000 Subject: [PATCH 8/8] Apply suggestion to docs/administration/CLI_tasks/user.md --- docs/administration/CLI_tasks/user.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 797641898..afeb8d52f 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -117,7 +117,7 @@ mix pleroma.user deactivate NICKNAME ## Deactivate all accounts from an instance and unsubscribe local users on it ```sh tab="OTP" - ./bin/pleroma_ctl user deacitivate_all_from_instance + ./bin/pleroma_ctl user deactivate_all_from_instance ``` ```sh tab="From Source"