From a3d77f318567427dce3f73b19d78ba1154c12d13 Mon Sep 17 00:00:00 2001
From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 18 Oct 2024 17:41:51 +0900
Subject: [PATCH] =?UTF-8?q?=E6=93=8D=E4=BD=9C=E3=83=96=E3=83=AD=E3=83=83?=
 =?UTF-8?q?=E3=82=AF=E3=81=AE=E6=8C=99=E5=8B=95=E3=82=92=E3=82=B5=E3=83=BC?=
 =?UTF-8?q?=E3=83=93=E3=82=B9=E5=81=B4=E3=81=AB=E7=A7=BB=E5=8B=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../backend/src/core/UserBlockingService.ts   | 16 +++++++++++++
 .../backend/src/core/UserFollowingService.ts  | 13 +++++++++-
 .../backend/src/core/UserMutingService.ts     | 16 +++++++++++++
 .../src/core/UserRenoteMutingService.ts       | 16 +++++++++++++
 .../ImportMutingProcessorService.ts           |  9 ++++++-
 .../RelationshipProcessorService.ts           | 17 +++++++++++--
 .../server/api/endpoints/blocking/create.ts   | 24 +++++++------------
 .../server/api/endpoints/following/delete.ts  | 20 +++++-----------
 .../src/server/api/endpoints/mute/create.ts   | 20 +++++-----------
 .../api/endpoints/renote-mute/create.ts       | 20 +++++-----------
 10 files changed, 110 insertions(+), 61 deletions(-)

diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 2f1310b8ef..ca57857eb1 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -8,6 +8,8 @@ import { ModuleRef } from '@nestjs/core';
 import { IdService } from '@/core/IdService.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiBlocking } from '@/models/Blocking.js';
+import type { MiMeta } from '@/models/Meta.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 import { QueueService } from '@/core/QueueService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
@@ -20,6 +22,7 @@ import { UserWebhookService } from '@/core/UserWebhookService.js';
 import { bindThis } from '@/decorators.js';
 import { CacheService } from '@/core/CacheService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
