-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
base: develop
Are you sure you want to change the base?
feat: 投稿の編集に対応 #14011
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #14011 +/- ##
===========================================
+ Coverage 40.18% 42.38% +2.20%
===========================================
Files 1524 1537 +13
Lines 188773 197676 +8903
Branches 3516 3564 +48
===========================================
+ Hits 75856 83789 +7933
- Misses 112345 113314 +969
- Partials 572 573 +1 ☔ View full report in Codecov by Sentry. |
このPRによるapi.jsonの差分 差分はこちら--- base
+++ head
@@ -56773,6 +56773,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",
@@ -60474,6 +60651,264 @@
}
}
},
+ "/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",
+ "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"
+ ]
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "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",
@@ -77671,6 +78106,13 @@
"type": "string",
"format": "date-time"
},
+ "updatedAt": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
"deletedAt": {
"type": [
"string",
@@ -77956,6 +78398,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": {
@@ -80305,6 +80864,9 @@
"canPublicNote": {
"type": "boolean"
},
+ "canEditNote": {
+ "type": "boolean"
+ },
"mentionLimit": {
"type": "integer"
},
@@ -80379,6 +80941,7 @@
"gtlAvailable",
"ltlAvailable",
"canPublicNote",
+ "canEditNote",
"mentionLimit",
"canInvite",
"inviteLimit", |
(ある場合)検索インデックスの更新とかハッシュタグテーブルの更新とかって、これでできてるのかしら(以前はそのへんまで対応が回らなかったのでお流れになったため) |
やってるっぽかった🙏 |
実装できたとしても編集を実現するためだけにしてはコード量が多くなりすぎるから、テストをかなり充実させるとかでない限り採用は難しいかもしれない |
テスト以外のおおよその実装は完了しました |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
とりあえずざっと見たところだけ書きました🙏
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> | ||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button> | ||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> | ||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
insertMention
を忘れてそうです
type: 'string', | ||
minLength: 1, | ||
maxLength: MAX_NOTE_TEXT_LENGTH, | ||
nullable: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ファイルか投票がついている、若しくはリノートの場合には text
は nullable になるので、そういうノートを編集しようとしたときに文字入力を要求されるのは不自然そうです。notes/create
のスキーマにある if
-then
を加えると良いと思います
</header> | ||
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/> | ||
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/> | ||
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quoteId
の編集は反映されないようなので
- 元ノートに引用がある場合はそれを消せないように
- 元ノートに引用がない場合はノート URL ペースト時に引用できないように
したほうが良さそうです
@@ -118,8 +118,7 @@ export interface NoteEventTypes { | |||
deletedAt: Date; | |||
}; | |||
updated: { | |||
cw: string | null; | |||
text: string; | |||
note: MiNote; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
に関してはストリーミングで受け取れているもののフロント側では反映されてなさそうなので use-note-capture
に処理を実装する必要がありそうです。あと misskey-js
の streaming.types.ts
に型を追加する必要がありそうです
} | ||
|
||
@bindThis | ||
public async checkHibernation(followings: MiFollowing[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkHibernation
はどこにも使われてなさそうです
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extractMentionedUsers
と isQuote
は NoteCreateService
のそれと同じなので NoteEntityService
とかにまとめたほうが適切そうな気もする?
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, | ||
} : undefined, | ||
}, undefined, me); | ||
this.globalEventService.publishNoteStream(note.id, 'updated', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AP のアクティビティとして届いたときにもストリームに流してほしいので edit
の方に書いたほうが自然そう?
// Pack the note | ||
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); | ||
|
||
this.globalEventService.publishNotesStream(noteObj); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
編集時にタイムラインに「新しいノートがあります」が表示されてしまいますし noteUpdated
を介して届いているのでこれは不要そうです
ドライブのファイルがアップデートされたときに紐づくノートに対してもUpdateアクティビティを出してあげると良さそうです(センシティブフラグが変更された際などに再登録する系のユースケース) 受け取り側はメディアしか変更が入っていない場合はドライブファイルについてのみ更新をかけるなどの運用で… |
What
ActivityPub経由の投稿の編集に対応しました。
Why
Mastodonなどからの投稿の編集が反映されないため
Resolve: #8364
Related: #11944
Additional info (optional)
Checklist