-
-
- {{ message }}
-
-
-
-
- {{ i18n.ts.continueOnRemote }}
-
-
-
-
-
-
-
- @
- @{{ host }}
-
-
-
-
-
-
-
-
-
- {{ signing ? i18n.ts.loggingIn : i18n.ts.login }}
-
-
-
-
{{ i18n.ts.useSecurityKey }}
-
- {{ i18n.ts.retry }}
-
-
-
-
-
- {{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})
-
-
-
- {{ signing ? i18n.ts.loggingIn : i18n.ts.login }}
-
-
-
-
-
-
- {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
-
-
{{ i18n.ts.useSecurityKey }}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index d48780e9de..8351d7d5e0 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
- {{ i18n.ts.login }}
-
-
-
-
-
+
+
+
{{ i18n.ts.login }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 5f4792eb74..9ad784c296 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin'];
// @public (undocumented)
type SigninRequest = {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null;
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 32646d28ed..3876a0bfe5 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -3782,16 +3782,13 @@ export type components = {
followingVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */
followersVisibility: 'public' | 'followers' | 'private';
- /** @default false */
- twoFactorEnabled: boolean;
- /** @default false */
- usePasswordLessLogin: boolean;
- /** @default false */
- securityKeys: boolean;
roles: components['schemas']['RoleLite'][];
followedMessage?: string | null;
memo: string | null;
moderationNote?: string;
+ twoFactorEnabled?: boolean;
+ usePasswordLessLogin?: boolean;
+ securityKeys?: boolean;
isFollowing?: boolean;
isFollowed?: boolean;
hasPendingFollowRequestFromYou?: boolean;
@@ -3972,6 +3969,12 @@ export type components = {
}[];
loggedInDays: number;
policies: components['schemas']['RolePolicies'];
+ /** @default false */
+ twoFactorEnabled: boolean;
+ /** @default false */
+ usePasswordLessLogin: boolean;
+ /** @default false */
+ securityKeys: boolean;
email?: string | null;
emailVerified?: boolean | null;
securityKeysList?: {
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 36b7f5bca3..98ac50e5a1 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -269,7 +269,7 @@ export type SignupPendingResponse = {
export type SigninRequest = {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null;
--
cgit v1.2.3-freya
From 0d7d1091c8970d9979e8efb02f0accd6dcd39422 Mon Sep 17 00:00:00 2001
From: おさむのひと <46447427+samunohito@users.noreply.github.com>
Date: Sat, 5 Oct 2024 14:37:52 +0900
Subject: enhance: 人気のPlayを10件以上表示できるように (#14443)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
---
CHANGELOG.md | 1 +
packages/backend/src/core/CoreModule.ts | 5 +
packages/backend/src/core/FlashService.ts | 40 ++++++
.../src/core/entities/FlashEntityService.ts | 41 ++++--
packages/backend/src/models/Flash.ts | 5 +-
.../src/server/api/endpoints/flash/featured.ts | 22 +--
packages/backend/test/unit/FlashService.ts | 152 +++++++++++++++++++++
packages/frontend/src/pages/flash/flash-index.vue | 3 +-
packages/misskey-js/etc/misskey-js.api.md | 4 +
packages/misskey-js/src/autogen/endpoint.ts | 3 +-
packages/misskey-js/src/autogen/entities.ts | 1 +
packages/misskey-js/src/autogen/types.ts | 10 ++
12 files changed, 262 insertions(+), 25 deletions(-)
create mode 100644 packages/backend/src/core/FlashService.ts
create mode 100644 packages/backend/test/unit/FlashService.ts
(limited to 'packages/backend/src/models')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04acc11ac3..6a9143ea1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新
- Enhance: l10nの更新
+- Enhance: Playの「人気」タブで10件以上表示可能に #14399
- Fix: 連合のホワイトリストが正常に登録されない問題を修正
### Client
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 3b3c35f976..734d135648 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
@@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookTestService,
$UtilityService,
$FileInfoService,
+ $FlashService,
$SearchService,
$ClipService,
$FeaturedService,
@@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts
new file mode 100644
index 0000000000..2a98225382
--- /dev/null
+++ b/packages/backend/src/core/FlashService.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { type FlashsRepository } from '@/models/_.js';
+
+/**
+ * MisskeyPlay関係のService
+ */
+@Injectable()
+export class FlashService {
+ constructor(
+ @Inject(DI.flashsRepository)
+ private flashRepository: FlashsRepository,
+ ) {
+ }
+
+ /**
+ * 人気のあるPlay一覧を取得する.
+ */
+ public async featured(opts?: { offset?: number, limit: number }) {
+ const builder = this.flashRepository.createQueryBuilder('flash')
+ .andWhere('flash.likedCount > 0')
+ .andWhere('flash.visibility = :visibility', { visibility: 'public' })
+ .addOrderBy('flash.likedCount', 'DESC')
+ .addOrderBy('flash.updatedAt', 'DESC')
+ .addOrderBy('flash.id', 'DESC');
+
+ if (opts?.offset) {
+ builder.skip(opts.offset);
+ }
+
+ builder.take(opts?.limit ?? 10);
+
+ return await builder.getMany();
+ }
+}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index 4aa7104c1e..0cdcf3310a 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
-import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
-
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
-
private userEntityService: UserEntityService,
private idService: IdService,
) {
@@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
- packedUser?: Packed<'UserLite'>
+ packedUser?: Packed<'UserLite'>,
+ likedFlashIds?: MiFlash['id'][],
},
): Promise
> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
- return await awaitAll({
+ // { schema: 'UserDetailed' } すると無限ループするので注意
+ const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
+
+ let isLiked = false;
+ if (meId) {
+ isLiked = hint?.likedFlashIds
+ ? hint.likedFlashIds.includes(flash.id)
+ : await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
+ }
+
+ return {
id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: user,
title: flash.title,
summary: flash.summary,
script: flash.script,
visibility: flash.visibility,
likedCount: flash.likedCount,
- isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
- });
+ isLiked: isLiked,
+ };
}
@bindThis
@@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
- return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
+ const _likedFlashIds = me
+ ? await this.flashLikesRepository.createQueryBuilder('flashLike')
+ .select('flashLike.flashId')
+ .where('flashLike.userId = :userId', { userId: me.id })
+ .getRawMany<{ flashLike_flashId: string }>()
+ .then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
+ : [];
+ return Promise.all(
+ flashes.map(flash => this.pack(flash, me, {
+ packedUser: _userMap.get(flash.userId),
+ likedFlashIds: _likedFlashIds,
+ })),
+ );
}
}
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index a1469a0d94..5db7dca992 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
+export const flashVisibility = ['public', 'private'] as const;
+export type FlashVisibility = typeof flashVisibility[number];
+
@Entity('flash')
export class MiFlash {
@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', {
length: 512, default: 'public',
})
- public visibility: 'public' | 'private';
+ public visibility: FlashVisibility;
}
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ offset: { type: 'integer', minimum: 0, default: 0 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.flashsRepository)
- private flashsRepository: FlashsRepository,
-
+ private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.flashsRepository.createQueryBuilder('flash')
- .andWhere('flash.likedCount > 0')
- .orderBy('flash.likedCount', 'DESC');
-
- const flashs = await query.limit(10).getMany();
-
- return await this.flashEntityService.packMany(flashs, me);
+ const result = await this.flashService.featured({
+ offset: ps.offset,
+ limit: ps.limit,
+ });
+ return await this.flashEntityService.packMany(result, me);
});
}
}
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+ let app: TestingModule;
+ let service: FlashService;
+
+ // --------------------------------------------------------------------------------------
+
+ let flashsRepository: FlashsRepository;
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+ let alice: MiUser;
+ let bob: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createFlash(data: Partial) {
+ return flashsRepository.insert({
+ id: idService.gen(),
+ updatedAt: new Date(),
+ userId: root.id,
+ title: 'title',
+ summary: 'summary',
+ script: 'script',
+ permissions: [],
+ likedCount: 0,
+ ...data,
+ }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createUser(data: Partial = {}) {
+ const user = await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeEach(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ FlashService,
+ IdService,
+ ],
+ }).compile();
+
+ service = app.get(FlashService);
+
+ flashsRepository = app.get(DI.flashsRepository);
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+ idService = app.get(IdService);
+
+ root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+ });
+
+ afterEach(async () => {
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ await flashsRepository.delete({});
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('featured', () => {
+ test('should return featured flashes', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash3, flash2, flash1]);
+ });
+
+ test('should return featured flashes public visibility only', async () => {
+ const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+ const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+ const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with offset', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 1,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with limit', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 2,
+ });
+
+ expect(result).toEqual([flash3, flash2]);
+ });
+ });
+});
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index f63a799365..2b85489706 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -55,7 +55,8 @@ const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
- noPaging: true,
+ limit: 5,
+ offsetMode: true,
};
const myFlashsPagination = {
endpoint: 'flash/my' as const,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 732352abd8..de52be3a61 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1680,6 +1680,7 @@ declare namespace entities {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -1929,6 +1930,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
// @public (undocumented)
type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+// @public (undocumented)
+type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+
// @public (undocumented)
type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 42c74599a5..bf61c20628 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -465,6 +465,7 @@ import type {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -889,7 +890,7 @@ export type Endpoints = {
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
- 'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
+ 'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 87ed653d44..72c7c35ed4 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -468,6 +468,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 3876a0bfe5..0938973481 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -23799,6 +23799,16 @@ export type operations = {
* **Credential required**: *No*
*/
flash___featured: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 0 */
+ offset?: number;
+ /** @default 10 */
+ limit?: number;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
--
cgit v1.2.3-freya
From d8cb7305ef4d5ad6398d9eb57ece2f3ba7ca73eb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 16:20:15 +0900
Subject: feat: 通報の強化 (#14704)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* wip
* Update CHANGELOG.md
* lint
* Update types.ts
* wip
* :v:
* Update MkAbuseReport.vue
* tweak
---
CHANGELOG.md | 3 +
locales/index.d.ts | 55 +++++++--
locales/ja-JP.yml | 15 ++-
.../1728085812127-refine-abuse-user-report.js | 18 +++
packages/backend/src/core/AbuseReportService.ts | 80 ++++++++++---
packages/backend/src/core/WebhookTestService.ts | 2 +
.../core/entities/AbuseUserReportEntityService.ts | 2 +
packages/backend/src/models/AbuseUserReport.ts | 18 +++
packages/backend/src/server/api/EndpointsModule.ts | 8 ++
packages/backend/src/server/api/endpoints.ts | 4 +
.../endpoints/admin/forward-abuse-user-report.ts | 55 +++++++++
.../endpoints/admin/resolve-abuse-user-report.ts | 4 +-
.../endpoints/admin/update-abuse-user-report.ts | 58 ++++++++++
packages/backend/src/types.ts | 15 ++-
packages/backend/test/e2e/synalio/abuse-report.ts | 6 -
packages/frontend/src/components/MkAbuseReport.vue | 74 +++++++++---
packages/frontend/src/pages/admin-user.vue | 3 +-
packages/frontend/src/pages/admin/abuses.vue | 11 +-
.../frontend/src/pages/admin/modlog.ModLog.vue | 5 +
packages/frontend/src/pages/instance-info.vue | 1 +
packages/frontend/src/pages/user/home.vue | 3 +-
packages/frontend/src/store.ts | 4 +
packages/misskey-js/etc/misskey-js.api.md | 16 ++-
packages/misskey-js/src/autogen/apiClientJSDoc.ts | 22 ++++
packages/misskey-js/src/autogen/endpoint.ts | 4 +
packages/misskey-js/src/autogen/entities.ts | 2 +
packages/misskey-js/src/autogen/types.ts | 127 ++++++++++++++++++++-
packages/misskey-js/src/consts.ts | 15 ++-
packages/misskey-js/src/entities.ts | 6 +
29 files changed, 574 insertions(+), 62 deletions(-)
create mode 100644 packages/backend/migration/1728085812127-refine-abuse-user-report.js
create mode 100644 packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
(limited to 'packages/backend/src/models')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a9143ea1b..3fd1b7f899 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@
### General
- Feat: サーバー初期設定時に初期パスワードを設定できるように
+- Feat: 通報にモデレーションノートを残せるように
+- Feat: 通報の解決種別を設定できるように
+- Enhance: 通報の解決と転送を個別に行えるように
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新
- Enhance: l10nの更新
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 1a0547ebc6..d502c5b432 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1834,6 +1834,10 @@ export interface Locale extends ILocale {
* モデレーションノート
*/
"moderationNote": string;
+ /**
+ * モデレーター間でだけ共有されるメモを記入することができます。
+ */
+ "moderationNoteDescription": string;
/**
* モデレーションノートを追加する
*/
@@ -2894,22 +2898,10 @@ export interface Locale extends ILocale {
* 通報元
*/
"reporterOrigin": string;
- /**
- * リモートサーバーに通報を転送する
- */
- "forwardReport": string;
- /**
- * リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。
- */
- "forwardReportIsAnonymous": string;
/**
* 送信
*/
"send": string;
- /**
- * 対応済みにする
- */
- "abuseMarkAsResolved": string;
/**
* 新しいタブで開く
*/
@@ -5170,6 +5162,37 @@ export interface Locale extends ILocale {
* フォロワーへのメッセージ
*/
"messageToFollower": string;
+ /**
+ * 対象
+ */
+ "target": string;
+ "_abuseUserReport": {
+ /**
+ * 転送
+ */
+ "forward": string;
+ /**
+ * 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。
+ */
+ "forwardDescription": string;
+ /**
+ * 解決
+ */
+ "resolve": string;
+ /**
+ * 是認
+ */
+ "accept": string;
+ /**
+ * 否認
+ */
+ "reject": string;
+ /**
+ * 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。
+ * 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。
+ */
+ "resolveTutorial": string;
+ };
"_delivery": {
/**
* 配信状態
@@ -9785,6 +9808,14 @@ export interface Locale extends ILocale {
* 通報を解決
*/
"resolveAbuseReport": string;
+ /**
+ * 通報を転送
+ */
+ "forwardAbuseReport": string;
+ /**
+ * 通報のモデレーションノート更新
+ */
+ "updateAbuseReportNote": string;
/**
* 招待コードを作成
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 92014c8abc..678bc7e66b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -454,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
moderator: "モデレーター"
moderation: "モデレーション"
moderationNote: "モデレーションノート"
+moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿"
@@ -719,10 +720,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
reporter: "通報者"
reporteeOrigin: "通報先"
reporterOrigin: "通報元"
-forwardReport: "リモートサーバーに通報を転送する"
-forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
send: "送信"
-abuseMarkAsResolved: "対応済みにする"
openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション"
@@ -1288,6 +1286,15 @@ unknownWebAuthnKey: "登録されていないパスキーです。"
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
+target: "対象"
+
+_abuseUserReport:
+ forward: "転送"
+ forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
+ resolve: "解決"
+ accept: "是認"
+ reject: "否認"
+ resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
_delivery:
status: "配信状態"
@@ -2593,6 +2600,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決"
+ forwardAbuseReport: "通報を転送"
+ updateAbuseReportNote: "通報のモデレーションノート更新"
createInvitation: "招待コードを作成"
createAd: "広告を作成"
deleteAd: "広告を削除"
diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
new file mode 100644
index 0000000000..57cbfdcf6d
--- /dev/null
+++ b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RefineAbuseUserReport1728085812127 {
+ name = 'RefineAbuseUserReport1728085812127'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
index 69c51509ba..cddfe5eb81 100644
--- a/packages/backend/src/core/AbuseReportService.ts
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -20,8 +20,10 @@ export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
@@ -77,16 +79,16 @@ export class AbuseReportService {
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
- * @param operator 通報を処理したユーザ
+ * @param moderator 通報を処理したユーザ
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
- forward: boolean;
+ resolvedAs: MiAbuseUserReport['resolvedAs'];
}[],
- operator: MiUser,
+ moderator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
@@ -99,25 +101,15 @@ export class AbuseReportService {
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
- assigneeId: operator.id,
- forwarded: ps.forward && report.targetUserHost !== null,
+ assigneeId: moderator.id,
+ resolvedAs: ps.resolvedAs,
});
- if (ps.forward && report.targetUserHost != null) {
- const actor = await this.instanceActorService.getInstanceActor();
- const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
-
- // eslint-disable-next-line
- const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
- const contextAssignedFlag = this.apRendererService.addContext(flag);
- this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
- }
-
this.moderationLogService
- .log(operator, 'resolveAbuseReport', {
+ .log(moderator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
- forwarded: ps.forward && report.targetUserHost !== null,
+ resolvedAs: ps.resolvedAs,
})
.then();
}
@@ -125,4 +117,58 @@ export class AbuseReportService {
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
+
+ @bindThis
+ public async forward(
+ reportId: MiAbuseUserReport['id'],
+ moderator: MiUser,
+ ) {
+ const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+ if (report.targetUserHost == null) {
+ throw new Error('The target user host is null.');
+ }
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ forwarded: true,
+ });
+
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+
+ this.moderationLogService
+ .log(moderator, 'forwardAbuseReport', {
+ reportId: report.id,
+ report: report,
+ })
+ .then();
+ }
+
+ @bindThis
+ public async update(
+ reportId: MiAbuseUserReport['id'],
+ params: {
+ moderationNote?: MiAbuseUserReport['moderationNote'];
+ },
+ moderator: MiUser,
+ ) {
+ const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ moderationNote: params.moderationNote,
+ });
+
+ if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
+ this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
+ reportId: report.id,
+ report: report,
+ before: report.moderationNote,
+ after: params.moderationNote,
+ });
+ }
+ }
}
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 149c753d4c..4c45b95a64 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -35,6 +35,8 @@ function generateAbuseReport(override?: Partial): AbuseUserRe
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
+ resolvedAs: null,
+ moderationNote: 'foo',
...override,
};
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index a13c244c19..70ead890ab 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
+ resolvedAs: report.resolvedAs,
+ moderationNote: report.moderationNote,
});
}
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index 0615fd7eb5..cb5672e4ac 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
})
public resolved: boolean;
+ /**
+ * リモートサーバーに転送したかどうか
+ */
@Column('boolean', {
default: false,
})
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
})
public comment: string;
+ @Column('varchar', {
+ length: 8192, default: '',
+ })
+ public moderationNote: string;
+
+ /**
+ * accept 是認 ... 通報内容が正当であり、肯定的に対応された
+ * reject 否認 ... 通報内容が正当でなく、否定的に対応された
+ * null ... その他
+ */
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public resolvedAs: 'accept' | 'reject' | null;
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 08a0468ab2..3557fa40a5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -453,6 +455,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
+const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
+const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -842,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
@@ -1225,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 2462781f7b..49b07d6ced 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -457,6 +459,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
+ ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
+ ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs],
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.forward(report.id, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 9b79100fcf..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
- forward: { type: 'boolean', default: false },
+ resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
},
required: ['reportId'],
} as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport);
}
- await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
+ await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ moderationNote: { type: 'string' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.update(report.id, {
+ moderationNote: ps.moderationNote,
+ }, me);
+ });
+ }
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 0389143daf..df3cfee171 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -99,6 +99,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
+ 'forwardAbuseReport',
+ 'updateAbuseReportNote',
'createInvitation',
'createAd',
'updateAd',
@@ -267,7 +269,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: {
reportId: string;
report: any;
- forwarded: boolean;
+ forwarded?: boolean;
+ resolvedAs?: string | null;
+ };
+ forwardAbuseReport: {
+ reportId: string;
+ report: any;
+ };
+ updateAbuseReportNote: {
+ reportId: string;
+ report: any;
+ before: string;
+ after: string;
};
createInvitation: {
invitations: any[];
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index 6ce6e47781..c98d199f35 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
});
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
});
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index c9c629046e..2f0e09fc4b 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -6,26 +6,33 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
+
(by )
{{ report.comment }}
-
+
- Target:
+ {{ i18n.ts.target }}:
#{{ report.targetUserId.toUpperCase() }}
@@ -36,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.details }}
-
+
@@ -51,6 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts.moderationNote }}
+ {{ moderationNote.length > 0 ? '...' : i18n.ts.none }}
+
+
+ {{ i18n.ts.moderationNoteDescription }}
+
+
+
+
{{ i18n.ts.moderator }}:
@@ -60,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only