+import { RoleService } from '@/core/RoleService.js';
 
 @Injectable()
 export class UserBlockingService implements OnModuleInit {
@@ -29,6 +32,9 @@ export class UserBlockingService implements OnModuleInit {
 	constructor(
 		private moduleRef: ModuleRef,
 
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.followRequestsRepository)
 		private followRequestsRepository: FollowRequestsRepository,
 
@@ -41,6 +47,7 @@ export class UserBlockingService implements OnModuleInit {
 		@Inject(DI.userListMembershipsRepository)
 		private userListMembershipsRepository: UserListMembershipsRepository,
 
+		private roleService: RoleService,
 		private cacheService: CacheService,
 		private userEntityService: UserEntityService,
 		private idService: IdService,
@@ -59,6 +66,15 @@ export class UserBlockingService implements OnModuleInit {
 
 	@bindThis
 	public async block(blocker: MiUser, blockee: MiUser, silent = false) {
+
+		// フォロー解除できない(=ブロックもできない)ユーザーの場合
+		if (
+			this.serverSettings.forciblyFollowedUsers.includes(blockee.id) &&
+			!await this.roleService.isModerator(blocker)
+		) {
+			throw new IdentifiableError('e2f04d25-0d94-4ac3-a4d8-ba401062741b', 'You cannot block that user due to server policy.');
+		}
+
 		await Promise.all([
 			this.cancelRequest(blocker, blockee, silent),
 			this.cancelRequest(blockee, blocker, silent),
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 8963003057..5e980f27af 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -27,6 +27,7 @@ import { CacheService } from '@/core/CacheService.js';
 import type { Config } from '@/config.js';
 import { AccountMoveService } from '@/core/AccountMoveService.js';
 import { UtilityService } from '@/core/UtilityService.js';
+import { RoleService } from '@/core/RoleService.js';
 import type { ThinUser } from '@/queue/types.js';
 import Logger from '../logger.js';
 
@@ -73,6 +74,7 @@ export class UserFollowingService implements OnModuleInit {
 		@Inject(DI.instancesRepository)
 		private instancesRepository: InstancesRepository,
 
+		private roleService: RoleService,
 		private cacheService: CacheService,
 		private utilityService: UtilityService,
 		private userEntityService: UserEntityService,
@@ -365,13 +367,22 @@ export class UserFollowingService implements OnModuleInit {
 	@bindThis
 	public async unfollow(
 		follower: {
-			id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
+			id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; isRoot: MiUser['isRoot']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
 		},
 		followee: {
 			id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
 		},
 		silent = false,
 	): Promise<void> {
+
+		// フォロー解除できないユーザーの場合
+		if (
+			this.meta.forciblyFollowedUsers.includes(followee.id) &&
+			!await this.roleService.isModerator(follower)
+		) {
+			throw new IdentifiableError('19f25f61-0141-4683-99dc-217a88d633cb', 'You cannot unfollow that user due to server policy.');
+		}
+
 		const following = await this.followingsRepository.findOne({
 			relations: {
 				follower: true,
diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts
index 06643be5fb..504b7e0e1b 100644
--- a/packages/backend/src/core/UserMutingService.ts
+++ b/packages/backend/src/core/UserMutingService.ts
@@ -5,19 +5,26 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { In } from 'typeorm';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 import type { MutingsRepository, MiMuting } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
 import type { MiUser } from '@/models/User.js';
+import type { MiMeta } from '@/models/Meta.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
 import { CacheService } from '@/core/CacheService.js';
 
 @Injectable()
 export class UserMutingService {
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.mutingsRepository)
 		private mutingsRepository: MutingsRepository,
 
+		private roleService: RoleService,
 		private idService: IdService,
 		private cacheService: CacheService,
 	) {
@@ -25,6 +32,15 @@ export class UserMutingService {
 
 	@bindThis
 	public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
+
+		// フォロー解除できない(=ミュートもできない)ユーザーの場合
+		if (
+			this.serverSettings.forciblyFollowedUsers.includes(target.id) &&
+			!await this.roleService.isModerator(user)
+		) {
+			throw new IdentifiableError('15273a89-374d-49fa-8df6-8bb3feeea455', 'You cannot mute that user due to server policy.');
+		}
+
 		await this.mutingsRepository.insert({
 			id: this.idService.gen(),
 			expiresAt: expiresAt ?? null,
diff --git a/packages/backend/src/core/UserRenoteMutingService.ts b/packages/backend/src/core/UserRenoteMutingService.ts
index bdc5e23f4b..1f5c311e3b 100644
--- a/packages/backend/src/core/UserRenoteMutingService.ts
+++ b/packages/backend/src/core/UserRenoteMutingService.ts
@@ -10,16 +10,23 @@ import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
 
 import { IdService } from '@/core/IdService.js';
 import type { MiUser } from '@/models/User.js';
+import type { MiMeta } from '@/models/Meta.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
 import { CacheService } from '@/core/CacheService.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 @Injectable()
 export class UserRenoteMutingService {
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.renoteMutingsRepository)
 		private renoteMutingsRepository: RenoteMutingsRepository,
 
+		private roleService: RoleService,
 		private idService: IdService,
 		private cacheService: CacheService,
 	) {
@@ -27,6 +34,15 @@ export class UserRenoteMutingService {
 
 	@bindThis
 	public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
+
+		// フォロー解除できない(=リノートミュートもできない)ユーザーの場合
+		if (
+			this.serverSettings.forciblyFollowedUsers.includes(target.id) &&
+			!await this.roleService.isModerator(user)
+		) {
+			throw new IdentifiableError('15273a89-374d-49fa-8df6-8bb3feeea455', 'You cannot mute that user due to server policy.');
+		}
+
 		await this.renoteMutingsRepository.insert({
 			id: this.idService.gen(),
 			muterId: user.id,
diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts
index ec9d2b6c4c..0ae1e6ae15 100644
--- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts
@@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
 import type { DbUserImportJobData } from '../types.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 @Injectable()
 export class ImportMutingProcessorService {
@@ -90,7 +91,13 @@ export class ImportMutingProcessorService {
 
 				this.logger.info(`Mute[${linenum}] ${target.id} ...`);
 
-				await this.userMutingService.mute(user, target);
+				await this.userMutingService.mute(user, target).catch((err) => {
+					if (err instanceof IdentifiableError && err.id === '15273a89-374d-49fa-8df6-8bb3feeea455') {
+						// フォロー解除できない(=ミュートもできない)ユーザー。動作は正常のため、エラーを無視する
+						return;
+					}
+					throw err;
+				});
 			} catch (e) {
 				this.logger.warn(`Error in line:${linenum} ${e}`);
 			}
diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts
index 408b02fb38..16ee99a2e0 100644
--- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts
+++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts
@@ -16,6 +16,7 @@ import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
 import { RelationshipJobData } from '../types.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 @Injectable()
 export class RelationshipProcessorService {
@@ -50,7 +51,13 @@ export class RelationshipProcessorService {
 			this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
 			this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
 		]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
-		await this.userFollowingService.unfollow(follower, followee, job.data.silent);
+		await this.userFollowingService.unfollow(follower, followee, job.data.silent).catch((err) => {
+			if (err instanceof IdentifiableError && err.id === '19f25f61-0141-4683-99dc-217a88d633cb') {
+				// フォロー解除できないユーザー。動作は正常のため、エラーを無視する
+				return;
+			}
+			throw err;
+		});
 		return 'ok';
 	}
 
@@ -61,7 +68,13 @@ export class RelationshipProcessorService {
 			this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
 			this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
 		]);
-		await this.userBlockingService.block(blockee, blocker, job.data.silent);
+		await this.userBlockingService.block(blockee, blocker, job.data.silent).catch((err) => {
+			if (err instanceof IdentifiableError && err.id === 'e2f04d25-0d94-4ac3-a4d8-ba401062741b') {
+				// フォロー解除できない(=ブロックもできない)ユーザー。動作は正常のため、エラーを無視する
+				return;
+			}
+			throw err;
+		});
 		return 'ok';
 	}
 
diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts
index 087140e4fa..30a5652cc3 100644
--- a/packages/backend/src/server/api/endpoints/blocking/create.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/create.ts
@@ -7,13 +7,12 @@ import ms from 'ms';
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
-import type { MiMeta } from '@/models/Meta.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { UserBlockingService } from '@/core/UserBlockingService.js';
-import { RoleService } from '@/core/RoleService.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 export const meta = {
 	tags: ['account'],
@@ -72,16 +71,12 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.meta)
-		private serverSettings: MiMeta,
-
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
 		@Inject(DI.blockingsRepository)
 		private blockingsRepository: BlockingsRepository,
 
-		private roleService: RoleService,
 		private userEntityService: UserEntityService,
 		private getterService: GetterService,
 		private userBlockingService: UserBlockingService,
@@ -96,7 +91,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Get blockee
 			const blockee = await this.getterService.getUser(ps.userId).catch(err => {
-				if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+				if (err instanceof IdentifiableError && err.id === '15348ddd-432d-49c2-8a5a-8069753becff') {
+					throw new ApiError(meta.errors.noSuchUser);
+				}
 				throw err;
 			});
 
@@ -112,14 +109,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.alreadyBlocking);
 			}
 
-			if (
-				this.serverSettings.forciblyFollowedUsers.includes(blockee.id) &&
-				!await this.roleService.isModerator(blocker)
-			) {
-				throw new ApiError(meta.errors.cannotBlockDueToServerPolicy);
-			}
-
-			await this.userBlockingService.block(blocker, blockee);
+			await this.userBlockingService.block(blocker, blockee).catch((err) => {
+				if (err instanceof IdentifiableError && err.id === meta.errors.cannotBlockDueToServerPolicy.id) {
+					throw new ApiError(meta.errors.cannotBlockDueToServerPolicy);
+				}
+			});
 
 			return await this.userEntityService.pack(blockee.id, blocker, {
 				schema: 'UserDetailedNotMe',
diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts
index 1709fb8e4c..ac1422c5ff 100644
--- a/packages/backend/src/server/api/endpoints/following/delete.ts
+++ b/packages/backend/src/server/api/endpoints/following/delete.ts
@@ -7,10 +7,9 @@ import ms from 'ms';
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { FollowingsRepository } from '@/models/_.js';
-import type { MiMeta } from '@/models/Meta.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
-import { RoleService } from '@/core/RoleService.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
@@ -72,13 +71,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.meta)
-		private serverSettings: MiMeta,
-
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
 
-		private roleService: RoleService,
 		private userEntityService: UserEntityService,
 		private getterService: GetterService,
 		private userFollowingService: UserFollowingService,
@@ -109,14 +104,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.notFollowing);
 			}
 
-			if (
-				this.serverSettings.forciblyFollowedUsers.includes(followee.id) &&
-				!await this.roleService.isModerator(follower)
-			) {
-				throw new ApiError(meta.errors.cannotUnfollowDueToServerPolicy);
-			}
-
-			await this.userFollowingService.unfollow(follower, followee);
+			await this.userFollowingService.unfollow(follower, followee).catch((err) => {
+				if (err instanceof IdentifiableError && err.id === meta.errors.cannotUnfollowDueToServerPolicy.id) {
+					throw new ApiError(meta.errors.cannotUnfollowDueToServerPolicy);
+				}
+			});
 
 			return await this.userEntityService.pack(followee.id, me);
 		});
diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts
index 69762610fa..c86d2b8de6 100644
--- a/packages/backend/src/server/api/endpoints/mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/mute/create.ts
@@ -7,12 +7,11 @@ import { Inject, Injectable } from '@nestjs/common';
 import ms from 'ms';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { MutingsRepository } from '@/models/_.js';
-import type { MiMeta } from '@/models/Meta.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { UserMutingService } from '@/core/UserMutingService.js';
-import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 export const meta = {
 	tags: ['account'],
@@ -71,13 +70,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.meta)
-		private serverSettings: MiMeta,
-
 		@Inject(DI.mutingsRepository)
 		private mutingsRepository: MutingsRepository,
 
-		private roleService: RoleService,
 		private getterService: GetterService,
 		private userMutingService: UserMutingService,
 	) {
@@ -107,18 +102,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.alreadyMuting);
 			}
 
-			if (
-				this.serverSettings.forciblyFollowedUsers.includes(mutee.id) &&
-				!await this.roleService.isModerator(muter)
-			) {
-				throw new ApiError(meta.errors.cannotMuteDueToServerPolicy);
-			}
-
 			if (ps.expiresAt && ps.expiresAt <= Date.now()) {
 				return;
 			}
 
-			await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
+			await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null).catch((err) => {
+				if (err instanceof IdentifiableError && err.id === meta.errors.cannotMuteDueToServerPolicy.id) {
+					throw new ApiError(meta.errors.cannotMuteDueToServerPolicy);
+				}
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
index 709f1b170f..40eea71cad 100644
--- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
@@ -9,10 +9,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
-import { RoleService } from '@/core/RoleService.js';
 import { UserRenoteMutingService } from '@/core/UserRenoteMutingService.js';
 import type { RenoteMutingsRepository } from '@/models/_.js';
-import type { MiMeta } from '@/models/Meta.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 export const meta = {
 	tags: ['account'],
@@ -66,13 +65,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.meta)
-		private serverSettings: MiMeta,
-
 		@Inject(DI.renoteMutingsRepository)
 		private renoteMutingsRepository: RenoteMutingsRepository,
 
-		private roleService: RoleService,
 		private getterService: GetterService,
 		private userRenoteMutingService: UserRenoteMutingService,
 	) {
@@ -102,15 +97,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.alreadyMuting);
 			}
 
-			if (
-				this.serverSettings.forciblyFollowedUsers.includes(mutee.id) &&
-				!await this.roleService.isModerator(muter)
-			) {
-				throw new ApiError(meta.errors.cannotMuteDueToServerPolicy);
-			}
-
 			// Create mute
-			await this.userRenoteMutingService.mute(muter, mutee);
+			await this.userRenoteMutingService.mute(muter, mutee).catch((err) => {
+				if (err instanceof IdentifiableError && err.id === meta.errors.cannotMuteDueToServerPolicy.id) {
+					throw new ApiError(meta.errors.cannotMuteDueToServerPolicy);
+				}
+			});
 		});
 	}
 }