From 4bc97c75c7c249361dde9d8223f8059baff914d5 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Mon, 28 Oct 2024 18:37:44 -0500 Subject: [PATCH] =?UTF-8?q?fix(backend):=20Webhook=20Test=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7+=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3hook=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: eternal-flame-AD --- packages/backend/src/core/QueueService.ts | 3 +- packages/backend/src/core/ReactionService.ts | 29 ++++++- .../backend/src/core/WebhookTestService.ts | 47 +++++++++-- packages/backend/src/misc/json-schema.ts | 5 ++ .../src/models/json-schema/user-webhook.ts | 80 +++++++++++++++++++ .../backend/test/unit/WebhookTestService.ts | 24 ++++-- 6 files changed, 171 insertions(+), 17 deletions(-) create mode 100644 packages/backend/src/models/json-schema/user-webhook.ts diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 37028026cc43..bbdd86b7d07b 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -35,6 +35,7 @@ import type { } from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; +import type { Packed } from '@/misc/json-schema.js'; @Injectable() export class QueueService { @@ -471,7 +472,7 @@ export class QueueService { public userWebhookDeliver( webhook: MiWebhook, type: typeof webhookEventTypes[number], - content: unknown, + content: Packed<'UserWebhookBody'>, opts?: { attempts?: number }, ) { const data: UserWebhookDeliverJobData = { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 6f9fe53937bc..0292f7bbb99e 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -30,6 +30,9 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; +import { UserWebhookService } from './UserWebhookService.js'; +import { QueueService } from './QueueService.js'; +import { Packed } from '@/misc/json-schema.js'; const FALLBACK = '\u2764'; @@ -94,6 +97,8 @@ export class ReactionService { private reactionsBufferingService: ReactionsBufferingService, private idService: IdService, private featuredService: FeaturedService, + private queueService: QueueService, + private webhookService: UserWebhookService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, @@ -254,12 +259,34 @@ export class ReactionService { userId: user.id, }); - // リアクションされたユーザーがローカルユーザーなら通知を作成 + // リアクションされたユーザーがローカルユーザーなら通知を作成してWebhookを送信 if (note.userHost === null) { this.notificationService.createNotification(note.userId, 'reaction', { noteId: note.id, reaction: reaction, }, user.id); + + + this.webhookService.getActiveWebhooks().then(async webhooks => { + webhooks = webhooks.filter(x => x.userId === note.userId && x.on.includes('reaction')); + if (webhooks.length === 0) return; + + const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); + const userObj = await this.userEntityService.pack(user.id, null, { schema: 'UserLite' }); + + const payload: Packed<'UserWebhookReactionBody'> = { + note: noteObj, + reaction: { + id: record.id, + user: userObj, + reaction: reaction, + }, + }; + + for (const webhook of webhooks) { + this.queueService.userWebhookDeliver(webhook, 'reaction', payload); + } + }) } //#region 配信 diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index c826a28963ae..5aaea06423da 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -259,6 +259,20 @@ function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailed }; } +function generateDummyReactionPayload(note_override?: Partial): Packed<'UserWebhookReactionBody'> { + const dummyNote = generateDummyNote(note_override); + const dummyReaction = { + id: 'dummy-reaction-1', + user: toPackedUserLite(generateDummyUser()), + reaction: 'test_reaction', + }; + + return { + note: toPackedNote(dummyNote), + reaction: dummyReaction, + }; +} + const dummyUser1 = generateDummyUser(); const dummyUser2 = generateDummyUser({ id: 'dummy-user-2', @@ -285,6 +299,10 @@ const dummyUser3 = generateDummyUser({ notesCount: 15900, }); +function wrapBodyEnum(tag: T, body: U): { [K in T]: U } { + return { [tag]: body } as { [K in T]: U }; +} + @Injectable() export class WebhookTestService { public static NoSuchWebhookError = class extends Error { @@ -321,7 +339,11 @@ export class WebhookTestService { } const webhook = webhooks[0]; - const send = (contents: unknown) => { + const send = (contents: + Packed<'UserWebhookNoteBody'> | + Packed<'UserWebhookUserBody'> | + Packed<'UserWebhookReactionBody'>, + ) => { const merged = { ...webhook, ...params.override, @@ -361,33 +383,42 @@ export class WebhookTestService { switch (params.type) { case 'note': { - send(toPackedNote(dummyNote1)); + send(wrapBodyEnum('note', toPackedNote(dummyNote1))); break; } case 'reply': { - send(toPackedNote(dummyReply1)); + send(wrapBodyEnum('note', toPackedNote(dummyReply1))); break; } case 'renote': { - send(toPackedNote(dummyRenote1)); + send(wrapBodyEnum('note', toPackedNote(dummyRenote1))); break; } case 'mention': { - send(toPackedNote(dummyMention1)); + send(wrapBodyEnum('note', toPackedNote(dummyMention1))); break; } case 'follow': { - send(toPackedUserDetailedNotMe(dummyUser1)); + send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2))); break; } case 'followed': { - send(toPackedUserLite(dummyUser2)); + send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2))); break; } case 'unfollow': { - send(toPackedUserDetailedNotMe(dummyUser3)); + send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2))); break; } + case 'reaction': { + send(generateDummyReactionPayload()); + break; + } + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _exhaustiveAssertion: never = params.type; + return; + } } } diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 040e36228ccf..b123879f8d01 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -59,6 +59,7 @@ import { } from '@/models/json-schema/meta.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; +import { packedUserWebhookBodySchema, packedUserWebhookNoteBodySchema, packedUserWebhookReactionBodySchema, packedUserWebhookUserBodySchema } from '@/models/json-schema/user-webhook.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -68,6 +69,10 @@ export const refs = { MeDetailed: packedMeDetailedSchema, UserDetailed: packedUserDetailedSchema, User: packedUserSchema, + UserWebhookBody: packedUserWebhookBodySchema, + UserWebhookNoteBody: packedUserWebhookNoteBodySchema, + UserWebhookUserBody: packedUserWebhookUserBodySchema, + UserWebhookReactionBody: packedUserWebhookReactionBodySchema, UserList: packedUserListSchema, Ad: packedAdSchema, diff --git a/packages/backend/src/models/json-schema/user-webhook.ts b/packages/backend/src/models/json-schema/user-webhook.ts new file mode 100644 index 000000000000..987118122518 --- /dev/null +++ b/packages/backend/src/models/json-schema/user-webhook.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedUserWebhookNoteBodySchema = { + type: 'object', + properties: { + note: { + type: 'object', + ref: 'Note', + optional: false, + nullable: false, + }, + }, + nullable: false, + optional: false, +} as const; + +export const packedUserWebhookUserBodySchema = { + type: 'object', + properties: { + user: { + type: 'object', + ref: 'UserDetailedNotMe', + optional: false, + nullable: false, + }, + }, + nullable: false, + optional: false, +} as const; + +export const packedUserWebhookReactionBodySchema = { + type: 'object', + properties: { + note: { + type: 'object', + ref: 'Note', + optional: false, + nullable: false, + }, + reaction: { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, + nullable: false, + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, + nullable: false, + }, + reaction: { + type: 'string', + optional: false, + nullable: false, + }, + }, + optional: false, + nullable: false, + }, + }, + nullable: false, + optional: false, +} as const; + +export const packedUserWebhookBodySchema = { + type: 'object', + oneOf: [ + packedUserWebhookNoteBodySchema, + packedUserWebhookUserBodySchema, + packedUserWebhookReactionBodySchema, + ], + nullable: false, + optional: false, +} as const; diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts index 5e63b86f8fb3..caffe67ec74e 100644 --- a/packages/backend/test/unit/WebhookTestService.ts +++ b/packages/backend/test/unit/WebhookTestService.ts @@ -14,6 +14,7 @@ import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersReposi import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; +import { Packed } from '@/misc/json-schema.js'; describe('WebhookTestService', () => { let app: TestingModule; @@ -122,7 +123,7 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('note'); - expect((calls[2] as any).id).toBe('dummy-note-1'); + expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-note-1'); }); test('reply', async () => { @@ -131,7 +132,7 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('reply'); - expect((calls[2] as any).id).toBe('dummy-reply-1'); + expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-reply-1'); }); test('renote', async () => { @@ -140,7 +141,7 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('renote'); - expect((calls[2] as any).id).toBe('dummy-renote-1'); + expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-renote-1'); }); test('mention', async () => { @@ -149,7 +150,7 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('mention'); - expect((calls[2] as any).id).toBe('dummy-mention-1'); + expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-mention-1'); }); test('follow', async () => { @@ -158,7 +159,7 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('follow'); - expect((calls[2] as any).id).toBe('dummy-user-1'); + expect((calls[2] as Packed<'UserWebhookUserBody'>).user.id).toBe('dummy-user-2'); }); test('followed', async () => { @@ -167,7 +168,7 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('followed'); - expect((calls[2] as any).id).toBe('dummy-user-2'); + expect((calls[2] as Packed<'UserWebhookUserBody'>).user.id).toBe('dummy-user-2'); }); test('unfollow', async () => { @@ -176,7 +177,16 @@ describe('WebhookTestService', () => { const calls = queueService.userWebhookDeliver.mock.calls[0]; expect((calls[0] as any).id).toBe('dummy-webhook'); expect(calls[1]).toBe('unfollow'); - expect((calls[2] as any).id).toBe('dummy-user-3'); + expect((calls[2] as Packed<'UserWebhookUserBody'>).user.id).toBe('dummy-user-2'); + }); + + test('reaction', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reaction' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('reaction'); + expect((calls[2] as Packed<'UserWebhookReactionBody'>).reaction.id).toBe('dummy-reaction-1'); }); describe('NoSuchWebhookError', () => {