summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorおさむのひと <46447427+samunohito@users.noreply.github.com>2024-09-19 17:20:50 +0900
committerGitHub <noreply@github.com>2024-09-19 17:20:50 +0900
commit4ac8aad50a1a1ef2ac2a13a04baca445294397ed (patch)
tree37a81a7ca6a760dc0be88b61f409c4d24354d3ca
parentfix(frontend): viteの一時ファイルがgitの変更に含まれないよ... (diff)
downloadsharkey-4ac8aad50a1a1ef2ac2a13a04baca445294397ed.tar.gz
sharkey-4ac8aad50a1a1ef2ac2a13a04baca445294397ed.tar.bz2
sharkey-4ac8aad50a1a1ef2ac2a13a04baca445294397ed.zip
feat: UserWebhook/SystemWebhookのテスト送信機能を追加 (#14489)
* feat: UserWebhook/SystemWebhookのテスト送信機能を追加 * fix CHANGELOG.md * 一部設定をパラメータから上書き出来るように修正 * remove async * regenerate autogen
-rw-r--r--CHANGELOG.md2
-rw-r--r--locales/index.d.ts4
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/QueueService.ts22
-rw-r--r--packages/backend/src/core/SystemWebhookService.ts13
-rw-r--r--packages/backend/src/core/UserWebhookService.ts29
-rw-r--r--packages/backend/src/core/WebhookTestService.ts434
-rw-r--r--packages/backend/src/models/Webhook.ts1
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts77
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/list.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/show.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/test.ts76
-rw-r--r--packages/backend/test/unit/SystemWebhookService.ts11
-rw-r--r--packages/backend/test/unit/UserWebhookService.ts245
-rw-r--r--packages/backend/test/unit/WebhookTestService.ts225
-rw-r--r--packages/frontend/src/components/MkSystemWebhookEditor.impl.ts3
-rw-r--r--packages/frontend/src/components/MkSystemWebhookEditor.vue76
-rw-r--r--packages/frontend/src/pages/settings/webhook.edit.vue88
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md8
-rw-r--r--packages/misskey-js/src/autogen/apiClientJSDoc.ts24
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts4
-rw-r--r--packages/misskey-js/src/autogen/entities.ts2
-rw-r--r--packages/misskey-js/src/autogen/types.ts150
27 files changed, 1477 insertions, 39 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c727cea78..4f3cd133bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
## Unreleased
### General
--
+- UserWebhookとSystemWebhookのテスト送信機能を追加 ( #14445 )
### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
diff --git a/locales/index.d.ts b/locales/index.d.ts
index b06e0f245b..bd2421a5ca 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9477,6 +9477,10 @@ export interface Locale extends ILocale {
* Webhookを削除しますか?
*/
"deleteConfirm": string;
+ /**
+ * スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
+ */
+ "testRemarks": string;
};
"_abuseReport": {
"_notificationRecipient": {
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 292569cc5a..2a5b530c9f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2514,6 +2514,7 @@ _webhookSettings:
abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき"
deleteConfirm: "Webhookを削除しますか?"
+ testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
_abuseReport:
_notificationRecipient:
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index c9427bbeb7..674241ac12 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -13,6 +13,7 @@ import {
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.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 $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
+const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -359,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
+ WebhookTestService,
UtilityService,
FileInfoService,
SearchService,
@@ -503,6 +506,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
+ $WebhookTestService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -648,6 +652,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
+ WebhookTestService,
UtilityService,
FileInfoService,
SearchService,
@@ -791,6 +796,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
+ $WebhookTestService,
$UtilityService,
$FileInfoService,
$SearchService,
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 80827a500b..ddb90a051f 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -452,10 +452,15 @@ export class QueueService {
/**
* @see UserWebhookDeliverJobData
- * @see WebhookDeliverProcessorService
+ * @see UserWebhookDeliverProcessorService
*/
@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 = {
type,
content,
@@ -468,7 +473,7 @@ export class QueueService {
};
return this.userWebhookDeliverQueue.add(webhook.id, data, {
- attempts: 4,
+ attempts: opts?.attempts ?? 4,
backoff: {
type: 'custom',
},
@@ -479,10 +484,15 @@ export class QueueService {
/**
* @see SystemWebhookDeliverJobData
- * @see WebhookDeliverProcessorService
+ * @see SystemWebhookDeliverProcessorService
*/
@bindThis
- public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ public systemWebhookDeliver(
+ webhook: MiSystemWebhook,
+ type: SystemWebhookEventType,
+ content: unknown,
+ opts?: { attempts?: number },
+ ) {
const data: SystemWebhookDeliverJobData = {
type,
content,
@@ -494,7 +504,7 @@ export class QueueService {
};
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
- attempts: 4,
+ attempts: opts?.attempts ?? 4,
backoff: {
type: 'custom',
},
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index bc6851f788..bb7c6b8c0e 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
* SystemWebhook の一覧を取得する.
*/
@bindThis
- public async fetchSystemWebhooks(params?: {
+ public fetchSystemWebhooks(params?: {
ids?: MiSystemWebhook['id'][];
isActive?: MiSystemWebhook['isActive'];
on?: MiSystemWebhook['on'];
@@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
/**
* SystemWebhook をWebhook配送キューに追加する
* @see QueueService.systemWebhookDeliver
+ * // TODO: contentの型を厳格化する
*/
@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'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
: webhook;
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;
}
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;
}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
index e96bfeea95..8a40a53688 100644
--- a/packages/backend/src/core/UserWebhookService.ts
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -5,8 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
+import { type WebhooksRepository } from '@/models/_.js';
+import { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
@@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
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
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
new file mode 100644
index 0000000000..0b4e107d21
--- /dev/null
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -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;
+ }
+ }
+ }
+}
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index db24c03b3d..b4cab4edc8 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -8,6 +8,7 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
+export type WebhookEventTypes = typeof webhookEventTypes[number];
@Entity('webhook')
export class MiWebhook {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 41576bedaa..08a0468ab2 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -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_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_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.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_update from './endpoints/i/webhooks/update.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_delete from './endpoints/invite/delete.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_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_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_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.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_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_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_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.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_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1028,6 +1033,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
@@ -1243,6 +1249,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1409,6 +1416,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 3dfb7fdad4..2462781f7b 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -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_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_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.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_update from './endpoints/i/webhooks/update.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_delete from './endpoints/invite/delete.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/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
+ ['admin/system-webhook/test', ep___admin_systemWebhook_test],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@@ -645,6 +648,7 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
+ ['i/webhooks/test', ep___i_webhooks_test],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
new file mode 100644
index 0000000000..fb2ddf4b44
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
@@ -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;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index 9eb7f5b3a0..6e84603f7a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
index fe07afb2d0..394c178f2a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks', 'account'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
index 5ddb79caf2..4a0c09ff0c 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
new file mode 100644
index 0000000000..2bf6df9ce2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
@@ -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;
+ }
+ });
+ }
+}
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
index 790cd1490e..5401dd74d8 100644
--- a/packages/backend/test/unit/SystemWebhookService.ts
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
@@ -6,6 +7,7 @@
import { setTimeout } from 'node:timers/promises';
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 { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
@@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
-import { randomString } from '../utils.js';
describe('SystemWebhookService', () => {
let app: TestingModule;
@@ -313,7 +314,7 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReport'],
});
- await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
+ await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
});
@@ -323,7 +324,7 @@ describe('SystemWebhookService', () => {
isActive: false,
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();
});
@@ -337,8 +338,8 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReportResolved'],
});
- await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
- await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
+ await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
+ await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts
new file mode 100644
index 0000000000..0e88835a02
--- /dev/null
+++ b/packages/backend/test/unit/UserWebhookService.ts
@@ -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]);
+ });
+ });
+ });
+});
diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts
new file mode 100644
index 0000000000..5e63b86f8f
--- /dev/null
+++ b/packages/backend/test/unit/WebhookTestService.ts
@@ -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');
+ });
+ });
+});
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
index 69b8edd85a..19e4eea733 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -4,9 +4,10 @@
*/
import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
-export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
+export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
export type MkSystemWebhookEditorProps = {
mode: 'create' | 'edit';
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index f5c7a3160b..ec3b1c90ca 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
- <div class="_gaps_s">
- <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
- <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
- </MkSwitch>
- <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
- <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
- </MkSwitch>
- <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
- <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
- </MkSwitch>
+ <div class="_gaps">
+ <div class="_gaps_s">
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
+ <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
+ </MkSwitch>
+ <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
+ </div>
+ <div :class="$style.switchBox">
+ <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>
</MkFolder>
@@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
+import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
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 () => {
await loadingScope(async () => {
switch (mode.value) {
@@ -235,4 +266,29 @@ onMounted(async () => {
-webkit-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>
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 058ef69c35..adeaf8550c 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -21,14 +21,41 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
- <div class="_gaps_s">
- <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
- <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
- <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
- <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
- <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
- <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
- <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
+ <div class="_gaps">
+ <div class="_gaps_s">
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
+ <MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton>
+ </div>
+ <div :class="$style.switchBox">
+ <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>
</FormSection>
@@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.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_mention = ref(webhook.on.includes('mention'));
-async function save(): Promise<void> {
- const events = [];
+function save() {
+ const events: Misskey.entities.UserWebhook['on'] = [];
if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note');
@@ -110,8 +138,21 @@ async function del(): Promise<void> {
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(() => []);
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const headerTabs = computed(() => []);
definePageMetadata(() => ({
@@ -119,3 +160,30 @@ definePageMetadata(() => ({
icon: 'ti ti-webhook',
}));
</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>
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 1ec7f0ec7f..d1050d4727 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -359,6 +359,9 @@ type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']
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)
type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -1308,6 +1311,7 @@ declare namespace entities {
AdminSystemWebhookShowResponse,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
+ AdminSystemWebhookTestRequest,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -1567,6 +1571,7 @@ declare namespace entities {
IWebhooksShowResponse,
IWebhooksUpdateRequest,
IWebhooksDeleteRequest,
+ IWebhooksTestRequest,
InviteCreateResponse,
InviteDeleteRequest,
InviteListRequest,
@@ -2370,6 +2375,9 @@ type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['co
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)
type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
// @public (undocumented)
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index e799d4a0c5..1d96196d1c 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -963,6 +963,18 @@ declare module '../api.js' {
/**
* 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.
+ *
* **Credential required**: *No*
*/
request<E extends 'announcements', P extends Endpoints[E]['req']>(
@@ -2822,6 +2834,18 @@ declare module '../api.js' {
/**
* 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.
+ *
* **Credential required**: *Yes* / **Permission**: *write:invite-codes*
*/
request<E extends 'invite/create', P extends Endpoints[E]['req']>(
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 8fbdbbb629..42c74599a5 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -117,6 +117,7 @@ import type {
AdminSystemWebhookShowResponse,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
+ AdminSystemWebhookTestRequest,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -376,6 +377,7 @@ import type {
IWebhooksShowResponse,
IWebhooksUpdateRequest,
IWebhooksDeleteRequest,
+ IWebhooksTestRequest,
InviteCreateResponse,
InviteDeleteRequest,
InviteListRequest,
@@ -660,6 +662,7 @@ export type Endpoints = {
'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
+ 'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
@@ -826,6 +829,7 @@ export type Endpoints = {
'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
+ 'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse };
'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
'invite/list': { req: InviteListRequest; res: InviteListResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 357b5e9eaf..87ed653d44 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -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 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 AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['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 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 IWebhooksUpdateRequest = operations['i___webhooks___update']['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 InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index b99a5373bb..03828b6552 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -797,6 +797,16 @@ export type paths = {
*/
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
@@ -2436,6 +2446,16 @@ export type paths = {
*/
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
@@ -10328,6 +10348,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
* @description No description provided.
*
@@ -20147,6 +20232,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
* @description No description provided.
*