Merge branch 'account-notes' into 'develop'
authorAlex Gleason <alex@alexgleason.me>
Sat, 25 Dec 2021 01:41:12 +0000 (01:41 +0000)
committerAlex Gleason <alex@alexgleason.me>
Sat, 25 Dec 2021 01:41:12 +0000 (01:41 +0000)
MastoAPI: Add user notes on accounts

See merge request pleroma/pleroma!3540

1  2 
docs/development/API/pleroma_api.md
lib/pleroma/web/api_spec/operations/account_operation.ex
lib/pleroma/web/api_spec/schemas/account.ex
lib/pleroma/web/api_spec/schemas/account_relationship.ex
lib/pleroma/web/api_spec/schemas/status.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/router.ex
test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
test/pleroma/web/mastodon_api/views/account_view_test.exs

index 74a1ad206347c1b2c9bffd0fe1cc955beba73047,b401a7cc74b0ff3608c1d8596e6c4cd1d0542da5..0e7367a726fb086875295684c1c6fdbeb75acfe3
@@@ -159,11 -159,11 +159,12 @@@ See [Admin-API](admin_api.md
    "muting": false,
    "muting_notifications": false,
    "subscribing": true,
 +  "notifying": true,
    "requested": false,
    "domain_blocking": false,
    "showing_reblogs": true,
-   "endorsed": false
+   "endorsed": false,
+   "note": ""
  }
  ```
  
    "muting": false,
    "muting_notifications": false,
    "subscribing": false,
 +  "notifying": false,
    "requested": false,
    "domain_blocking": false,
    "showing_reblogs": true,
-   "endorsed": false
+   "endorsed": false,
+   "note": ""
  }
  ```
  
index 4fe5a3c033b94938423c842ddf3399f3dbb0d4a8,8613f3a98729faab595d45eb4d786492d2ba3483..8cd2e824d1e47d07eb5c1c24cefc7a869c043cc3
@@@ -226,12 -226,6 +226,12 @@@ defmodule Pleroma.Web.ApiSpec.AccountOp
                  type: :boolean,
                  description: "Receive this account's reblogs in home timeline? Defaults to true.",
                  default: true
 +              },
 +              notify: %Schema{
 +                type: :boolean,
 +                description:
 +                  "Receive notifications for all statuses posted by the account? Defaults to false.",
 +                default: false
                }
              }
            },
      }
    end
  
+   def note_operation do
+     %Operation{
+       tags: ["Account actions"],
+       summary: "Set a private note about a user.",
+       operationId: "AccountController.note",
+       security: [%{"oAuth" => ["follow", "write:accounts"]}],
+       requestBody: request_body("Parameters", note_request()),
+       description: "Create a note for the given account.",
+       parameters: [
+         %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+         Operation.parameter(
+           :comment,
+           :query,
+           %Schema{type: :string},
+           "Account note body"
+         )
+       ],
+       responses: %{
+         200 => Operation.response("Relationship", "application/json", AccountRelationship)
+       }
+     }
+   end
    def follow_by_uri_operation do
      %Operation{
        tags: ["Account actions"],
            "blocked_by" => true,
            "muting" => false,
            "muting_notifications" => false,
+           "note" => "",
            "requested" => false,
            "domain_blocking" => false,
            "subscribing" => false,
 +          "notifying" => false,
            "endorsed" => true
          },
          %{
            "blocked_by" => true,
            "muting" => true,
            "muting_notifications" => false,
+           "note" => "",
            "requested" => true,
            "domain_blocking" => false,
            "subscribing" => false,
 +          "notifying" => false,
            "endorsed" => false
          },
          %{
            "blocked_by" => false,
            "muting" => true,
            "muting_notifications" => false,
+           "note" => "",
            "requested" => false,
            "domain_blocking" => true,
            "subscribing" => true,
 +          "notifying" => true,
            "endorsed" => false
          }
        ]
      }
    end
  
