summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
authorおさむのひと <46447427+samunohito@users.noreply.github.com>2024-07-29 21:31:32 +0900
committerGitHub <noreply@github.com>2024-07-29 21:31:32 +0900
commit72bc78974657b22ab6b1f5a36f6144c294e36de3 (patch)
treeb2d25d52e254f91e1b718123c25e73afea2039bb /packages/backend
parentNew Crowdin updates (#13916) (diff)
downloadsharkey-72bc78974657b22ab6b1f5a36f6144c294e36de3.tar.gz
sharkey-72bc78974657b22ab6b1f5a36f6144c294e36de3.tar.bz2
sharkey-72bc78974657b22ab6b1f5a36f6144c294e36de3.zip
feature: ユーザ作成時にSystemWebhookを発信できるようにする (#14321)
* feature: ユーザ作成時にSystemWebhookを発信できるようにする * fix CHANGELOG.md
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/src/core/AbuseReportNotificationService.ts2
-rw-r--r--packages/backend/src/core/SignupService.ts5
-rw-r--r--packages/backend/src/core/UserService.ts24
-rw-r--r--packages/backend/src/models/SystemWebhook.ts2
-rw-r--r--packages/backend/test/e2e/synalio/abuse-report.ts61
-rw-r--r--packages/backend/test/e2e/synalio/user-create.ts130
-rw-r--r--packages/backend/test/utils.ts55
7 files changed, 221 insertions, 58 deletions
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index 42e5931212..7be5335885 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -44,7 +44,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
/**
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
- * 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
+ * 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
*
* @see RoleService.getModeratorIds
* @see GlobalEventService.publishAdminStream
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 5522ecd6cc..de45898328 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
+import { UserService } from '@/core/UserService.js';
@Injectable()
export class SignupService {
@@ -35,6 +36,7 @@ export class SignupService {
private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
+ private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
@@ -148,7 +150,8 @@ export class SignupService {
}));
});
- this.usersChart.update(account, true);
+ this.usersChart.update(account, true).then();
+ this.userService.notifySystemWebhook(account, 'userCreated').then();
return { account, secret };
}
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
index 72fa4d928d..9b1961c631 100644
--- a/packages/backend/src/core/UserService.ts
+++ b/packages/backend/src/core/UserService.ts
@@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
@Injectable()
export class UserService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
-
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+ private systemWebhookService: SystemWebhookService,
+ private userEntityService: UserEntityService,
) {
}
@@ -50,4 +53,23 @@ export class UserService {
});
}
}
+
+ /**
+ * SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
+ * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+ *
+ * @see SystemWebhookService.enqueueSystemWebhook
+ */
+ @bindThis
+ public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
+ const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
+ const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
+ for (const webhookId of recipientWebhookIds) {
+ await this.systemWebhookService.enqueueSystemWebhook(
+ webhookId,
+ type,
+ packedUser,
+ );
+ }
+ }
}
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
index 86fb323d1d..d6c27eae51 100644
--- a/packages/backend/src/models/SystemWebhook.ts
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -12,6 +12,8 @@ export const systemWebhookEventTypes = [
'abuseReport',
// 通報を処理したとき
'abuseReportResolved',
+ // ユーザが作成された時
+ 'userCreated',
] as const;
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index b0cc3d13ec..6ce6e47781 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -5,65 +5,24 @@
import { entities } from 'misskey-js';
import { beforeEach, describe, test } from '@jest/globals';
-import Fastify from 'fastify';
-import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
+import {
+ api,
+ captureWebhook,
+ randomString,
+ role,
+ signup,
+ startJobQueue,
+ UserToken,
+ WEBHOOK_HOST,
+} from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
-const WEBHOOK_HOST = 'http://localhost:15080';
-const WEBHOOK_PORT = 15080;
-process.env.NODE_ENV = 'test';
-
describe('[シナリオ] ユーザ通報', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;
let alice: entities.SignupResponse;
let bob: entities.SignupResponse;
- type SystemWebhookPayload = {
- server: string;
- hookId: string;
- eventId: string;
- createdAt: string;
- type: string;
- body: any;
- }
-
- // -------------------------------------------------------------------------------------------
-
- async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>): Promise<T> {
- const fastify = Fastify();
-
- let timeoutHandle: NodeJS.Timeout | null = null;
- const result = await new Promise<string>(async (resolve, reject) => {
- fastify.all('/', async (req, res) => {
- timeoutHandle && clearTimeout(timeoutHandle);
-
- const body = JSON.stringify(req.body);
- res.status(200).send('ok');
- await fastify.close();
- resolve(body);
- });
-
- await fastify.listen({ port: WEBHOOK_PORT });
-
- timeoutHandle = setTimeout(async () => {
- await fastify.close();
- reject(new Error('timeout'));
- }, 3000);
-
- try {
- await postAction();
- } catch (e) {
- await fastify.close();
- reject(e);
- }
- });
-
- await fastify.close();
-
- return JSON.parse(result) as T;
- }
-
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
const res = await api(
'admin/system-webhook/create',
diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts
new file mode 100644
index 0000000000..cb0f68dfea
--- /dev/null
+++ b/packages/backend/test/e2e/synalio/user-create.ts
@@ -0,0 +1,130 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { setTimeout } from 'node:timers/promises';
+import { entities } from 'misskey-js';
+import { beforeEach, describe, test } from '@jest/globals';
+import {
+ api,
+ captureWebhook,
+ randomString,
+ role,
+ signup,
+ startJobQueue,
+ UserToken,
+ WEBHOOK_HOST,
+} from '../../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('[シナリオ] ユーザ作成', () => {
+ let queue: INestApplicationContext;
+ let admin: entities.SignupResponse;
+
+ async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
+ const res = await api(
+ 'admin/system-webhook/create',
+ {
+ isActive: true,
+ name: randomString(),
+ on: ['userCreated'],
+ url: WEBHOOK_HOST,
+ secret: randomString(),
+ ...args,
+ },
+ credential ?? admin,
+ );
+ return res.body;
+ }
+
+ // -------------------------------------------------------------------------------------------
+
+ beforeAll(async () => {
+ queue = await startJobQueue();
+ admin = await signup({ username: 'admin' });
+
+ await role(admin, { isAdministrator: true });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await queue.close();
+ });
+
+ // -------------------------------------------------------------------------------------------
+
+ describe('SystemWebhook', () => {
+ beforeEach(async () => {
+ const webhooks = await api('admin/system-webhook/list', {}, admin);
+ for (const webhook of webhooks.body) {
+ await api('admin/system-webhook/delete', { id: webhook.id }, admin);
+ }
+ });
+
+ test('ユーザが作成された -> userCreatedが送出される', async () => {
+ const webhook = await createSystemWebhook({
+ on: ['userCreated'],
+ isActive: true,
+ });
+
+ let alice: any = null;
+ const webhookBody = await captureWebhook(async () => {
+ alice = await signup({ username: 'alice' });
+ });
+
+ // webhookの送出後にいろいろやってるのでちょっと待つ必要がある
+ await setTimeout(2000);
+
+ console.log(alice);
+ console.log(JSON.stringify(webhookBody, null, 2));
+
+ expect(webhookBody.hookId).toBe(webhook.id);
+ expect(webhookBody.type).toBe('userCreated');
+
+ const body = webhookBody.body as entities.UserLite;
+ expect(alice.id).toBe(body.id);
+ expect(alice.name).toBe(body.name);
+ expect(alice.username).toBe(body.username);
+ expect(alice.host).toBe(body.host);
+ expect(alice.avatarUrl).toBe(body.avatarUrl);
+ expect(alice.avatarBlurhash).toBe(body.avatarBlurhash);
+ expect(alice.avatarDecorations).toEqual(body.avatarDecorations);
+ expect(alice.isBot).toBe(body.isBot);
+ expect(alice.isCat).toBe(body.isCat);
+ expect(alice.instance).toEqual(body.instance);
+ expect(alice.emojis).toEqual(body.emojis);
+ expect(alice.onlineStatus).toBe(body.onlineStatus);
+ expect(alice.badgeRoles).toEqual(body.badgeRoles);
+ });
+
+ test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => {
+ await createSystemWebhook({
+ on: [],
+ isActive: true,
+ });
+
+ let alice: any = null;
+ const webhookBody = await captureWebhook(async () => {
+ alice = await signup({ username: 'alice' });
+ }).catch(e => e.message);
+
+ expect(webhookBody).toBe('timeout');
+ expect(alice.id).not.toBeNull();
+ });
+
+ test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => {
+ await createSystemWebhook({
+ on: ['userCreated'],
+ isActive: false,
+ });
+
+ let alice: any = null;
+ const webhookBody = await captureWebhook(async () => {
+ alice = await signup({ username: 'alice' });
+ }).catch(e => e.message);
+
+ expect(webhookBody).toBe('timeout');
+ expect(alice.id).not.toBeNull();
+ });
+ });
+});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index e70befeebe..26de19eaf1 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -12,13 +12,14 @@ import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
-import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
+import { type Response } from 'node-fetch';
+import Fastify from 'fastify';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js';
-import { type Response } from 'node-fetch';
-import { ApiError } from "@/server/api/error.js";
+import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
+import { ApiError } from '@/server/api/error.js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@@ -27,11 +28,23 @@ export interface UserToken {
bearer?: boolean;
}
+export type SystemWebhookPayload = {
+ server: string;
+ hookId: string;
+ eventId: string;
+ createdAt: string;
+ type: string;
+ body: any;
+}
+
const config = loadConfig();
export const port = config.port;
export const origin = config.url;
export const host = new URL(config.url).host;
+export const WEBHOOK_HOST = 'http://localhost:15080';
+export const WEBHOOK_PORT = 15080;
+
export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
};
@@ -645,3 +658,37 @@ export async function sendEnvResetRequest() {
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
return obj as { error: ApiError };
}
+
+export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>, port = WEBHOOK_PORT): Promise<T> {
+ const fastify = Fastify();
+
+ let timeoutHandle: NodeJS.Timeout | null = null;
+ const result = await new Promise<string>(async (resolve, reject) => {
+ fastify.all('/', async (req, res) => {
+ timeoutHandle && clearTimeout(timeoutHandle);
+
+ const body = JSON.stringify(req.body);
+ res.status(200).send('ok');
+ await fastify.close();
+ resolve(body);
+ });
+
+ await fastify.listen({ port });
+
+ timeoutHandle = setTimeout(async () => {
+ await fastify.close();
+ reject(new Error('timeout'));
+ }, 3000);
+
+ try {
+ await postAction();
+ } catch (e) {
+ await fastify.close();
+ reject(e);
+ }
+ });
+
+ await fastify.close();
+
+ return JSON.parse(result) as T;
+}