Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 投稿の編集に対応 #14011

Open
wants to merge 75 commits into
base: develop
Choose a base branch
from

Conversation

GrapeApple0
Copy link
Sponsor Contributor

@GrapeApple0 GrapeApple0 commented Jun 15, 2024

What

ActivityPub経由の投稿の編集に対応しました。

Why

Mastodonなどからの投稿の編集が反映されないため
Resolve: #8364
Related: #11944

Additional info (optional)

  • 投稿の編集の受信
  • 投稿の編集の送信
  • 編集履歴
  • フロントエンド側の対応
  • テストの実装
  • 投票を含む投稿の編集

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

@github-actions github-actions bot added packages/backend Server side specific issue/PR packages/misskey-js labels Jun 15, 2024
Copy link

codecov bot commented Jun 15, 2024

Codecov Report

Attention: Patch coverage is 67.09356% with 925 lines in your changes missing coverage. Please review.

Project coverage is 42.08%. Comparing base (3fe7e37) to head (415168d).
Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
packages/backend/src/core/NoteEditService.ts 32.90% 310 Missing ⚠️
...ckend/src/core/activitypub/models/ApNoteService.ts 4.61% 186 Missing ⚠️
packages/frontend/src/components/MkNoteHistory.vue 0.00% 147 Missing and 1 partial ⚠️
...kend/src/core/entities/NoteHistoryEntityService.ts 47.55% 75 Missing ⚠️
...s/backend/src/server/api/endpoints/notes/update.ts 71.59% 50 Missing ⚠️
...ackages/frontend/src/components/MkNoteDetailed.vue 0.00% 36 Missing ⚠️
...ackend/src/server/api/endpoints/notes/histories.ts 69.73% 23 Missing ⚠️
packages/frontend/src/pages/admin/roles.editor.vue 0.00% 20 Missing ⚠️
packages/frontend/src/os.ts 13.63% 19 Missing ⚠️
...ges/backend/src/core/entities/NoteEntityService.ts 74.24% 17 Missing ⚠️
... and 8 more
Additional details and impacted files
@@             Coverage Diff              @@
##           develop   #14011       +/-   ##
============================================
+ Coverage    20.17%   42.08%   +21.90%     
============================================
  Files          725     1560      +835     
  Lines       100502   199746    +99244     
  Branches      1045     3620     +2575     
============================================
+ Hits         20279    84063    +63784     
- Misses       79681   115090    +35409     
- Partials       542      593       +51     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

github-actions bot commented Jun 15, 2024

このPRによるapi.jsonの差分

差分はこちら
--- base
+++ head
@@ -56808,6 +56808,183 @@
         }
       }
     },