+   defp note_request do
+     %Schema{
+       title: "AccountNoteRequest",
+       description: "POST body for adding a note for an account",
+       type: :object,
+       properties: %{
+         comment: %Schema{
+           type: :string,
+           description: "Account note body"
+         }
+       },
+       example: %{
+         "comment" => "Example note"
+       }
+     }
+   end
    defp array_of_lists do
      %Schema{
        title: "ArrayOfLists",
index ad1a855441374cd5587757c93dd85372faf40d8b,e0bd2728b8731864696aca6cec008a2cea885468..548e7054412f32a3874ef7bce139828bed0593ee
@@@ -194,10 -194,10 +194,11 @@@ defmodule Pleroma.Web.ApiSpec.Schemas.A
            "id" => "9tKi3esbG7OQgZ2920",
            "muting" => false,
            "muting_notifications" => false,
+           "note" => "",
            "requested" => false,
            "showing_reblogs" => true,
 -          "subscribing" => false
 +          "subscribing" => false,
 +          "notifying" => false
          },
          "settings_store" => %{
            "pleroma-fe" => %{}
index b4f6d25b091de440d132e7d5548f2c8769574a68,163066032a9154d690b977dfe84ce06cfac48433..5d9e3b56e3310f2893fb784cc2921d9c4aacfdce
@@@ -22,10 -22,10 +22,11 @@@ defmodule Pleroma.Web.ApiSpec.Schemas.A
        id: FlakeID,
        muting: %Schema{type: :boolean},
        muting_notifications: %Schema{type: :boolean},
+       note: %Schema{type: :string},
        requested: %Schema{type: :boolean},
        showing_reblogs: %Schema{type: :boolean},
 -      subscribing: %Schema{type: :boolean}
 +      subscribing: %Schema{type: :boolean},
 +      notifying: %Schema{type: :boolean}
      },
      example: %{
        "blocked_by" => false,
        "id" => "9tKi3esbG7OQgZ2920",
        "muting" => false,
        "muting_notifications" => false,
+       "note" => "",
        "requested" => false,
        "showing_reblogs" => true,
 -      "subscribing" => false
 +      "subscribing" => false,
 +      "notifying" => false
      }
    })
  end
index 0bf3312d1c9f6b663c0c5738e5e63bfa84eeb415,60801f3227e1f103b1441b61d8d695b115eb5bac..3caab0f00dd7f723144e73d89d092569b30a115b
@@@ -282,10 -282,10 +282,11 @@@ defmodule Pleroma.Web.ApiSpec.Schemas.S
              "id" => "9toJCsKN7SmSf3aj5c",
              "muting" => false,
              "muting_notifications" => false,
+             "note" => "",
              "requested" => false,
              "showing_reblogs" => true,
 -            "subscribing" => false
 +            "subscribing" => false,
 +            "notifying" => false
            },
            "skip_thread_containment" => false,
            "tags" => []
index 4290d11aeccd721c432880fb48aade501e36978c,a3a9f95487a00e56e6087dd05df91528ace7513c..3c8dd03537b5d79860ffec4a0c9e58e733b43700
@@@ -7,6 -7,7 +7,7 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
  
    alias Pleroma.FollowingRelationship
    alias Pleroma.User
+   alias Pleroma.UserNote
    alias Pleroma.UserRelationship
    alias Pleroma.Web.CommonAPI.Utils
    alias Pleroma.Web.MastodonAPI.AccountView
          User.following?(target, reading_user)
        end
  
 +    subscribing =
 +      UserRelationship.exists?(
 +        user_relationships,
 +        :inverse_subscription,
 +        target,
 +        reading_user,
 +        &User.subscribed_to?(&2, &1)
 +      )
 +
      # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
      %{
        id: to_string(target.id),
            target,
            &User.muted_notifications?(&1, &2)
          ),
 -      subscribing:
 -        UserRelationship.exists?(
 -          user_relationships,
 -          :inverse_subscription,
 -          target,
 -          reading_user,
 -          &User.subscribed_to?(&2, &1)
 -        ),
 +      subscribing: subscribing,
 +      notifying: subscribing,
        requested: follow_state == :follow_pending,
        domain_blocking: User.blocks_domain?(reading_user, target),
        showing_reblogs:
            target,
            &User.muting_reblogs?(&1, &2)
          ),
-       endorsed: false
+       endorsed: false,
+       note:
+         UserNote.show(
+           reading_user,
+           target
+         )
      }
    end
  
          ap_id: user.ap_id,
          also_known_as: user.also_known_as,
          is_confirmed: user.is_confirmed,
 +        is_suggested: user.is_suggested,
          tags: user.tags,
          hide_followers_count: user.hide_followers_count,
          hide_follows_count: user.hide_follows_count,
