From 40414bf177c93b39d75c6091ef0ce1db093edb6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= <git@mkljczk.pl>
Date: Sun, 21 Nov 2021 16:53:30 +0100
Subject: [PATCH] MastoAPI: Add user notes on accounts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
---
 docs/development/API/pleroma_api.md           |  6 ++-
 lib/pleroma/user_note.ex                      | 52 +++++++++++++++++++
 .../api_spec/operations/account_operation.ex  | 43 +++++++++++++++
 lib/pleroma/web/api_spec/schemas/account.ex   |  1 +
 .../api_spec/schemas/account_relationship.ex  |  2 +
 lib/pleroma/web/api_spec/schemas/status.ex    |  1 +
 .../controllers/account_controller.ex         | 18 ++++++-
 .../web/mastodon_api/views/account_view.ex    |  8 ++-
 lib/pleroma/web/router.ex                     |  1 +
 .../20211121000000_create_user_notes.exs      | 17 ++++++
 .../controllers/account_controller_test.exs   | 14 +++++
 .../mastodon_api/views/account_view_test.exs  |  3 +-
 12 files changed, 160 insertions(+), 6 deletions(-)
 create mode 100644 lib/pleroma/user_note.ex
 create mode 100644 priv/repo/migrations/20211121000000_create_user_notes.exs

diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md
index 8f6422da0..b401a7cc7 100644
--- a/docs/development/API/pleroma_api.md
+++ b/docs/development/API/pleroma_api.md
@@ -162,7 +162,8 @@ See [Admin-API](admin_api.md)
   "requested": false,
   "domain_blocking": false,
   "showing_reblogs": true,
-  "endorsed": false
+  "endorsed": false,
+  "note": ""
 }
 ```
 
@@ -186,7 +187,8 @@ See [Admin-API](admin_api.md)
   "requested": false,
   "domain_blocking": false,
   "showing_reblogs": true,
-  "endorsed": false
+  "endorsed": false,
+  "note": ""
 }
 ```
 
diff --git a/lib/pleroma/user_note.ex b/lib/pleroma/user_note.ex
new file mode 100644
index 000000000..5e82d359f
--- /dev/null
+++ b/lib/pleroma/user_note.ex
@@ -0,0 +1,52 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.UserNote do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+  import Ecto.Query
+
+  alias Pleroma.Repo
+  alias Pleroma.User
+  alias Pleroma.UserNote
+
+  schema "user_notes" do
+    belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
+    belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
+    field(:comment, :string)
+
+    timestamps()
+  end
+
+  def changeset(%UserNote{} = user_note, params \\ %{}) do
+    user_note
+    |> cast(params, [:source_id, :target_id, :comment])
+    |> validate_required([:source_id, :target_id])
+  end
+
+  def show(%User{} = source, %User{} = target) do
+    with %UserNote{} = note <-
+           UserNote
+           |> where(source_id: ^source.id, target_id: ^target.id)
+           |> Repo.one() do
+      note.comment
+    else
+      _ -> ""
+    end
+  end
+
+  def create(%User{} = source, %User{} = target, comment) do
+    %UserNote{}
+    |> changeset(%{
+      source_id: source.id,
+      target_id: target.id,
+      comment: comment
+    })
+    |> Repo.insert(
+      on_conflict: {:replace, [:comment]},
+      conflict_target: [:source_id, :target_id]
+    )
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 54e5ebc76..6bec9f178 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -328,6 +328,29 @@ def unblock_operation do
     }
   end
 
+  def note_operation do
+    %Operation{
+      tags: ["Account actions"],
+      summary: "Create note",
+      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"],
@@ -685,6 +708,7 @@ defp array_of_relationships do
           "blocked_by" => true,
           "muting" => false,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => false,
           "domain_blocking" => false,
           "subscribing" => false,
@@ -699,6 +723,7 @@ defp array_of_relationships do
           "blocked_by" => true,
           "muting" => true,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => true,
           "domain_blocking" => false,
           "subscribing" => false,
@@ -713,6 +738,7 @@ defp array_of_relationships do
           "blocked_by" => false,
           "muting" => true,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => false,
           "domain_blocking" => true,
           "subscribing" => true,
@@ -760,6 +786,23 @@ defp mute_request do
     }
   end
 
