fix(backend): Webhook Test一致性+リアクションhook実装
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
e1ebf46071
commit
91aee4392d
6 changed files with 171 additions and 17 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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 配信
|
||||
|
|
|
@ -259,6 +259,20 @@ function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailed
|
|||
};
|
||||
}
|
||||
|
||||
function generateDummyReactionPayload(note_override?: Partial<MiNote>): 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<T extends string, U>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
80
packages/backend/src/models/json-schema/user-webhook.ts
Normal file
80
packages/backend/src/models/json-schema/user-webhook.ts
Normal file
|
@ -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;
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in a new issue