Merge branch 'pr-webhook-payload'

This commit is contained in:
ゆめ 2024-11-01 02:38:00 -05:00
commit 6e7aee287a
No known key found for this signature in database
9 changed files with 216 additions and 17 deletions

View file

@ -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 = {

View file

@ -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,33 @@ 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 配信

View file

@ -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(dummyUser1)));
break;
}
case 'followed': {
send(toPackedUserLite(dummyUser2));
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2)));
break;
}
case 'unfollow': {
send(toPackedUserDetailedNotMe(dummyUser3));
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser3)));
break;
}
case 'reaction': {
send(generateDummyReactionPayload());
break;
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
}
}

View file

@ -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,

View 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: 'UserLite',
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;

View file

@ -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-1');
});
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-3');
});
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', () => {

View file

@ -1810,6 +1810,10 @@ declare namespace entities {
MeDetailed,
UserDetailed,
User,
UserWebhookBody,
UserWebhookNoteBody,
UserWebhookUserBody,
UserWebhookReactionBody,
UserList,
Ad,
Announcement,
@ -3402,6 +3406,18 @@ type UsersShowResponse = operations['users___show']['responses']['200']['content
// @public (undocumented)
type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
// @public (undocumented)
type UserWebhookBody = components['schemas']['UserWebhookBody'];
// @public (undocumented)
type UserWebhookNoteBody = components['schemas']['UserWebhookNoteBody'];
// @public (undocumented)
type UserWebhookReactionBody = components['schemas']['UserWebhookReactionBody'];
// @public (undocumented)
type UserWebhookUserBody = components['schemas']['UserWebhookUserBody'];
// Warnings were encountered during analysis:
//
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts

View file

@ -7,6 +7,10 @@ export type UserDetailedNotMe = components['schemas']['UserDetailedNotMe'];
export type MeDetailed = components['schemas']['MeDetailed'];
export type UserDetailed = components['schemas']['UserDetailed'];
export type User = components['schemas']['User'];
export type UserWebhookBody = components['schemas']['UserWebhookBody'];
export type UserWebhookNoteBody = components['schemas']['UserWebhookNoteBody'];
export type UserWebhookUserBody = components['schemas']['UserWebhookUserBody'];
export type UserWebhookReactionBody = components['schemas']['UserWebhookReactionBody'];
export type UserList = components['schemas']['UserList'];
export type Ad = components['schemas']['Ad'];
export type Announcement = components['schemas']['Announcement'];

View file

@ -4013,6 +4013,32 @@ export type components = {
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'];
UserWebhookBody: OneOf<[{
note: components['schemas']['Note'];
}, {
user: components['schemas']['UserLite'];
}, {
note: components['schemas']['Note'];
reaction: {
id: string;
user: components['schemas']['UserLite'];
reaction: string;
};
}]>;
UserWebhookNoteBody: {
note: components['schemas']['Note'];
};
UserWebhookUserBody: {
user: components['schemas']['UserLite'];
};
UserWebhookReactionBody: {
note: components['schemas']['Note'];
reaction: {
id: string;
user: components['schemas']['UserLite'];
reaction: string;
};
};
UserList: {
/**
* Format: id