index 965cd507ff4707d00c2b2383e718d1eb1288ca96,ca5db8ea359bf1a7433abea01c2e320540d1286c..9ce35ad6bd597d68ead6fd7af1bc72b4b139bfbc
@@@ -4,7 -4,6 +4,7 @@@
  
  defmodule Pleroma.Web.Router do
    use Pleroma.Web, :router
 +  import Phoenix.LiveDashboard.Router
  
    pipeline :accepts_html do
      plug(:accepts, ["html"])
      get("/emoji", UtilController, :emoji)
      get("/captcha", UtilController, :captcha)
      get("/healthcheck", UtilController, :healthcheck)
 +    post("/remote_interaction", UtilController, :remote_interaction)
    end
  
    scope "/api/v1/pleroma", Pleroma.Web do
      post("/uploader_callback/:upload_path", UploaderController, :callback)
    end
  
 +  # AdminAPI: only admins can perform these actions
    scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
      pipe_through([:admin_api, :require_admin])
  
      put("/users/disable_mfa", AdminAPIController, :disable_mfa)
 -    put("/users/tag", AdminAPIController, :tag_users)
 -    delete("/users/tag", AdminAPIController, :untag_users)
  
      get("/users/:nickname/permission_group", AdminAPIController, :right_get)
      get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
  
      post("/users/follow", UserController, :follow)
      post("/users/unfollow", UserController, :unfollow)
 -    delete("/users", UserController, :delete)
      post("/users", UserController, :create)
 +
 +    patch("/users/suggest", UserController, :suggest)
 +    patch("/users/unsuggest", UserController, :unsuggest)
 +
 +    get("/relay", RelayController, :index)
 +    post("/relay", RelayController, :follow)
 +    delete("/relay", RelayController, :unfollow)
 +
 +    get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
 +    patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
 +    get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
 +    patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
 +
 +    get("/instance_document/:name", InstanceDocumentController, :show)
 +    patch("/instance_document/:name", InstanceDocumentController, :update)
 +    delete("/instance_document/:name", InstanceDocumentController, :delete)
 +
 +    patch("/users/confirm_email", AdminAPIController, :confirm_email)
 +    patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
 +
 +    get("/config", ConfigController, :show)
 +    post("/config", ConfigController, :update)
 +    get("/config/descriptions", ConfigController, :descriptions)
 +    get("/need_reboot", AdminAPIController, :need_reboot)
 +    get("/restart", AdminAPIController, :restart)
 +
 +    get("/oauth_app", OAuthAppController, :index)
 +    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)
 +
 +    get("/frontends", FrontendController, :index)
 +    post("/frontends/install", FrontendController, :install)
 +
 +    post("/backups", AdminAPIController, :create_backup)
 +  end
 +
 +  # AdminAPI: admins and mods (staff) can perform these actions
 +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
 +    pipe_through(:admin_api)
 +
 +    put("/users/tag", AdminAPIController, :tag_users)
 +    delete("/users/tag", AdminAPIController, :untag_users)
 +
      patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
      patch("/users/activate", UserController, :activate)
      patch("/users/deactivate", UserController, :deactivate)
      patch("/users/approve", UserController, :approve)
  
 -    get("/relay", RelayController, :index)
 -    post("/relay", RelayController, :follow)
 -    delete("/relay", RelayController, :unfollow)
 +    delete("/users", UserController, :delete)
  
      post("/users/invite_token", InviteController, :create)
      get("/users/invites", InviteController, :index)
      get("/instances/:instance/statuses", InstanceController, :list_statuses)
      delete("/instances/:instance", InstanceController, :delete)
  
 -    get("/instance_document/:name", InstanceDocumentController, :show)
 -    patch("/instance_document/:name", InstanceDocumentController, :update)
 -    delete("/instance_document/:name", InstanceDocumentController, :delete)
 -
 -    patch("/users/confirm_email", AdminAPIController, :confirm_email)
 -    patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
 -
      get("/reports", ReportController, :index)
      get("/reports/:id", ReportController, :show)
      patch("/reports", ReportController, :update)
      delete("/statuses/:id", StatusController, :delete)
      get("/statuses", StatusController, :index)
  
 -    get("/config", ConfigController, :show)
 -    post("/config", ConfigController, :update)
 -    get("/config/descriptions", ConfigController, :descriptions)
 -    get("/need_reboot", AdminAPIController, :need_reboot)
 -    get("/restart", AdminAPIController, :restart)
 -
      get("/moderation_log", AdminAPIController, :list_log)
  
      post("/reload_emoji", AdminAPIController, :reload_emoji)
      get("/stats", AdminAPIController, :stats)
  
 -    get("/oauth_app", OAuthAppController, :index)
 -    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)
 -
      get("/chats/:id", ChatController, :show)
      get("/chats/:id/messages", ChatController, :messages)
      delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
 -
 -    get("/frontends", FrontendController, :index)
 -    post("/frontends/install", FrontendController, :install)
 -
 -    post("/backups", AdminAPIController, :create_backup)
    end
  
    scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
      scope "/pack" do
 -      pipe_through([:admin_api, :require_admin])
 +      pipe_through(:admin_api)
  
        post("/", EmojiPackController, :create)
        patch("/", EmojiPackController, :update)
  
      # Modifying packs
      scope "/packs" do
 -      pipe_through([:admin_api, :require_admin])
 +      pipe_through(:admin_api)
  
        get("/import", EmojiPackController, :import_from_filesystem)
        get("/remote", EmojiPackController, :remote)
      post("/accounts/:id/unblock", AccountController, :unblock)
      post("/accounts/:id/mute", AccountController, :mute)
      post("/accounts/:id/unmute", AccountController, :unmute)
