Compare commits
1 commit
master
...
pr-webhook
Author | SHA1 | Date | |
---|---|---|---|
91aee4392d |
6 changed files with 171 additions and 17 deletions
|
@ -35,6 +35,7 @@ import type {
|
||||||
} from './QueueModule.js';
|
} from './QueueModule.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -471,7 +472,7 @@ export class QueueService {
|
||||||
public userWebhookDeliver(
|
public userWebhookDeliver(
|
||||||
webhook: MiWebhook,
|
webhook: MiWebhook,
|
||||||
type: typeof webhookEventTypes[number],
|
type: typeof webhookEventTypes[number],
|
||||||
content: unknown,
|
content: Packed<'UserWebhookBody'>,
|
||||||
opts?: { attempts?: number },
|
opts?: { attempts?: number },
|
||||||
) {
|
) {
|
||||||
const data: UserWebhookDeliverJobData = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
|
|
|
@ -30,6 +30,9 @@ import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||||
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.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';
|
const FALLBACK = '\u2764';
|
||||||
|
|
||||||
|
@ -94,6 +97,8 @@ export class ReactionService {
|
||||||
private reactionsBufferingService: ReactionsBufferingService,
|
private reactionsBufferingService: ReactionsBufferingService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private featuredService: FeaturedService,
|
private featuredService: FeaturedService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
private webhookService: UserWebhookService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
|
@ -254,12 +259,34 @@ export class ReactionService {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
// リアクションされたユーザーがローカルユーザーなら通知を作成してWebhookを送信
|
||||||
if (note.userHost === null) {
|
if (note.userHost === null) {
|
||||||
this.notificationService.createNotification(note.userId, 'reaction', {
|
this.notificationService.createNotification(note.userId, 'reaction', {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
}, user.id);
|
}, 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 配信
|
//#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 dummyUser1 = generateDummyUser();
|
||||||
const dummyUser2 = generateDummyUser({
|
const dummyUser2 = generateDummyUser({
|
||||||
id: 'dummy-user-2',
|
id: 'dummy-user-2',
|
||||||
|
@ -285,6 +299,10 @@ const dummyUser3 = generateDummyUser({
|
||||||
notesCount: 15900,
|
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()
|
@Injectable()
|
||||||
export class WebhookTestService {
|
export class WebhookTestService {
|
||||||
public static NoSuchWebhookError = class extends Error {
|
public static NoSuchWebhookError = class extends Error {
|
||||||
|
@ -321,7 +339,11 @@ export class WebhookTestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhook = webhooks[0];
|
const webhook = webhooks[0];
|
||||||
const send = (contents: unknown) => {
|
const send = (contents:
|
||||||
|
Packed<'UserWebhookNoteBody'> |
|
||||||
|
Packed<'UserWebhookUserBody'> |
|
||||||
|
Packed<'UserWebhookReactionBody'>,
|
||||||
|
) => {
|
||||||
const merged = {
|
const merged = {
|
||||||
...webhook,
|
...webhook,
|
||||||
...params.override,
|
...params.override,
|
||||||
|
@ -361,33 +383,42 @@ export class WebhookTestService {
|
||||||
|
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
case 'note': {
|
case 'note': {
|
||||||
send(toPackedNote(dummyNote1));
|
send(wrapBodyEnum('note', toPackedNote(dummyNote1)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'reply': {
|
case 'reply': {
|
||||||
send(toPackedNote(dummyReply1));
|
send(wrapBodyEnum('note', toPackedNote(dummyReply1)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'renote': {
|
case 'renote': {
|
||||||
send(toPackedNote(dummyRenote1));
|
send(wrapBodyEnum('note', toPackedNote(dummyRenote1)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mention': {
|
case 'mention': {
|
||||||
send(toPackedNote(dummyMention1));
|
send(wrapBodyEnum('note', toPackedNote(dummyMention1)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'follow': {
|
case 'follow': {
|
||||||
send(toPackedUserDetailedNotMe(dummyUser1));
|
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'followed': {
|
case 'followed': {
|
||||||
send(toPackedUserLite(dummyUser2));
|
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'unfollow': {
|
case 'unfollow': {
|
||||||
send(toPackedUserDetailedNotMe(dummyUser3));
|
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2)));
|
||||||
break;
|
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';
|
} from '@/models/json-schema/meta.js';
|
||||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.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 = {
|
export const refs = {
|
||||||
UserLite: packedUserLiteSchema,
|
UserLite: packedUserLiteSchema,
|
||||||
|
@ -68,6 +69,10 @@ export const refs = {
|
||||||
MeDetailed: packedMeDetailedSchema,
|
MeDetailed: packedMeDetailedSchema,
|
||||||
UserDetailed: packedUserDetailedSchema,
|
UserDetailed: packedUserDetailedSchema,
|
||||||
User: packedUserSchema,
|
User: packedUserSchema,
|
||||||
|
UserWebhookBody: packedUserWebhookBodySchema,
|
||||||
|
UserWebhookNoteBody: packedUserWebhookNoteBodySchema,
|
||||||
|
UserWebhookUserBody: packedUserWebhookUserBodySchema,
|
||||||
|
UserWebhookReactionBody: packedUserWebhookReactionBodySchema,
|
||||||
|
|
||||||
UserList: packedUserListSchema,
|
UserList: packedUserListSchema,
|
||||||
Ad: packedAdSchema,
|
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 { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
describe('WebhookTestService', () => {
|
describe('WebhookTestService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
|
@ -122,7 +123,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('note');
|
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 () => {
|
test('reply', async () => {
|
||||||
|
@ -131,7 +132,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('reply');
|
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 () => {
|
test('renote', async () => {
|
||||||
|
@ -140,7 +141,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('renote');
|
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 () => {
|
test('mention', async () => {
|
||||||
|
@ -149,7 +150,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('mention');
|
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 () => {
|
test('follow', async () => {
|
||||||
|
@ -158,7 +159,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('follow');
|
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 () => {
|
test('followed', async () => {
|
||||||
|
@ -167,7 +168,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('followed');
|
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 () => {
|
test('unfollow', async () => {
|
||||||
|
@ -176,7 +177,16 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('unfollow');
|
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', () => {
|
describe('NoSuchWebhookError', () => {
|
||||||
|
|
Loading…
Reference in a new issue