+    "/notes/histories": {
+      "post": {
+        "operationId": "notes___histories",
+        "summary": "notes/histories",
+        "description": "No description provided.\n\n**Credential required**: *No*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/notes/histories.ts"
+        },
+        "tags": [
+          "notes"
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "limit": {
+                    "type": "integer",
+                    "minimum": 1,
+                    "maximum": 100,
+                    "default": 10
+                  },
+                  "noteId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "sinceId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "untilId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  }
+                },
+                "required": [
+                  "noteId"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "OK (with results)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "$ref": "#/components/schemas/NoteHistory"
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_NOTE": {
+                    "value": {
+                      "error": {
+                        "message": "No such note.",
+                        "code": "NO_SUCH_NOTE",
+                        "id": "24fcbfc6-2e37-42b6-8388-c29b3861a08d"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/notes/hybrid-timeline": {
       "post": {
         "operationId": "notes___hybrid-timeline",
@@ -60509,6 +60686,302 @@
         }
       }
     },
+    "/notes/update": {
+      "post": {
+        "operationId": "notes___update",
+        "summary": "notes/update",
+        "description": "No description provided.\n\n**Credential required**: *Yes* / **Permission**: *write:notes*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/notes/update.ts"
+        },
+        "tags": [
+          "notes"
+        ],
+        "security": [
+          {
+            "bearerAuth": []
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "noteId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "text": {
+                    "type": [
+                      "string",
+                      "null"
+                    ],
+                    "minLength": 1,
+                    "maxLength": 3000
+                  },
+                  "cw": {
+                    "type": [
+                      "string",
+                      "null"
+                    ],
+                    "maxLength": 100
+                  },
+                  "fileIds": {
+                    "type": "array",
+                    "uniqueItems": true,
+                    "minItems": 1,
+                    "maxItems": 16,
+                    "items": {
+                      "type": "string",
+                      "format": "misskey:id"
+                    }
+                  },
+                  "poll": {
+                    "type": [
+                      "object",
+                      "null"
+                    ],
+                    "properties": {
+                      "choices": {
+                        "type": "array",
+                        "uniqueItems": true,
+                        "minItems": 2,
+                        "maxItems": 10,
+                        "items": {
+                          "type": "string",
+                          "minLength": 1,
+                          "maxLength": 50
+                        }
+                      },
+                      "multiple": {
+                        "type": "boolean"
+                      },
+                      "expiresAt": {
+                        "type": [
+                          "integer",
+                          "null"
+                        ]
+                      },
+                      "expiredAfter": {
+                        "type": [
+                          "integer",
+                          "null"
+                        ],
+                        "minimum": 1
+                      }
+                    },
+                    "required": [
+                      "choices"
+                    ]
+                  }
+                },
+                "if": {
+                  "properties": {
+                    "renoteId": {
+                      "type": "null"
+                    },
+                    "fileIds": {
+                      "type": "null"
+                    },
+                    "poll": {
+                      "type": "null"
+                    }
+                  }
+                },
+                "then": {
+                  "properties": {
+                    "text": {
+                      "type": "string",
+                      "minLength": 1,
+                      "maxLength": 3000,
+                      "pattern": "[^\\s]+"
+                    }
+                  },
+                  "required": [
+                    "text"
+                  ]
+                },
+                "required": [
+                  "noteId",
+                  "text",
+                  "cw"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "204": {
+            "description": "OK (without any results)"
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_NOTE": {
+                    "value": {
+                      "error": {
+                        "message": "No such note.",
+                        "code": "NO_SUCH_NOTE",
+                        "id": "a6584e14-6e01-4ad3-b566-851e7bf0d474"
+                      }
+                    }
+                  },
+                  "ACCESS_DENIED": {
+                    "value": {
+                      "error": {
+                        "message": "Access denied.",
+                        "code": "ACCESS_DENIED",
+                        "id": "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9"
+                      }
+                    }
+                  },
+                  "CANNOT_EDIT_NOTE": {
+                    "value": {
+                      "error": {
+                        "message": "Editing notes are not allowed by the role policy.",
+                        "code": "CANNOT_EDIT_NOTE",
+                        "id": "59ece09c-56ab-4bd5-905c-0f6bbf5af143"
+                      }
+                    }
+                  },
+                  "CONTAINS_PROHIBITED_WORDS": {
+                    "value": {
+                      "error": {
+                        "message": "Cannot post because it contains prohibited words.",
+                        "code": "CONTAINS_PROHIBITED_WORDS",
+                        "id": "aa6e01d3-a85c-669d-758a-76aab43af334"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "429": {
+            "description": "To many requests",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "RATE_LIMIT_EXCEEDED": {
+                    "value": {
+                      "error": {
+                        "message": "Rate limit exceeded. Please try again later.",
+                        "code": "RATE_LIMIT_EXCEEDED",
+                        "id": "d5826d14-3982-4d2e-8011-b9e9f02499ef"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/notes/user-list-timeline": {
       "post": {
         "operationId": "notes___user-list-timeline",
@@ -77706,6 +78179,13 @@
             "type": "string",
             "format": "date-time"
           },
+          "updatedAt": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "format": "date-time"
+          },
           "deletedAt": {
             "type": [
               "string",
@@ -77998,6 +78478,123 @@
           "repliesCount"
         ]
       },
+      "NoteHistory": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string",
+            "format": "id",
+            "example": "xxxxxxxxxx"
+          },
+          "targetId": {
+            "type": "string",
+            "format": "id",
+            "example": "xxxxxxxxxx"
+          },
+          "createdAt": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "text": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "cw": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "mentions": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "format": "id"
+            }
+          },
+          "fileIds": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "format": "id"
+            }
+          },
+          "files": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "$ref": "#/components/schemas/DriveFile"
+            }
+          },
+          "tags": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "poll": {
+            "type": [
+              "object",
+              "null"
+            ],
+            "properties": {
+              "expiresAt": {
+                "type": [
+                  "string",
+                  "null"
+                ],
+                "format": "date-time"
+              },
+              "multiple": {
+                "type": "boolean"
+              },
+              "choices": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "isVoted": {
+                      "type": "boolean"
+                    },
+                    "text": {
+                      "type": "string"
+                    },
+                    "votes": {
+                      "type": "number"
+                    }
+                  },
+                  "required": [
+                    "isVoted",
+                    "text",
+                    "votes"
+                  ]
+                }
+              }
+            },
+            "required": [
+              "multiple",
+              "choices"
+            ]
+          },
+          "emojis": {
+            "type": "object",
+            "additionalProperties": {
+              "anyOf": [
+                {
+                  "type": "string"
+                }
+              ]
+            }
+          }
+        },
+        "required": [
+          "id",
+          "targetId",
+          "createdAt"
+        ]
+      },
       "NoteReaction": {
         "type": "object",
         "properties": {
@@ -80445,6 +81042,9 @@
           "canPublicNote": {
             "type": "boolean"
           },
+          "canEditNote": {
+            "type": "boolean"
+          },
           "mentionLimit": {
             "type": "integer"
           },
@@ -80519,6 +81119,7 @@
           "gtlAvailable",
           "ltlAvailable",
           "canPublicNote",
+          "canEditNote",
           "mentionLimit",
           "canInvite",
           "inviteLimit",

Get diff files from Workflow Page

@KisaragiEffective KisaragiEffective added the 🌌Federation The Federation/ActivityPub feature label Jun 15, 2024
@kakkokari-gtyih
Copy link
Contributor

(ある場合)検索インデックスの更新とかハッシュタグテーブルの更新とかって、これでできてるのかしら(以前はそのへんまで対応が回らなかったのでお流れになったため)

@kakkokari-gtyih
Copy link
Contributor

kakkokari-gtyih commented Jun 16, 2024

(ある場合)検索インデックスの更新とかハッシュタグテーブルの更新とかって、これでできてるのかしら(以前はそのへんまで対応が回らなかったのでお流れになったため)

やってるっぽかった🙏
ドライブのセンシティブフラグが変わったときに紐付くノートについて連合に対する更新処理をかける必要もありそう

@syuilo
Copy link
Member

syuilo commented Jun 16, 2024

実装できたとしても編集を実現するためだけにしてはコード量が多くなりすぎるから、テストをかなり充実させるとかでない限り採用は難しいかもしれない

@GrapeApple0
Copy link
Sponsor Contributor Author

とりあえずざっと見たところだけ書きました🙏

修正できそうな部分は修正しました🙏

Copy link
Contributor

@zyoshoka zyoshoka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

とりあえず


if (data.poll && data.poll.expiresAt) {
const delay = data.poll.expiresAt.getTime() - Date.now();
this.queueService.endedPollNotificationQueue.add(note.id, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

元のノートに期限付き投票が含まれている、かつその期限を迎えていない場合、キューがダブってしまうので元のキューを消す必要がありそうです

const showContent = ref(false);
const menuButton = ref<HTMLButtonElement | null>(null);
const props = defineProps<{
text: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text は nullable なので要修正

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ファイル・投票の編集も表示されてほしい気がします

@@ -68,6 +68,7 @@ export const moderationLogTypes = [
'promoteQueue',
'deleteDriveFile',
'deleteNote',
'editNote',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ロケールの追加必要そう & modlog.ModLog.vue でいい感じに差分表示できると良さそうです

packages/misskey-js/src/consts.ts Outdated Show resolved Hide resolved
packages/frontend/src/components/MkEditForm.vue Outdated Show resolved Hide resolved
packages/backend/src/core/NoteEditService.ts Outdated Show resolved Hide resolved
: null,
name: data.name,
text: data.text,
hasPoll: data.poll != null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

現状 notes/update では poll も編集可能になっていて、それに応じてノートの hasPoll も更新されるようになっていますが、現状その投票の変更についてはデータベースに登録されていません。よって例えば、投票のないノートを編集して新たな投票を付けた場合、hasPolltrue になる一方でそのノートに紐づく投票が存在しないので後続の pack で失敗してしまいます。

とはいえ、そもそも投票の選択肢が編集できてしまうのはマズイ気がする(例えば2択の投票の選択肢を終了間際に反転させるなどができる)ので API で禁止すべきかと思います(これは issue で議論すべき)。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そもそも投票の選択肢が編集できてしまうのはマズイ気がする(例えば2択の投票の選択肢を終了間際に反転させるなどができる)

Mastodon では投票の選択肢を編集するとそれまでに入った票はリセットされるようなので、その方向性の実装でこの懸念は解消できるかもしれません

@GrapeApple0
Copy link
Sponsor Contributor Author

投票の編集(投票を含む投稿の編集)はMastodonの場合だとNoteではなくQuestion型で飛んでくるので、別途対応する必要がありそうです

Copy link
Contributor

github-actions bot commented Sep 3, 2024

Thank you for sending us a great Pull Request! 👍
Please regenerate misskey-js type definitions! 🙏

example:

pnpm run build-misskey-js-with-types

@zyoshoka
Copy link
Contributor

zyoshoka commented Sep 7, 2024

ドライブのファイルがアップデートされたときに紐づくノートに対してもUpdateアクティビティを出してあげると良さそうです(センシティブフラグが変更された際などに再登録する系のユースケース)

これは別の PR でやるべき内容かなと個人的には思います(そもそもちょっと懸念がある)

#11209 (comment)

@zyoshoka
Copy link
Contributor

zyoshoka commented Sep 7, 2024

(とはいえ、センシティブフラグを反映させるためだけにノートに適当な編集を加えないといけないのが変な挙動なのは確か)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🌌Federation The Federation/ActivityPub feature packages/backend:test packages/backend Server side specific issue/PR packages/frontend Client side specific issue/PR packages/misskey-js
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Recieve and Apply Mastodon Note Edits
6 participants