mirror of
https://codeberg.org/yeentown/barkey
synced 2024-11-25 15:15:12 +00:00
feat: UserWebhook/SystemWebhookのテスト送信機能を追加 (#14489)
* feat: UserWebhook/SystemWebhookのテスト送信機能を追加 * fix CHANGELOG.md * 一部設定をパラメータから上書き出来るように修正 * remove async * regenerate autogen
This commit is contained in:
parent
ceb4640669
commit
4ac8aad50a
27 changed files with 1477 additions and 39 deletions
|
@ -1,7 +1,7 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- UserWebhookとSystemWebhookのテスト送信機能を追加 ( #14445 )
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -9477,6 +9477,10 @@ export interface Locale extends ILocale {
|
||||||
* Webhookを削除しますか?
|
* Webhookを削除しますか?
|
||||||
*/
|
*/
|
||||||
"deleteConfirm": string;
|
"deleteConfirm": string;
|
||||||
|
/**
|
||||||
|
* スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
|
||||||
|
*/
|
||||||
|
"testRemarks": string;
|
||||||
};
|
};
|
||||||
"_abuseReport": {
|
"_abuseReport": {
|
||||||
"_notificationRecipient": {
|
"_notificationRecipient": {
|
||||||
|
|
|
@ -2514,6 +2514,7 @@ _webhookSettings:
|
||||||
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
||||||
userCreated: "ユーザーが作成されたとき"
|
userCreated: "ユーザーが作成されたとき"
|
||||||
deleteConfirm: "Webhookを削除しますか?"
|
deleteConfirm: "Webhookを削除しますか?"
|
||||||
|
testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
|
||||||
|
|
||||||
_abuseReport:
|
_abuseReport:
|
||||||
_notificationRecipient:
|
_notificationRecipient:
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
|
@ -211,6 +212,7 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||||
|
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||||
|
@ -359,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
WebhookTestService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -503,6 +506,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
$WebhookTestService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
@ -648,6 +652,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
WebhookTestService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -791,6 +796,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
$WebhookTestService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
|
|
@ -452,10 +452,15 @@ export class QueueService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see UserWebhookDeliverJobData
|
* @see UserWebhookDeliverJobData
|
||||||
* @see WebhookDeliverProcessorService
|
* @see UserWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
public userWebhookDeliver(
|
||||||
|
webhook: MiWebhook,
|
||||||
|
type: typeof webhookEventTypes[number],
|
||||||
|
content: unknown,
|
||||||
|
opts?: { attempts?: number },
|
||||||
|
) {
|
||||||
const data: UserWebhookDeliverJobData = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
@ -468,7 +473,7 @@ export class QueueService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: opts?.attempts ?? 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
|
@ -479,10 +484,15 @@ export class QueueService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see SystemWebhookDeliverJobData
|
* @see SystemWebhookDeliverJobData
|
||||||
* @see WebhookDeliverProcessorService
|
* @see SystemWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
public systemWebhookDeliver(
|
||||||
|
webhook: MiSystemWebhook,
|
||||||
|
type: SystemWebhookEventType,
|
||||||
|
content: unknown,
|
||||||
|
opts?: { attempts?: number },
|
||||||
|
) {
|
||||||
const data: SystemWebhookDeliverJobData = {
|
const data: SystemWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
@ -494,7 +504,7 @@ export class QueueService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: opts?.attempts ?? 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
* SystemWebhook の一覧を取得する.
|
* SystemWebhook の一覧を取得する.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchSystemWebhooks(params?: {
|
public fetchSystemWebhooks(params?: {
|
||||||
ids?: MiSystemWebhook['id'][];
|
ids?: MiSystemWebhook['id'][];
|
||||||
isActive?: MiSystemWebhook['isActive'];
|
isActive?: MiSystemWebhook['isActive'];
|
||||||
on?: MiSystemWebhook['on'];
|
on?: MiSystemWebhook['on'];
|
||||||
|
@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
/**
|
/**
|
||||||
* SystemWebhook をWebhook配送キューに追加する
|
* SystemWebhook をWebhook配送キューに追加する
|
||||||
* @see QueueService.systemWebhookDeliver
|
* @see QueueService.systemWebhookDeliver
|
||||||
|
* // TODO: contentの型を厳格化する
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
|
||||||
|
webhook: MiSystemWebhook | MiSystemWebhook['id'],
|
||||||
|
type: T,
|
||||||
|
content: unknown,
|
||||||
|
) {
|
||||||
const webhookEntity = typeof webhook === 'string'
|
const webhookEntity = typeof webhook === 'string'
|
||||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||||
: webhook;
|
: webhook;
|
||||||
if (!webhookEntity || !webhookEntity.isActive) {
|
if (!webhookEntity || !webhookEntity.isActive) {
|
||||||
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!webhookEntity.on.includes(type)) {
|
if (!webhookEntity.on.includes(type)) {
|
||||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
import { type WebhooksRepository } from '@/models/_.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
|
||||||
return this.activeWebhooks;
|
return this.activeWebhooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserWebhook の一覧を取得する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public fetchWebhooks(params?: {
|
||||||
|
ids?: MiWebhook['id'][];
|
||||||
|
isActive?: MiWebhook['active'];
|
||||||
|
on?: MiWebhook['on'];
|
||||||
|
}): Promise<MiWebhook[]> {
|
||||||
|
const query = this.webhooksRepository.createQueryBuilder('webhook');
|
||||||
|
if (params) {
|
||||||
|
if (params.ids && params.ids.length > 0) {
|
||||||
|
query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
|
||||||
|
}
|
||||||
|
if (params.isActive !== undefined) {
|
||||||
|
query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
|
||||||
|
}
|
||||||
|
if (params.on && params.on.length > 0) {
|
||||||
|
query.andWhere(':on <@ webhook.on', { on: params.on });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
434
packages/backend/src/core/WebhookTestService.ts
Normal file
434
packages/backend/src/core/WebhookTestService.ts
Normal file
|
@ -0,0 +1,434 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
|
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
|
||||||
|
return {
|
||||||
|
id: 'dummy-abuse-report1',
|
||||||
|
targetUserId: 'dummy-target-user',
|
||||||
|
targetUser: null,
|
||||||
|
reporterId: 'dummy-reporter-user',
|
||||||
|
reporter: null,
|
||||||
|
assigneeId: null,
|
||||||
|
assignee: null,
|
||||||
|
resolved: false,
|
||||||
|
forwarded: false,
|
||||||
|
comment: 'This is a dummy report for testing purposes.',
|
||||||
|
targetUserHost: null,
|
||||||
|
reporterHost: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||||
|
return {
|
||||||
|
id: 'dummy-user-1',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 7),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
|
||||||
|
hideOnlineStatus: false,
|
||||||
|
username: 'dummy1',
|
||||||
|
usernameLower: 'dummy1',
|
||||||
|
name: 'DummyUser1',
|
||||||
|
followersCount: 10,
|
||||||
|
followingCount: 5,
|
||||||
|
movedToUri: null,
|
||||||
|
movedAt: null,
|
||||||
|
alsoKnownAs: null,
|
||||||
|
notesCount: 30,
|
||||||
|
avatarId: null,
|
||||||
|
avatar: null,
|
||||||
|
bannerId: null,
|
||||||
|
banner: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
bannerUrl: null,
|
||||||
|
avatarBlurhash: null,
|
||||||
|
bannerBlurhash: null,
|
||||||
|
avatarDecorations: [],
|
||||||
|
tags: [],
|
||||||
|
isSuspended: false,
|
||||||
|
isLocked: false,
|
||||||
|
isBot: false,
|
||||||
|
isCat: true,
|
||||||
|
isRoot: false,
|
||||||
|
isExplorable: true,
|
||||||
|
isHibernated: false,
|
||||||
|
isDeleted: false,
|
||||||
|
emojis: [],
|
||||||
|
host: null,
|
||||||
|
inbox: null,
|
||||||
|
sharedInbox: null,
|
||||||
|
featured: null,
|
||||||
|
uri: null,
|
||||||
|
followersUri: null,
|
||||||
|
token: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||||
|
return {
|
||||||
|
id: 'dummy-note-1',
|
||||||
|
replyId: null,
|
||||||
|
reply: null,
|
||||||
|
renoteId: null,
|
||||||
|
renote: null,
|
||||||
|
threadId: null,
|
||||||
|
text: 'This is a dummy note for testing purposes.',
|
||||||
|
name: null,
|
||||||
|
cw: null,
|
||||||
|
userId: 'dummy-user-1',
|
||||||
|
user: null,
|
||||||
|
localOnly: true,
|
||||||
|
reactionAcceptance: 'likeOnly',
|
||||||
|
renoteCount: 10,
|
||||||
|
repliesCount: 5,
|
||||||
|
clippedCount: 0,
|
||||||
|
reactions: {},
|
||||||
|
visibility: 'public',
|
||||||
|
uri: null,
|
||||||
|
url: null,
|
||||||
|
fileIds: [],
|
||||||
|
attachedFileTypes: [],
|
||||||
|
visibleUserIds: [],
|
||||||
|
mentions: [],
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
reactionAndUserPairCache: [],
|
||||||
|
emojis: [],
|
||||||
|
tags: [],
|
||||||
|
hasPoll: false,
|
||||||
|
channelId: null,
|
||||||
|
channel: null,
|
||||||
|
userHost: null,
|
||||||
|
replyUserId: null,
|
||||||
|
replyUserHost: null,
|
||||||
|
renoteUserId: null,
|
||||||
|
renoteUserHost: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
deletedAt: null,
|
||||||
|
text: note.text,
|
||||||
|
cw: note.cw,
|
||||||
|
userId: note.userId,
|
||||||
|
user: toPackedUserLite(note.user ?? generateDummyUser()),
|
||||||
|
replyId: note.replyId,
|
||||||
|
renoteId: note.renoteId,
|
||||||
|
isHidden: false,
|
||||||
|
visibility: note.visibility,
|
||||||
|
mentions: note.mentions,
|
||||||
|
visibleUserIds: note.visibleUserIds,
|
||||||
|
fileIds: note.fileIds,
|
||||||
|
files: [],
|
||||||
|
tags: note.tags,
|
||||||
|
poll: null,
|
||||||
|
emojis: note.emojis,
|
||||||
|
channelId: note.channelId,
|
||||||
|
channel: note.channel,
|
||||||
|
localOnly: note.localOnly,
|
||||||
|
reactionAcceptance: note.reactionAcceptance,
|
||||||
|
reactionEmojis: {},
|
||||||
|
reactions: {},
|
||||||
|
reactionCount: 0,
|
||||||
|
renoteCount: note.renoteCount,
|
||||||
|
repliesCount: note.repliesCount,
|
||||||
|
uri: note.uri ?? undefined,
|
||||||
|
url: note.url ?? undefined,
|
||||||
|
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||||
|
...(detail ? {
|
||||||
|
clippedCount: note.clippedCount,
|
||||||
|
reply: note.reply ? toPackedNote(note.reply, false) : null,
|
||||||
|
renote: note.renote ? toPackedNote(note.renote, true) : null,
|
||||||
|
myReaction: null,
|
||||||
|
} : {}),
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
host: user.host,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
avatarBlurhash: user.avatarBlurhash,
|
||||||
|
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||||
|
id: it.id,
|
||||||
|
angle: it.angle,
|
||||||
|
flipH: it.flipH,
|
||||||
|
url: 'https://example.com/dummy-image001.png',
|
||||||
|
offsetX: it.offsetX,
|
||||||
|
offsetY: it.offsetY,
|
||||||
|
})),
|
||||||
|
isBot: user.isBot,
|
||||||
|
isCat: user.isCat,
|
||||||
|
emojis: user.emojis,
|
||||||
|
onlineStatus: 'active',
|
||||||
|
badgeRoles: [],
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
|
||||||
|
return {
|
||||||
|
...toPackedUserLite(user),
|
||||||
|
url: null,
|
||||||
|
uri: null,
|
||||||
|
movedTo: null,
|
||||||
|
alsoKnownAs: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||||
|
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||||
|
bannerUrl: user.bannerUrl,
|
||||||
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
isLocked: user.isLocked,
|
||||||
|
isSilenced: false,
|
||||||
|
isSuspended: user.isSuspended,
|
||||||
|
description: null,
|
||||||
|
location: null,
|
||||||
|
birthday: null,
|
||||||
|
lang: null,
|
||||||
|
fields: [],
|
||||||
|
verifiedLinks: [],
|
||||||
|
followersCount: user.followersCount,
|
||||||
|
followingCount: user.followingCount,
|
||||||
|
notesCount: user.notesCount,
|
||||||
|
pinnedNoteIds: [],
|
||||||
|
pinnedNotes: [],
|
||||||
|
pinnedPageId: null,
|
||||||
|
pinnedPage: null,
|
||||||
|
publicReactions: true,
|
||||||
|
followersVisibility: 'public',
|
||||||
|
followingVisibility: 'public',
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
usePasswordLessLogin: false,
|
||||||
|
securityKeys: false,
|
||||||
|
roles: [],
|
||||||
|
memo: null,
|
||||||
|
moderationNote: undefined,
|
||||||
|
isFollowing: false,
|
||||||
|
isFollowed: false,
|
||||||
|
hasPendingFollowRequestFromYou: false,
|
||||||
|
hasPendingFollowRequestToYou: false,
|
||||||
|
isBlocking: false,
|
||||||
|
isBlocked: false,
|
||||||
|
isMuted: false,
|
||||||
|
isRenoteMuted: false,
|
||||||
|
notify: 'none',
|
||||||
|
withReplies: true,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dummyUser1 = generateDummyUser();
|
||||||
|
const dummyUser2 = generateDummyUser({
|
||||||
|
id: 'dummy-user-2',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 30),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis),
|
||||||
|
username: 'dummy2',
|
||||||
|
usernameLower: 'dummy2',
|
||||||
|
name: 'DummyUser2',
|
||||||
|
followersCount: 40,
|
||||||
|
followingCount: 50,
|
||||||
|
notesCount: 900,
|
||||||
|
});
|
||||||
|
const dummyUser3 = generateDummyUser({
|
||||||
|
id: 'dummy-user-3',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 15),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
|
||||||
|
username: 'dummy3',
|
||||||
|
usernameLower: 'dummy3',
|
||||||
|
name: 'DummyUser3',
|
||||||
|
followersCount: 60,
|
||||||
|
followingCount: 70,
|
||||||
|
notesCount: 15900,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebhookTestService {
|
||||||
|
public static NoSuchWebhookError = class extends Error {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userWebhookService: UserWebhookService,
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserWebhookのテスト送信を行う.
|
||||||
|
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||||
|
*
|
||||||
|
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||||
|
* - Webhookそのものの有効・無効設定(active)
|
||||||
|
* - 送信対象イベント(on)に関する設定
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async testUserWebhook(
|
||||||
|
params: {
|
||||||
|
webhookId: MiWebhook['id'],
|
||||||
|
type: WebhookEventTypes,
|
||||||
|
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||||
|
},
|
||||||
|
sender: MiUser | null,
|
||||||
|
) {
|
||||||
|
const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
|
||||||
|
.then(it => it.filter(it => it.userId === sender?.id));
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new WebhookTestService.NoSuchWebhookError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = webhooks[0];
|
||||||
|
const send = (contents: unknown) => {
|
||||||
|
const merged = {
|
||||||
|
...webhook,
|
||||||
|
...params.override,
|
||||||
|
};
|
||||||
|
|
||||||
|
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
|
// また、Jobの試行回数も1回だけ.
|
||||||
|
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const dummyNote1 = generateDummyNote({
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
});
|
||||||
|
const dummyReply1 = generateDummyNote({
|
||||||
|
id: 'dummy-reply-1',
|
||||||
|
replyId: dummyNote1.id,
|
||||||
|
reply: dummyNote1,
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
});
|
||||||
|
const dummyRenote1 = generateDummyNote({
|
||||||
|
id: 'dummy-renote-1',
|
||||||
|
renoteId: dummyNote1.id,
|
||||||
|
renote: dummyNote1,
|
||||||
|
userId: dummyUser2.id,
|
||||||
|
user: dummyUser2,
|
||||||
|
text: null,
|
||||||
|
});
|
||||||
|
const dummyMention1 = generateDummyNote({
|
||||||
|
id: 'dummy-mention-1',
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
text: `@${dummyUser2.username} This is a mention to you.`,
|
||||||
|
mentions: [dummyUser2.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (params.type) {
|
||||||
|
case 'note': {
|
||||||
|
send(toPackedNote(dummyNote1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reply': {
|
||||||
|
send(toPackedNote(dummyReply1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'renote': {
|
||||||
|
send(toPackedNote(dummyRenote1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mention': {
|
||||||
|
send(toPackedNote(dummyMention1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'follow': {
|
||||||
|
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'followed': {
|
||||||
|
send(toPackedUserLite(dummyUser2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'unfollow': {
|
||||||
|
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhookのテスト送信を行う.
|
||||||
|
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||||
|
*
|
||||||
|
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||||
|
* - Webhookそのものの有効・無効設定(isActive)
|
||||||
|
* - 送信対象イベント(on)に関する設定
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async testSystemWebhook(
|
||||||
|
params: {
|
||||||
|
webhookId: MiSystemWebhook['id'],
|
||||||
|
type: SystemWebhookEventType,
|
||||||
|
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new WebhookTestService.NoSuchWebhookError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = webhooks[0];
|
||||||
|
const send = (contents: unknown) => {
|
||||||
|
const merged = {
|
||||||
|
...webhook,
|
||||||
|
...params.override,
|
||||||
|
};
|
||||||
|
|
||||||
|
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
|
// また、Jobの試行回数も1回だけ.
|
||||||
|
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (params.type) {
|
||||||
|
case 'abuseReport': {
|
||||||
|
send(generateAbuseReport({
|
||||||
|
targetUserId: dummyUser1.id,
|
||||||
|
targetUser: dummyUser1,
|
||||||
|
reporterId: dummyUser2.id,
|
||||||
|
reporter: dummyUser2,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'abuseReportResolved': {
|
||||||
|
send(generateAbuseReport({
|
||||||
|
targetUserId: dummyUser1.id,
|
||||||
|
targetUser: dummyUser1,
|
||||||
|
reporterId: dummyUser2.id,
|
||||||
|
reporter: dummyUser2,
|
||||||
|
assigneeId: dummyUser3.id,
|
||||||
|
assignee: dummyUser3,
|
||||||
|
resolved: true,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userCreated': {
|
||||||
|
send(toPackedUserLite(dummyUser1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||||
|
export type WebhookEventTypes = typeof webhookEventTypes[number];
|
||||||
|
|
||||||
@Entity('webhook')
|
@Entity('webhook')
|
||||||
export class MiWebhook {
|
export class MiWebhook {
|
||||||
|
|
|
@ -92,6 +92,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
|
||||||
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
||||||
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
||||||
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
||||||
|
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
|
@ -258,6 +259,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||||
|
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
|
||||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||||
|
@ -475,6 +477,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
|
||||||
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
|
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
|
||||||
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
|
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
|
||||||
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
|
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
|
||||||
|
const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
|
||||||
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||||
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
|
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
|
||||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
||||||
|
@ -641,6 +644,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
|
||||||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
|
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
|
||||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
|
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
|
||||||
|
const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
|
||||||
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
|
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
|
||||||
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
||||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
|
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
|
||||||
|
@ -862,6 +866,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_systemWebhook_list,
|
$admin_systemWebhook_list,
|
||||||
$admin_systemWebhook_show,
|
$admin_systemWebhook_show,
|
||||||
$admin_systemWebhook_update,
|
$admin_systemWebhook_update,
|
||||||
|
$admin_systemWebhook_test,
|
||||||
$announcements,
|
$announcements,
|
||||||
$announcements_show,
|
$announcements_show,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
|
@ -1028,6 +1033,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
$i_webhooks_update,
|
$i_webhooks_update,
|
||||||
$i_webhooks_delete,
|
$i_webhooks_delete,
|
||||||
|
$i_webhooks_test,
|
||||||
$invite_create,
|
$invite_create,
|
||||||
$invite_delete,
|
$invite_delete,
|
||||||
$invite_list,
|
$invite_list,
|
||||||
|
@ -1243,6 +1249,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_systemWebhook_list,
|
$admin_systemWebhook_list,
|
||||||
$admin_systemWebhook_show,
|
$admin_systemWebhook_show,
|
||||||
$admin_systemWebhook_update,
|
$admin_systemWebhook_update,
|
||||||
|
$admin_systemWebhook_test,
|
||||||
$announcements,
|
$announcements,
|
||||||
$announcements_show,
|
$announcements_show,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
|
@ -1409,6 +1416,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
$i_webhooks_update,
|
$i_webhooks_update,
|
||||||
$i_webhooks_delete,
|
$i_webhooks_delete,
|
||||||
|
$i_webhooks_test,
|
||||||
$invite_create,
|
$invite_create,
|
||||||
$invite_delete,
|
$invite_delete,
|
||||||
$invite_list,
|
$invite_list,
|
||||||
|
|
|
@ -98,6 +98,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
|
||||||
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
||||||
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
||||||
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
||||||
|
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
|
@ -264,6 +265,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||||
|
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
|
||||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||||
|
@ -479,6 +481,7 @@ const eps = [
|
||||||
['admin/system-webhook/list', ep___admin_systemWebhook_list],
|
['admin/system-webhook/list', ep___admin_systemWebhook_list],
|
||||||
['admin/system-webhook/show', ep___admin_systemWebhook_show],
|
['admin/system-webhook/show', ep___admin_systemWebhook_show],
|
||||||
['admin/system-webhook/update', ep___admin_systemWebhook_update],
|
['admin/system-webhook/update', ep___admin_systemWebhook_update],
|
||||||
|
['admin/system-webhook/test', ep___admin_systemWebhook_test],
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['announcements/show', ep___announcements_show],
|
['announcements/show', ep___announcements_show],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
|
@ -645,6 +648,7 @@ const eps = [
|
||||||
['i/webhooks/show', ep___i_webhooks_show],
|
['i/webhooks/show', ep___i_webhooks_show],
|
||||||
['i/webhooks/update', ep___i_webhooks_update],
|
['i/webhooks/update', ep___i_webhooks_update],
|
||||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||||
|
['i/webhooks/test', ep___i_webhooks_test],
|
||||||
['invite/create', ep___invite_create],
|
['invite/create', ep___invite_create],
|
||||||
['invite/delete', ep___invite_delete],
|
['invite/delete', ep___invite_delete],
|
||||||
['invite/list', ep___invite_list],
|
['invite/list', ep___invite_list],
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import ms from 'ms';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['webhooks'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'read:admin:system-webhook',
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('15min'),
|
||||||
|
max: 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchWebhook: {
|
||||||
|
message: 'No such webhook.',
|
||||||
|
code: 'NO_SUCH_WEBHOOK',
|
||||||
|
id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
webhookId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: systemWebhookEventTypes,
|
||||||
|
},
|
||||||
|
override: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string', nullable: false },
|
||||||
|
secret: { type: 'string', nullable: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['webhookId', 'type'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private webhookTestService: WebhookTestService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps) => {
|
||||||
|
try {
|
||||||
|
await this.webhookTestService.testSystemWebhook({
|
||||||
|
webhookId: ps.webhookId,
|
||||||
|
type: ps.type,
|
||||||
|
override: ps.override,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof WebhookTestService.NoSuchWebhookError) {
|
||||||
|
throw new ApiError(meta.errors.noSuchWebhook);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
|
// TODO: UserWebhook schemaの適用
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['webhooks'],
|
tags: ['webhooks'],
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
import type { WebhooksRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
// TODO: UserWebhook schemaの適用
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['webhooks', 'account'],
|
tags: ['webhooks', 'account'],
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
// TODO: UserWebhook schemaの適用
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['webhooks'],
|
tags: ['webhooks'],
|
||||||
|
|
||||||
|
|
76
packages/backend/src/server/api/endpoints/i/webhooks/test.ts
Normal file
76
packages/backend/src/server/api/endpoints/i/webhooks/test.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import ms from 'ms';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { webhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['webhooks'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'read:account',
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('15min'),
|
||||||
|
max: 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchWebhook: {
|
||||||
|
message: 'No such webhook.',
|
||||||
|
code: 'NO_SUCH_WEBHOOK',
|
||||||
|
id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
webhookId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: webhookEventTypes,
|
||||||
|
},
|
||||||
|
override: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string' },
|
||||||
|
secret: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['webhookId', 'type'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private webhookTestService: WebhookTestService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
try {
|
||||||
|
await this.webhookTestService.testUserWebhook({
|
||||||
|
webhookId: ps.webhookId,
|
||||||
|
type: ps.type,
|
||||||
|
override: ps.override,
|
||||||
|
}, me);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof WebhookTestService.NoSuchWebhookError) {
|
||||||
|
throw new ApiError(meta.errors.noSuchWebhook);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -6,6 +7,7 @@
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { randomString } from '../utils.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { randomString } from '../utils.js';
|
|
||||||
|
|
||||||
describe('SystemWebhookService', () => {
|
describe('SystemWebhookService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
|
@ -313,7 +314,7 @@ describe('SystemWebhookService', () => {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
on: ['abuseReport'],
|
on: ['abuseReport'],
|
||||||
});
|
});
|
||||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
|
|
||||||
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -323,7 +324,7 @@ describe('SystemWebhookService', () => {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
on: ['abuseReport'],
|
on: ['abuseReport'],
|
||||||
});
|
});
|
||||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
|
|
||||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -337,8 +338,8 @@ describe('SystemWebhookService', () => {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
on: ['abuseReportResolved'],
|
on: ['abuseReportResolved'],
|
||||||
});
|
});
|
||||||
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
|
|
||||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
245
packages/backend/test/unit/UserWebhookService.ts
Normal file
245
packages/backend/test/unit/UserWebhookService.ts
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { randomString } from '../utils.js';
|
||||||
|
import { MiUser } from '@/models/User.js';
|
||||||
|
import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
|
|
||||||
|
describe('UserWebhookService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let service: UserWebhookService;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userWebhooksRepository: WebhooksRepository;
|
||||||
|
let idService: IdService;
|
||||||
|
let queueService: jest.Mocked<QueueService>;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let root: MiUser;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
return await usersRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWebhook(data: Partial<MiWebhook> = {}) {
|
||||||
|
return userWebhooksRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
name: randomString(),
|
||||||
|
on: ['mention'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
userId: root.id,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function beforeAllImpl() {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
UserWebhookService,
|
||||||
|
IdService,
|
||||||
|
LoggerService,
|
||||||
|
GlobalEventService,
|
||||||
|
{
|
||||||
|
provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
userWebhooksRepository = app.get(DI.webhooksRepository);
|
||||||
|
|
||||||
|
service = app.get(UserWebhookService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function afterAllImpl() {
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function beforeEachImpl() {
|
||||||
|
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function afterEachImpl() {
|
||||||
|
await usersRepository.delete({});
|
||||||
|
await userWebhooksRepository.delete({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('アプリを毎回作り直す必要のないグループ', () => {
|
||||||
|
beforeAll(beforeAllImpl);
|
||||||
|
afterAll(afterAllImpl);
|
||||||
|
beforeEach(beforeEachImpl);
|
||||||
|
afterEach(afterEachImpl);
|
||||||
|
|
||||||
|
describe('fetchSystemWebhooks', () => {
|
||||||
|
test('フィルタなし', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['reply'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchWebhooks();
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('activeのみ', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['reply'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchWebhooks({ isActive: true });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('特定のイベントのみ', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['reply'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('activeな特定のイベントのみ', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['reply'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['reply'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定(他条件とANDになるか見たい)', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: ['mention'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
active: true,
|
||||||
|
on: ['reply'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
active: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
225
packages/backend/test/unit/WebhookTestService.ts
Normal file
225
packages/backend/test/unit/WebhookTestService.ts
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { beforeAll, describe, jest } from '@jest/globals';
|
||||||
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
|
describe('WebhookTestService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let service: WebhookTestService;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
let queueService: jest.Mocked<QueueService>;
|
||||||
|
let userWebhookService: jest.Mocked<UserWebhookService>;
|
||||||
|
let systemWebhookService: jest.Mocked<SystemWebhookService>;
|
||||||
|
let idService: IdService;
|
||||||
|
|
||||||
|
let root: MiUser;
|
||||||
|
let alice: MiUser;
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
const user = await usersRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
await userProfilesRepository.insert({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
WebhookTestService,
|
||||||
|
IdService,
|
||||||
|
{
|
||||||
|
provide: QueueService, useFactory: () => ({
|
||||||
|
systemWebhookDeliver: jest.fn(),
|
||||||
|
userWebhookDeliver: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UserWebhookService, useFactory: () => ({
|
||||||
|
fetchWebhooks: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SystemWebhookService, useFactory: () => ({
|
||||||
|
fetchSystemWebhooks: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||||
|
|
||||||
|
service = app.get(WebhookTestService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||||
|
userWebhookService = app.get(UserWebhookService) as jest.Mocked<UserWebhookService>;
|
||||||
|
systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||||
|
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||||
|
|
||||||
|
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||||
|
{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
|
||||||
|
]));
|
||||||
|
systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([
|
||||||
|
{ id: 'dummy-webhook', isActive: true } as MiSystemWebhook,
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
queueService.systemWebhookDeliver.mockClear();
|
||||||
|
queueService.userWebhookDeliver.mockClear();
|
||||||
|
userWebhookService.fetchWebhooks.mockClear();
|
||||||
|
systemWebhookService.fetchSystemWebhooks.mockClear();
|
||||||
|
|
||||||
|
await usersRepository.delete({});
|
||||||
|
await userProfilesRepository.delete({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('testUserWebhook', () => {
|
||||||
|
test('note', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reply', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renote', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mention', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('follow', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('followed', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unfollow', async () => {
|
||||||
|
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('NoSuchWebhookError', () => {
|
||||||
|
test('user not match', async () => {
|
||||||
|
userWebhookService.fetchWebhooks.mockClear();
|
||||||
|
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||||
|
{ id: 'dummy-webhook', active: true } as MiWebhook,
|
||||||
|
]));
|
||||||
|
|
||||||
|
await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root))
|
||||||
|
.rejects.toThrow(WebhookTestService.NoSuchWebhookError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('testSystemWebhook', () => {
|
||||||
|
test('abuseReport', async () => {
|
||||||
|
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' });
|
||||||
|
|
||||||
|
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||||
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
|
expect(calls[1]).toBe('abuseReport');
|
||||||
|
expect((calls[2] as any).id).toBe('dummy-abuse-report1');
|
||||||
|
expect((calls[2] as any).resolved).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('abuseReportResolved', async () => {
|
||||||
|
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' });
|
||||||
|
|
||||||
|
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||||
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
|
expect(calls[1]).toBe('abuseReportResolved');
|
||||||
|
expect((calls[2] as any).id).toBe('dummy-abuse-report1');
|
||||||
|
expect((calls[2] as any).resolved).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('userCreated', async () => {
|
||||||
|
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' });
|
||||||
|
|
||||||
|
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||||
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
|
expect(calls[1]).toBe('userCreated');
|
||||||
|
expect((calls[2] as any).id).toBe('dummy-user-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,9 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
|
export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
|
||||||
|
|
||||||
export type MkSystemWebhookEditorProps = {
|
export type MkSystemWebhookEditorProps = {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
|
|
|
@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFolder :defaultOpen="true">
|
<MkFolder :defaultOpen="true">
|
||||||
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps">
|
||||||
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
<div class="_gaps_s">
|
||||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
<div :class="$style.switchBox">
|
||||||
</MkSwitch>
|
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||||
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
</MkSwitch>
|
||||||
</MkSwitch>
|
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
|
||||||
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
</div>
|
||||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
<div :class="$style.switchBox">
|
||||||
</MkSwitch>
|
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.switchBox">
|
||||||
|
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="mode === 'edit'" :class="$style.description">
|
||||||
|
{{ i18n.ts._webhookSettings.testRemarks }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import {
|
import {
|
||||||
|
@ -180,6 +196,21 @@ async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> {
|
||||||
|
if (!id.value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
await os.apiWithDialog('admin/system-webhook/test', {
|
||||||
|
webhookId: id.value,
|
||||||
|
type,
|
||||||
|
override: {
|
||||||
|
secret: secret.value,
|
||||||
|
url: url.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadingScope(async () => {
|
await loadingScope(async () => {
|
||||||
switch (mode.value) {
|
switch (mode.value) {
|
||||||
|
@ -235,4 +266,29 @@ onMounted(async () => {
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switchBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
.testButton {
|
||||||
|
$buttonSize: 28px;
|
||||||
|
padding: 0;
|
||||||
|
width: $buttonSize;
|
||||||
|
min-width: $buttonSize;
|
||||||
|
max-width: $buttonSize;
|
||||||
|
height: $buttonSize;
|
||||||
|
margin-left: auto;
|
||||||
|
line-height: normal;
|
||||||
|
font-size: 90%;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 8px 0 0 0;
|
||||||
|
color: var(--fgTransparentWeak);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,14 +21,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps">
|
||||||
<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
|
<div class="_gaps_s">
|
||||||
<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
|
<div :class="$style.switchBox">
|
||||||
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
|
||||||
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton>
|
||||||
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
</div>
|
||||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
<div :class="$style.switchBox">
|
||||||
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
|
||||||
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_followed)" @click="test('followed')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.switchBox">
|
||||||
|
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
||||||
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_note)" @click="test('note')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.switchBox">
|
||||||
|
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
||||||
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reply)" @click="test('reply')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.switchBox">
|
||||||
|
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
||||||
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.switchBox">
|
||||||
|
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||||
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.switchBox">
|
||||||
|
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
||||||
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_mention)" @click="test('mention')"><i class="ti ti-send"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.description">
|
||||||
|
{{ i18n.ts._webhookSettings.testRemarks }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
@ -76,8 +104,8 @@ const event_renote = ref(webhook.on.includes('renote'));
|
||||||
const event_reaction = ref(webhook.on.includes('reaction'));
|
const event_reaction = ref(webhook.on.includes('reaction'));
|
||||||
const event_mention = ref(webhook.on.includes('mention'));
|
const event_mention = ref(webhook.on.includes('mention'));
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
function save() {
|
||||||
const events = [];
|
const events: Misskey.entities.UserWebhook['on'] = [];
|
||||||
if (event_follow.value) events.push('follow');
|
if (event_follow.value) events.push('follow');
|
||||||
if (event_followed.value) events.push('followed');
|
if (event_followed.value) events.push('followed');
|
||||||
if (event_note.value) events.push('note');
|
if (event_note.value) events.push('note');
|
||||||
|
@ -110,8 +138,21 @@ async function del(): Promise<void> {
|
||||||
router.push('/settings/webhook');
|
router.push('/settings/webhook');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {
|
||||||
|
await os.apiWithDialog('i/webhooks/test', {
|
||||||
|
webhookId: props.webhookId,
|
||||||
|
type,
|
||||||
|
override: {
|
||||||
|
secret: secret.value,
|
||||||
|
url: url.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const headerActions = computed(() => []);
|
const headerActions = computed(() => []);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const headerTabs = computed(() => []);
|
const headerTabs = computed(() => []);
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
|
@ -119,3 +160,30 @@ definePageMetadata(() => ({
|
||||||
icon: 'ti ti-webhook',
|
icon: 'ti ti-webhook',
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.switchBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
.testButton {
|
||||||
|
$buttonSize: 28px;
|
||||||
|
padding: 0;
|
||||||
|
width: $buttonSize;
|
||||||
|
min-width: $buttonSize;
|
||||||
|
max-width: $buttonSize;
|
||||||
|
height: $buttonSize;
|
||||||
|
margin-left: auto;
|
||||||
|
line-height: inherit;
|
||||||
|
font-size: 90%;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 8px 0 0 0;
|
||||||
|
color: var(--fgTransparentWeak);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -358,6 +358,9 @@ type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
|
type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
|
type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -1308,6 +1311,7 @@ declare namespace entities {
|
||||||
AdminSystemWebhookShowResponse,
|
AdminSystemWebhookShowResponse,
|
||||||
AdminSystemWebhookUpdateRequest,
|
AdminSystemWebhookUpdateRequest,
|
||||||
AdminSystemWebhookUpdateResponse,
|
AdminSystemWebhookUpdateResponse,
|
||||||
|
AdminSystemWebhookTestRequest,
|
||||||
AnnouncementsRequest,
|
AnnouncementsRequest,
|
||||||
AnnouncementsResponse,
|
AnnouncementsResponse,
|
||||||
AnnouncementsShowRequest,
|
AnnouncementsShowRequest,
|
||||||
|
@ -1567,6 +1571,7 @@ declare namespace entities {
|
||||||
IWebhooksShowResponse,
|
IWebhooksShowResponse,
|
||||||
IWebhooksUpdateRequest,
|
IWebhooksUpdateRequest,
|
||||||
IWebhooksDeleteRequest,
|
IWebhooksDeleteRequest,
|
||||||
|
IWebhooksTestRequest,
|
||||||
InviteCreateResponse,
|
InviteCreateResponse,
|
||||||
InviteDeleteRequest,
|
InviteDeleteRequest,
|
||||||
InviteListRequest,
|
InviteListRequest,
|
||||||
|
@ -2369,6 +2374,9 @@ type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['co
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
|
type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
|
type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
|
|
@ -960,6 +960,18 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
|
||||||
|
*/
|
||||||
|
request<E extends 'admin/system-webhook/test', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
@ -2819,6 +2831,18 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
request<E extends 'i/webhooks/test', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -117,6 +117,7 @@ import type {
|
||||||
AdminSystemWebhookShowResponse,
|
AdminSystemWebhookShowResponse,
|
||||||
AdminSystemWebhookUpdateRequest,
|
AdminSystemWebhookUpdateRequest,
|
||||||
AdminSystemWebhookUpdateResponse,
|
AdminSystemWebhookUpdateResponse,
|
||||||
|
AdminSystemWebhookTestRequest,
|
||||||
AnnouncementsRequest,
|
AnnouncementsRequest,
|
||||||
AnnouncementsResponse,
|
AnnouncementsResponse,
|
||||||
AnnouncementsShowRequest,
|
AnnouncementsShowRequest,
|
||||||
|
@ -376,6 +377,7 @@ import type {
|
||||||
IWebhooksShowResponse,
|
IWebhooksShowResponse,
|
||||||
IWebhooksUpdateRequest,
|
IWebhooksUpdateRequest,
|
||||||
IWebhooksDeleteRequest,
|
IWebhooksDeleteRequest,
|
||||||
|
IWebhooksTestRequest,
|
||||||
InviteCreateResponse,
|
InviteCreateResponse,
|
||||||
InviteDeleteRequest,
|
InviteDeleteRequest,
|
||||||
InviteListRequest,
|
InviteListRequest,
|
||||||
|
@ -660,6 +662,7 @@ export type Endpoints = {
|
||||||
'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
|
'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
|
||||||
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
|
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
|
||||||
'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
|
'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
|
||||||
|
'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
|
||||||
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
|
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
|
||||||
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
|
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
|
||||||
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
|
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
|
||||||
|
@ -826,6 +829,7 @@ export type Endpoints = {
|
||||||
'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
|
'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
|
||||||
'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
|
'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
|
||||||
'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
|
'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
|
||||||
|
'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse };
|
||||||
'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
|
'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
|
||||||
'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
|
'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
|
||||||
'invite/list': { req: InviteListRequest; res: InviteListResponse };
|
'invite/list': { req: InviteListRequest; res: InviteListResponse };
|
||||||
|
|
|
@ -120,6 +120,7 @@ export type AdminSystemWebhookShowRequest = operations['admin___system-webhook__
|
||||||
export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
|
export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
|
||||||
export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
|
export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
|
||||||
export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
|
export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
|
||||||
|
export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
|
||||||
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
|
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
|
||||||
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
|
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
|
||||||
export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
|
export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
|
||||||
|
@ -379,6 +380,7 @@ export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBod
|
||||||
export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
|
export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
|
||||||
export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
|
export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
|
||||||
export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
|
export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
|
||||||
|
export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
|
||||||
export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json'];
|
export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json'];
|
||||||
export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
|
export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
|
||||||
export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
|
export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -797,6 +797,16 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['admin___system-webhook___update'];
|
post: operations['admin___system-webhook___update'];
|
||||||
};
|
};
|
||||||
|
'/admin/system-webhook/test': {
|
||||||
|
/**
|
||||||
|
* admin/system-webhook/test
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
|
||||||
|
*/
|
||||||
|
post: operations['admin___system-webhook___test'];
|
||||||
|
};
|
||||||
'/announcements': {
|
'/announcements': {
|
||||||
/**
|
/**
|
||||||
* announcements
|
* announcements
|
||||||
|
@ -2436,6 +2446,16 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['i___webhooks___delete'];
|
post: operations['i___webhooks___delete'];
|
||||||
};
|
};
|
||||||
|
'/i/webhooks/test': {
|
||||||
|
/**
|
||||||
|
* i/webhooks/test
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
post: operations['i___webhooks___test'];
|
||||||
|
};
|
||||||
'/invite/create': {
|
'/invite/create': {
|
||||||
/**
|
/**
|
||||||
* invite/create
|
* invite/create
|
||||||
|
@ -10327,6 +10347,71 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* admin/system-webhook/test
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
|
||||||
|
*/
|
||||||
|
'admin___system-webhook___test': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
webhookId: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'abuseReport' | 'abuseReportResolved' | 'userCreated';
|
||||||
|
override?: {
|
||||||
|
url?: string;
|
||||||
|
secret?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description To many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* announcements
|
* announcements
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
@ -20146,6 +20231,71 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* i/webhooks/test
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
i___webhooks___test: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
webhookId: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction';
|
||||||
|
override?: {
|
||||||
|
url?: string;
|
||||||
|
secret?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description To many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* invite/create
|
* invite/create
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
Loading…
Reference in a new issue