+     post("/accounts/:id/note", AccountController, :note)
  
      get("/conversations", ConversationController, :index)
      post("/conversations/:id/read", ConversationController, :mark_as_read)
      delete("/push/subscription", SubscriptionController, :delete)
  
      get("/suggestions", SuggestionController, :index)
 +    delete("/suggestions/:account_id", SuggestionController, :dismiss)
  
      get("/timelines/home", TimelineController, :home)
      get("/timelines/direct", TimelineController, :direct)
      get("/search", SearchController, :search2)
  
      post("/media", MediaController, :create2)
 +
 +    get("/suggestions", SuggestionController, :index2)
    end
  
    scope "/api", Pleroma.Web do
      get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
    end
  
 +  scope "/", Pleroma.Web do
 +    pipe_through(:api)
 +
 +    get("/manifest.json", ManifestController, :show)
 +  end
 +
 +  scope "/", Pleroma.Web do
 +    pipe_through(:pleroma_html)
 +
 +    post("/auth/password", TwitterAPI.PasswordController, :request)
 +  end
 +
    scope "/proxy/", Pleroma.Web do
      get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
      get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
      end
    end
  
 +  scope "/" do
 +    pipe_through([:pleroma_html, :authenticate, :require_admin])
 +    live_dashboard("/phoenix/live_dashboard")
 +  end
 +
    # Test-only routes needed to test action dispatching and plug chain execution
    if Pleroma.Config.get(:env) == :test do
      @test_actions [
index 581944b8a1b5aceaff68a2198de31767f90685b6,4f855ac5c696ef07ca3754e1ab04491c1c09b70b..966a4072d4665c92c42bd8a5247a62b49d05bb17
@@@ -922,27 -922,6 +922,27 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
                 |> json_response_and_validate_schema(200)
      end
  
 +    test "following with subscription and unsubscribing" do
 +      %{conn: conn} = oauth_access(["follow"])
 +      followed = insert(:user)
 +
 +      ret_conn =
 +        conn
 +        |> put_req_header("content-type", "application/json")
 +        |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: true})
 +
 +      assert %{"id" => _id, "subscribing" => true} =
 +               json_response_and_validate_schema(ret_conn, 200)
 +
 +      ret_conn =
 +        conn
 +        |> put_req_header("content-type", "application/json")
 +        |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: false})
 +
 +      assert %{"id" => _id, "subscribing" => false} =
 +               json_response_and_validate_schema(ret_conn, 200)
 +    end
 +
      test "following / unfollowing errors", %{user: user, conn: conn} do
        # self follow
        conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
  
      assert [%{"id" => ^id2}] = result
    end
+   test "create a note on a user" do
+     %{conn: conn} = oauth_access(["write:accounts", "read:follows"])
+     other_user = insert(:user)
+     conn
+     |> put_req_header("content-type", "application/json")
+     |> post("/api/v1/accounts/#{other_user.id}/note", %{
+       "comment" => "Example note"
+     })
+     assert [%{"note" => "Example note"}] =
+              conn
+              |> put_req_header("content-type", "application/json")
+              |> get("/api/v1/accounts/relationships?id=#{other_user.id}")
+              |> json_response_and_validate_schema(200)
+   end
  end
index a37169bf9d802513562f53b3b2d9ef4ec95ee86e,9fe9d73bc7556e42b2b2c58865f76fa383e4f978..39b9b0cef5278faaef45f70bd1c08284c3c37766
@@@ -83,7 -83,6 +83,7 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
          tags: [],
          is_admin: false,
          is_moderator: false,
 +        is_suggested: false,
          hide_favorites: true,
          hide_followers: false,
          hide_follows: false,
          tags: [],
          is_admin: false,
          is_moderator: false,
 +        is_suggested: false,
          hide_favorites: true,
          hide_followers: false,
          hide_follows: false,
        muting: false,
        muting_notifications: false,
        subscribing: false,
 +      notifying: false,
        requested: false,
        domain_blocking: false,
        showing_reblogs: true,
-       endorsed: false
+       endorsed: false,
+       note: ""
      }
  
      test "represent a relationship for the following and followed user" do
              muting: true,
              muting_notifications: true,
              subscribing: true,
 +            notifying: true,
              showing_reblogs: false,
              id: to_string(other_user.id)
            }