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 50 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 63.83789% with 1044 lines in your changes missing coverage. Please review.

Project coverage is 42.38%. Comparing base (8959ff8) to head (6341656).
Report is 2 commits behind head on develop.

Files Patch % Lines
packages/backend/src/core/NoteEditService.ts 30.18% 370 Missing ⚠️
...kend/src/core/entities/NoteHistoryEntityService.ts 27.20% 198 Missing ⚠️
...ckend/src/core/activitypub/models/ApNoteService.ts 4.61% 186 Missing ⚠️
packages/frontend/src/components/MkNoteHistory.vue 0.00% 144 Missing and 1 partial ⚠️
...s/backend/src/server/api/endpoints/notes/update.ts 67.60% 46 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/os.ts 13.63% 19 Missing ⚠️
packages/frontend/src/scripts/get-note-menu.ts 0.00% 9 Missing ⚠️
packages/backend/src/models/NoteHistory.ts 93.82% 5 Missing ⚠️
... and 4 more
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.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

github-actions bot commented Jun 15, 2024

この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",

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

テスト以外のおおよその実装は完了しました

@GrapeApple0 GrapeApple0 marked this pull request as ready for review July 23, 2024 11:24
@GrapeApple0 GrapeApple0 changed the title WIP: feat: 投稿の編集に対応 feat: 投稿の編集に対応 Jul 24, 2024
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.

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

<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>
Copy link
Contributor

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,
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 になるので、そういうノートを編集しようとしたときに文字入力を要求されるのは不自然そうです。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>
Copy link
Contributor

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;
Copy link
Contributor

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-jsstreaming.types.ts に型を追加する必要がありそうです

}

@bindThis
public async checkHibernation(followings: MiFollowing[]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

checkHibernation はどこにも使われてなさそうです

Copy link
Contributor

Choose a reason for hiding this comment

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

extractMentionedUsersisQuoteNoteCreateService のそれと同じなので NoteEntityService とかにまとめたほうが適切そうな気もする?

expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
}, undefined, me);
this.globalEventService.publishNoteStream(note.id, 'updated', {
Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

編集時にタイムラインに「新しいノートがあります」が表示されてしまいますし noteUpdated を介して届いているのでこれは不要そうです

@kakkokari-gtyih
Copy link
Contributor

kakkokari-gtyih commented Aug 7, 2024

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

受け取り側はメディアしか変更が入っていない場合はドライブファイルについてのみ更新をかけるなどの運用で…

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