From c54b6bf55d94ac74ee59631003d8b78a8993bbbb Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 12 Feb 2025 15:11:19 -0500 Subject: [PATCH] append mandatory CW in `Update(Note)` activities --- packages/backend/src/core/NoteEditService.ts | 24 +--- .../src/core/activitypub/ApRendererService.ts | 15 ++- packages/backend/test/unit/activitypub.ts | 111 +++++++++++++----- 3 files changed, 97 insertions(+), 53 deletions(-) diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 18912181d7..854112ec1d 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -224,13 +224,7 @@ export class NoteEditService implements OnApplicationShutdown { } @bindThis - public async edit(user: { - id: MiUser['id']; - username: MiUser['username']; - host: MiUser['host']; - isBot: MiUser['isBot']; - noindex: MiUser['noindex']; - }, editid: MiNote['id'], data: Option, silent = false): Promise { + public async edit(user: MiUser, editid: MiNote['id'], data: Option, silent = false): Promise { if (!editid) { throw new Error('fail'); } @@ -584,13 +578,7 @@ export class NoteEditService implements OnApplicationShutdown { } @bindThis - private async postNoteEdited(note: MiNote, oldNote: MiNote, user: { - id: MiUser['id']; - username: MiUser['username']; - host: MiUser['host']; - isBot: MiUser['isBot']; - noindex: MiUser['noindex']; - }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { + private async postNoteEdited(note: MiNote, oldNote: MiNote, user: MiUser, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { // Register host if (this.meta.enableStatsForFederatedInstances) { if (this.userEntityService.isRemoteUser(user)) { @@ -703,7 +691,7 @@ export class NoteEditService implements OnApplicationShutdown { //#region AP deliver if (!data.localOnly && this.userEntityService.isLocalUser(user)) { (async () => { - const noteActivity = await this.renderNoteOrRenoteActivity(data, note); + const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); // メンションされたリモートユーザーに配送 @@ -834,14 +822,12 @@ export class NoteEditService implements OnApplicationShutdown { } @bindThis - private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { + private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) { if (data.localOnly) return null; - const user = await this.usersRepository.findOneBy({ id: note.userId }); - if (user == null) throw new Error('user not found'); const content = this.isRenote(data) && !this.isQuote(data) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) - : this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, false), user); + : this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, user, false), user); return this.apRendererService.addContext(content); } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 22cb6b8282..cb9b74f6d7 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -642,7 +642,7 @@ export class ApRendererService { } @bindThis - public async renderUpNote(note: MiNote, dive = true): Promise { + public async renderUpNote(note: MiNote, author: MiUser, dive = true): Promise { const getPromisedFiles = async (ids: string[]): Promise => { if (ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); @@ -656,14 +656,14 @@ export class ApRendererService { inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); if (inReplyToNote != null) { - const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } }); + const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); - if (inReplyToUserExist) { + if (inReplyToUser) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { if (dive) { - inReplyTo = await this.renderUpNote(inReplyToNote, false); + inReplyTo = await this.renderUpNote(inReplyToNote, inReplyToUser, false); } else { inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`; } @@ -726,7 +726,12 @@ export class ApRendererService { apAppend += `\n\nRE: ${quote}`; } - const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; + let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; + + // Apply mandatory CW, if applicable + if (author.mandatoryCW) { + summary = appendContentWarning(summary, author.mandatoryCW); + } const { content } = this.apMfmService.getNoteHtml(note, apAppend); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 105a3292bf..b4cffbc706 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -483,38 +483,38 @@ describe('ActivityPub', () => { }); describe(ApRendererService, () => { - describe('renderNote', () => { - let note: MiNote; - let author: MiUser; + let note: MiNote; + let author: MiUser; - beforeEach(() => { - author = new MiUser({ - id: idService.gen(), - }); - note = new MiNote({ - id: idService.gen(), - userId: author.id, - visibility: 'public', - localOnly: false, - text: 'Note text', - cw: null, - renoteCount: 0, - repliesCount: 0, - clippedCount: 0, - reactions: {}, - fileIds: [], - attachedFileTypes: [], - visibleUserIds: [], - mentions: [], - // This is fucked tbh - it's JSON stored in a TEXT column that gets parsed/serialized all over the place - mentionedRemoteUsers: '[]', - reactionAndUserPairCache: [], - emojis: [], - tags: [], - hasPoll: false, - }); + beforeEach(() => { + author = new MiUser({ + id: idService.gen(), }); + note = new MiNote({ + id: idService.gen(), + userId: author.id, + visibility: 'public', + localOnly: false, + text: 'Note text', + cw: null, + renoteCount: 0, + repliesCount: 0, + clippedCount: 0, + reactions: {}, + fileIds: [], + attachedFileTypes: [], + visibleUserIds: [], + mentions: [], + // This is fucked tbh - it's JSON stored in a TEXT column that gets parsed/serialized all over the place + mentionedRemoteUsers: '[]', + reactionAndUserPairCache: [], + emojis: [], + tags: [], + hasPoll: false, + }); + }); + describe('renderNote', () => { describe('summary', () => { // I actually don't know why it does this, but the logic was already there so I've preserved it. it('should be special character when CW is empty string', async () => { @@ -566,5 +566,58 @@ describe('ActivityPub', () => { }); }); }); + + describe('renderUpnote', () => { + describe('summary', () => { + // I actually don't know why it does this, but the logic was already there so I've preserved it. + it('should be special character when CW is empty string', async () => { + note.cw = ''; + + const result = await rendererService.renderUpNote(note, author, false); + + expect(result.summary).toBe(String.fromCharCode(0x200B)); + }); + + it('should be undefined when CW is null', async () => { + const result = await rendererService.renderUpNote(note, author, false); + + expect(result.summary).toBeUndefined(); + }); + + it('should be CW when present without mandatoryCW', async () => { + note.cw = 'original'; + + const result = await rendererService.renderUpNote(note, author, false); + + expect(result.summary).toBe('original'); + }); + + it('should be mandatoryCW when present without CW', async () => { + author.mandatoryCW = 'mandatory'; + + const result = await rendererService.renderUpNote(note, author, false); + + expect(result.summary).toBe('mandatory'); + }); + + it('should be merged when CW and mandatoryCW are both present', async () => { + note.cw = 'original'; + author.mandatoryCW = 'mandatory'; + + const result = await rendererService.renderUpNote(note, author, false); + + expect(result.summary).toBe('original, mandatory'); + }); + + it('should be CW when CW includes mandatoryCW', async () => { + note.cw = 'original and mandatory'; + author.mandatoryCW = 'mandatory'; + + const result = await rendererService.renderUpNote(note, author, false); + + expect(result.summary).toBe('original and mandatory'); + }); + }); + }); }); });