+  defp note_request do
+    %Schema{
+      title: "AccountNoteRequest",
+      description: "POST body for adding anote 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",
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index bd7143ab9..e0bd2728b 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -194,6 +194,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
           "id" => "9tKi3esbG7OQgZ2920",
           "muting" => false,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => false,
           "showing_reblogs" => true,
           "subscribing" => false
diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
index 16b73ebb4..163066032 100644
--- a/lib/pleroma/web/api_spec/schemas/account_relationship.ex
+++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
@@ -22,6 +22,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
       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}
@@ -36,6 +37,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
       "id" => "9tKi3esbG7OQgZ2920",
       "muting" => false,
       "muting_notifications" => false,
+      "note" => "",
       "requested" => false,
       "showing_reblogs" => true,
       "subscribing" => false
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 3d042dc19..60801f322 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -282,6 +282,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
             "id" => "9toJCsKN7SmSf3aj5c",
             "muting" => false,
             "muting_notifications" => false,
+            "note" => "",
             "requested" => false,
             "showing_reblogs" => true,
             "subscribing" => false
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 5fcbffc34..8a43d49d3 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
 
   alias Pleroma.Maps
   alias Pleroma.User
+  alias Pleroma.UserNote
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.Pipeline
@@ -53,7 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
     when action in [:verify_credentials, :endorsements, :identity_proofs]
   )
 
-  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:accounts"]}
+    when action in [:update_credentials, :note]
+  )
 
   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
 
@@ -79,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
 
   @relationship_actions [:follow, :unfollow]
-  @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
+  @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note)a
 
   plug(
     RateLimiter,
@@ -435,6 +440,15 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
     end
   end
 
+  @doc "POST /api/v1/accounts/:id/note"
+  def note(%{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn, _params) do
+    with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
+      render(conn, "relationship.json", user: noter, target: target)
+    else
+      {:error, message} -> json_response(conn, :forbidden, %{error: message})
+    end
+  end
+
   @doc "POST /api/v1/follows"
   def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
     case User.get_cached_by_nickname(uri) do
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 9e9de33f6..a3a9f9548 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
   alias Pleroma.FollowingRelationship
   alias Pleroma.User
+  alias Pleroma.UserNote
   alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.AccountView
@@ -156,7 +157,12 @@ def render(
           target,
           &User.muting_reblogs?(&1, &2)
         ),
-      endorsed: false
+      endorsed: false,
+      note:
+        UserNote.show(
+          reading_user,
+          target
+        )
     }
   end
 
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index abb332ec2..ca5db8ea3 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -456,6 +456,7 @@ defmodule Pleroma.Web.Router do
     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)
diff --git a/priv/repo/migrations/20211121000000_create_user_notes.exs b/priv/repo/migrations/20211121000000_create_user_notes.exs
new file mode 100644
index 000000000..8fc23749f
--- /dev/null
+++ b/priv/repo/migrations/20211121000000_create_user_notes.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.CreateUserNotes do
+  use Ecto.Migration
+
+  def change do
+    create_if_not_exists table(:user_notes) do
+      add(:source_id, references(:users, type: :uuid, on_delete: :delete_all))
+      add(:target_id, references(:users, type: :uuid, on_delete: :delete_all))
+      add(:comment, :string)
+
+      timestamps()
+    end
+
+    create_if_not_exists(
+      unique_index(:user_notes, [:source_id, :target_id])
+    )
+  end
+end
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index a92a58224..48e658dd2 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -1776,4 +1776,18 @@ test "getting a list of blocks" do
 
     assert [%{"id" => ^id2}] = result
   end
+
+  test "create a note on a user" do
+    %{conn: conn} = oauth_access(["write:accounts"])
+    other_user = insert(:user)
+
+    ret_conn =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> post("/api/v1/accounts/#{other_user.id}/note", %{
+        "comment" => "Example note"
+      })
+
+    assert %{"note" => "Example note"} = json_response_and_validate_schema(ret_conn, 200)
+  end
 end
diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs
index 60881756d..9fe9d73bc 100644
--- a/test/pleroma/web/mastodon_api/views/account_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs
@@ -271,7 +271,8 @@ defp test_relationship_rendering(user, other_user, expected_result) do
       requested: false,
       domain_blocking: false,
       showing_reblogs: true,
-      endorsed: false
+      endorsed: false,
+      note: ""
     }
 
     test "represent a relationship for the following and followed user" do