diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index 9a24b8ae5..3f7aecaf3 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -68,6 +68,15 @@ export class NoteUpdateService { * @param ps New note info */ async update(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, ps: Pick, quiet = false, updater?: MiUser) { + if (!ps.updatedAt) { + throw new Error('update time is required'); + } + + if (note.history?.findIndex(h => h.createdAt === ps.updatedAt?.toISOString()) !== -1) { + // Same history already exists, skip this + return; + } + const newNote = { ...note, ...ps, // Overwrite updated fields @@ -90,18 +99,33 @@ export class NoteUpdateService { } } - this.searchService.indexNote(newNote); + // Check if is latest or previous version + const history = [...(note.history || []), { + createdAt: (note.updatedAt || this.idService.parse(note.id).date).toISOString(), + cw: note.cw, + text: note.text, + }]; + if (note.updatedAt && note.updatedAt >= ps.updatedAt) { + // Previous version, just update history + history.sort((h1, h2) => new Date(h1.createdAt).getTime() - new Date(h2.createdAt).getTime()); // earliest -> latest - await this.notesRepository.update({ id: note.id }, { - updatedAt: ps.updatedAt, - history: [...(note.history || []), { - createdAt: (note.updatedAt || this.idService.parse(note.id).date).toISOString(), - cw: note.cw, - text: note.text, - }], - cw: ps.cw, - text: ps.text, - }); + await this.notesRepository.update({ id: note.id }, { + history, + }); + } else { + // Latest version + + // Update index + this.searchService.indexNote(newNote); + + // Update note info + await this.notesRepository.update({ id: note.id }, { + updatedAt: ps.updatedAt, + history, + cw: ps.cw, + text: ps.text, + }); + } // Currently not implemented // if (updater && (note.userId !== updater.id)) { diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 0049b866c..e7c6a5c1b 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -335,17 +335,22 @@ export class ApNoteService { const uri = typeof value === 'string' ? value : value.id; if (uri == null) throw new Error('uri is null'); - // Is from local + // Check if note is from local if (uri.startsWith(`${this.config.url}/`)) throw new Error('uri points local'); const originNote = await this.notesRepository.findOneBy({ uri }); - if (originNote == null) throw new Error('Note is not registered'); + if (originNote === null) throw new Error('note is not registered'); // Process new note const note = value as IPost; + // Check updated timestamp + if (!note.updated) { + throw new Error('note.updated field is required'); + } + // Fetch note author - if (note.attributedTo == null) { + if (!note.attributedTo) { throw new Error('invalid note.attributedTo: ' + note.attributedTo); } @@ -363,12 +368,10 @@ export class ApNoteService { text = this.apMfmService.htmlToMfm(note.content, note.tag); } - const updatedAt = note.updated || new Date(); - await this.noteUpdateService.update(actor, originNote, { cw, text, - updatedAt, + updatedAt: note.updated, }, silent); } diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index 1e6981294..e78fcb6ce 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -70,6 +70,11 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchNote); } + if (note.text === ps.text && note.cw === ps.cw) { + // The same as old note, nothing to do + return; + } + await this.noteUpdateService.update(await this.usersRepository.findOneByOrFail({ id: note.userId }), note, { text: ps.text, cw: ps.cw,