diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f097d16fd..f208661257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ ### General - Feat: コンテンツの表示にログインを必須にできるように - Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように +- Feat: ユーザー登録時に自動で特定のユーザーをフォローすることができるように + - フォローさせるユーザーのフォロー解除・ミュート・ブロックができないように指定することもできます - Enhance: 依存関係の更新 - Enhance: l10nの更新 - Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 ) diff --git a/locales/index.d.ts b/locales/index.d.ts index 0ae188f1f7..26853d7935 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5222,6 +5222,38 @@ export interface Locale extends ILocale { * 注意事項を理解した上でオンにします。 */ "acknowledgeNotesAndEnable": string; + /** + * デフォルトでフォローするユーザー (ID) + */ + "defaultFollowedUsers": string; + /** + * 今後アカウントが作成された際に自動でフォローされるユーザー(解除可能)のユーザーIDを改行区切りで指定します。 + */ + "defaultFollowedUsersDescription": string; + /** + * 交流を断てないユーザー (ID) + */ + "forciblyFollowedUsers": string; + /** + * 今後アカウントが作成された際には自動でフォローされ、フォローの解除やミュート・ブロックができないユーザーのユーザーIDを改行区切りで指定します。 + */ + "forciblyFollowedUsersDescription": string; + /** + * 「デフォルトでフォローするユーザー」と「交流を絶てないユーザー」が重複しています。 + */ + "defaultFollowedUsersDuplicated": string; + /** + * サーバー管理者はこのユーザーをフォロー解除することを禁止しています。 + */ + "unfollowThisUserIsProhibited": string; + /** + * サーバー管理者はこのユーザーをブロックすることを禁止しています。 + */ + "blockThisUserIsProhibited": string; + /** + * サーバー管理者はこのユーザーをミュートすることを禁止しています。 + */ + "muteThisUserIsProhibited": string; "_accountSettings": { /** * コンテンツの表示にログインを必須にする diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1b59708d85..a67e567cfa 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1301,6 +1301,14 @@ lockdown: "ロックダウン" pleaseSelectAccount: "アカウントを選択してください" availableRoles: "利用可能なロール" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" +defaultFollowedUsers: "デフォルトでフォローするユーザー (ID)" +defaultFollowedUsersDescription: "今後アカウントが作成された際に自動でフォローされるユーザー(解除可能)のユーザーIDを改行区切りで指定します。" +forciblyFollowedUsers: "交流を断てないユーザー (ID)" +forciblyFollowedUsersDescription: "今後アカウントが作成された際には自動でフォローされ、フォローの解除やミュート・ブロックができないユーザーのユーザーIDを改行区切りで指定します。" +defaultFollowedUsersDuplicated: "「デフォルトでフォローするユーザー」と「交流を絶てないユーザー」が重複しています。" +unfollowThisUserIsProhibited: "サーバー管理者はこのユーザーをフォロー解除することを禁止しています。" +blockThisUserIsProhibited: "サーバー管理者はこのユーザーをブロックすることを禁止しています。" +muteThisUserIsProhibited: "サーバー管理者はこのユーザーをミュートすることを禁止しています。" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" diff --git a/packages/backend/migration/1728986848483-defaultFollowUsers.js b/packages/backend/migration/1728986848483-defaultFollowUsers.js new file mode 100644 index 0000000000..0a3efd050c --- /dev/null +++ b/packages/backend/migration/1728986848483-defaultFollowUsers.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class DefaultFollowUsers1728986848483 { + name = 'defaultFollowUsers1728986848483' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "defaultFollowedUsers" character varying(1024) array NOT NULL DEFAULT '{}'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "forciblyFollowedUsers" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "forciblyFollowedUsers"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultFollowedUsers"`); + } +} diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 3865392b7f..79bc3ebd1c 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 { UserService } from '@/core/UserService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; @Injectable() export class SignupService { @@ -39,6 +40,7 @@ export class SignupService { private utilityService: UtilityService, private userService: UserService, + private userFollowingService: UserFollowingService, private userEntityService: UserEntityService, private idService: IdService, private instanceActorService: InstanceActorService, @@ -151,6 +153,23 @@ export class SignupService { }); this.usersChart.update(account, true); + + //#region Default following + if ( + !isTheFirstUser && + (this.meta.defaultFollowedUsers.length > 0 || this.meta.forciblyFollowedUsers.length > 0) + ) { + const userIdsToFollow = [ + ...this.meta.defaultFollowedUsers, + ...this.meta.forciblyFollowedUsers, + ]; + + await Promise.allSettled(userIdsToFollow.map(async userId => { + await this.userFollowingService.follow(account, { id: userId }); + })); + } + //#endregion + this.userService.notifySystemWebhook(account, 'userCreated'); return { account, secret }; diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 2f1310b8ef..e18183f628 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 ( + blocker.host == null && + 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..bdf215e8c5 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 { + // フォロー解除できないユーザーの場合 + if ( + follower.host == null && + 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..256d9b4025 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 { + // フォロー解除できない(=ミュートもできない)ユーザーの場合 + if ( + user.host == null && + 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..155c844664 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 { + // フォロー解除できない(=リノートミュートもできない)ユーザーの場合 + if ( + user.host == null && + 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/models/Meta.ts b/packages/backend/src/models/Meta.ts index ad5e31ad6f..e6fda2d5ff 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -61,6 +61,22 @@ export class MiMeta { }) public pinnedUsers: string[]; + /** + * アカウント作成の段階でデフォルトでフォローしているユーザー(あとから解除可能) + */ + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public defaultFollowedUsers: string[]; + + /** + * デフォルトでフォローしていて、フォロー解除・ブロック・ミュートができないユーザー + */ + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public forciblyFollowedUsers: string[]; + @Column('varchar', { length: 1024, array: true, default: '{}', }) 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/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 64e3cc33bd..7c02fa0295 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -149,6 +149,20 @@ export const meta = { type: 'string', }, }, + defaultFollowedUsers: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + forciblyFollowedUsers: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, hiddenTags: { type: 'array', optional: false, nullable: false, @@ -591,6 +605,8 @@ export default class extends Endpoint { // eslint- cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, pinnedUsers: instance.pinnedUsers, + defaultFollowedUsers: instance.defaultFollowedUsers, + forciblyFollowedUsers: instance.forciblyFollowedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, silencedHosts: instance.silencedHosts, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 38ef0d1de8..8b1dd58349 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -3,11 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Injectable, Inject } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; import type { MiMeta } from '@/models/Meta.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['admin'], @@ -15,6 +17,14 @@ export const meta = { requireCredential: true, requireAdmin: true, kind: 'write:admin:meta', + + errors: { + followedUserDuplicated: { + message: 'Some items in "defaultFollowedUsers" and "forciblyFollowedUsers" are duplicated.', + code: 'FOLLOWED_USER_DUPLICATED', + id: 'bcf088ec-fec5-42d0-8b9e-16d3b4797a4d', + }, + }, } as const; export const paramDef = { @@ -26,6 +36,18 @@ export const paramDef = { type: 'string', }, }, + defaultFollowedUsers: { + type: 'array', nullable: true, items: { + type: 'string', + format: 'misskey:id', + }, + }, + forciblyFollowedUsers: { + type: 'array', nullable: true, items: { + type: 'string', + format: 'misskey:id', + }, + }, hiddenTags: { type: 'array', nullable: true, items: { type: 'string', @@ -192,6 +214,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private metaService: MetaService, private moderationLogService: ModerationLogService, ) { @@ -206,6 +231,22 @@ export default class extends Endpoint { // eslint- set.pinnedUsers = ps.pinnedUsers.filter(Boolean); } + if (Array.isArray(ps.defaultFollowedUsers)) { + if (ps.defaultFollowedUsers.some(x => this.serverSettings.forciblyFollowedUsers.includes(x) || ps.forciblyFollowedUsers?.includes(x))) { + throw new ApiError(meta.errors.followedUserDuplicated); + } + + set.defaultFollowedUsers = ps.defaultFollowedUsers.filter(Boolean); + } + + if (Array.isArray(ps.forciblyFollowedUsers)) { + if (ps.forciblyFollowedUsers.some(x => this.serverSettings.defaultFollowedUsers.includes(x) || ps.defaultFollowedUsers?.includes(x))) { + throw new ApiError(meta.errors.followedUserDuplicated); + } + + set.forciblyFollowedUsers = ps.forciblyFollowedUsers.filter(Boolean); + } + if (Array.isArray(ps.hiddenTags)) { set.hiddenTags = ps.hiddenTags.filter(Boolean); } diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 5066215749..30a5652cc3 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -12,6 +12,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.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'], @@ -43,6 +44,13 @@ export const meta = { code: 'ALREADY_BLOCKING', id: '787fed64-acb9-464a-82eb-afbd745b9614', }, + + cannotBlockDueToServerPolicy: { + message: 'You cannot block that user due to server policy.', + code: 'CANNOT_BLOCK_DUE_TO_SERVER_POLICY', + id: 'e2f04d25-0d94-4ac3-a4d8-ba401062741b', + httpStatusCode: 403, + }, }, res: { @@ -83,7 +91,9 @@ export default class extends Endpoint { // 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; }); @@ -99,7 +109,11 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.alreadyBlocking); } - 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 ba146b6703..ac1422c5ff 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -7,6 +7,7 @@ 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 { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; @@ -43,6 +44,13 @@ export const meta = { code: 'NOT_FOLLOWING', id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', }, + + cannotUnfollowDueToServerPolicy: { + message: 'You cannot unfollow that user due to server policy.', + code: 'CANNOT_UNFOLLOW_DUE_TO_SERVER_POLICY', + id: '19f25f61-0141-4683-99dc-217a88d633cb', + httpStatusCode: 403, + }, }, res: { @@ -96,7 +104,11 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.notFollowing); } - 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 e39c133b43..c86d2b8de6 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { ApiError } from '../../error.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; export const meta = { tags: ['account'], @@ -43,6 +44,13 @@ export const meta = { code: 'ALREADY_MUTING', id: '7e7359cb-160c-4956-b08f-4d1c653cd007', }, + + cannotMuteDueToServerPolicy: { + message: 'You cannot mute that user due to server policy.', + code: 'CANNOT_MUTE_DUE_TO_SERVER_POLICY', + id: '15273a89-374d-49fa-8df6-8bb3feeea455', + httpStatusCode: 403, + }, }, } as const; @@ -98,7 +106,11 @@ export default class extends Endpoint { // eslint- 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 84a1f010d4..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,8 +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 { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; +import { UserRenoteMutingService } from '@/core/UserRenoteMutingService.js'; import type { RenoteMutingsRepository } from '@/models/_.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; export const meta = { tags: ['account'], @@ -43,6 +44,13 @@ export const meta = { code: 'ALREADY_MUTING', id: 'ccfecbe4-1f1c-4fc2-8a3d-c3ffee61cb7b', }, + + cannotMuteDueToServerPolicy: { + message: 'You cannot mute that user due to server policy.', + code: 'CANNOT_MUTE_DUE_TO_SERVER_POLICY', + id: '15273a89-374d-49fa-8df6-8bb3feeea455', + httpStatusCode: 403, + }, }, } as const; @@ -90,7 +98,11 @@ export default class extends Endpoint { // eslint- } // 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); + } + }); }); } } diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index c1dc67f776..bc4f4e6e96 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -98,6 +98,14 @@ async function onClick() { await misskeyApi('following/delete', { userId: props.user.id, + }).catch((err) => { + if (err.id === '19f25f61-0141-4683-99dc-217a88d633cb') { + os.alert({ + type: 'error', + title: i18n.ts.permissionDeniedError, + text: i18n.ts.unfollowThisUserIsProhibited, + }); + } }); } else { if (defaultStore.state.alwaysConfirmFollow) { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index ac1fe7783c..0a8723109a 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -36,6 +36,23 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
+ + + + + + + + + {{ i18n.ts.save }} +
+
+ @@ -149,6 +166,8 @@ const prohibitedWords = ref(''); const prohibitedWordsForNameOfUser = ref(''); const hiddenTags = ref(''); const preservedUsernames = ref(''); +const defaultFollowedUsers = ref(''); +const forciblyFollowedUsers = ref(''); const blockedHosts = ref(''); const silencedHosts = ref(''); const mediaSilencedHosts = ref(''); @@ -162,6 +181,8 @@ async function init() { prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); + defaultFollowedUsers.value = meta.defaultFollowedUsers.join('\n'); + forciblyFollowedUsers.value = meta.forciblyFollowedUsers.join('\n'); blockedHosts.value = meta.blockedHosts.join('\n'); silencedHosts.value = meta.silencedHosts?.join('\n') ?? ''; mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); @@ -201,6 +222,19 @@ function save_preservedUsernames() { }); } +function save_defaultUsers() { + os.apiWithDialog('admin/update-meta', { + defaultFollowedUsers: defaultFollowedUsers.value.split('\n'), + forciblyFollowedUsers: forciblyFollowedUsers.value.split('\n'), + }, undefined, { + 'bcf088ec-fec5-42d0-8b9e-16d3b4797a4d': { + text: i18n.ts.defaultFollowedUsersDuplicated, + } + }).then(() => { + fetchInstance(true); + }); +} + function save_sensitiveWords() { os.apiWithDialog('admin/update-meta', { sensitiveWords: sensitiveWords.value.split('\n'), diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index d15279d633..107495210b 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -60,6 +60,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter os.apiWithDialog('mute/create', { userId: user.id, expiresAt, + }, undefined, { + '15273a89-374d-49fa-8df6-8bb3feeea455': { + title: i18n.ts.permissionDeniedError, + text: i18n.ts.muteThisUserIsProhibited, + }, }).then(() => { user.isMuted = true; }); @@ -69,6 +74,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter async function toggleRenoteMute() { os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', { userId: user.id, + }, undefined, { + '15273a89-374d-49fa-8df6-8bb3feeea455': { + title: i18n.ts.permissionDeniedError, + text: i18n.ts.muteThisUserIsProhibited, + }, }).then(() => { user.isRenoteMuted = !user.isRenoteMuted; }); @@ -79,6 +89,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { userId: user.id, + }, undefined, { + 'e2f04d25-0d94-4ac3-a4d8-ba401062741b': { + title: i18n.ts.permissionDeniedError, + text: i18n.ts.blockThisUserIsProhibited, + }, }).then(() => { user.isBlocking = !user.isBlocking; }); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 280abba727..125931623e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5132,6 +5132,8 @@ export type operations = { silencedHosts?: string[]; mediaSilencedHosts: string[]; pinnedUsers: string[]; + defaultFollowedUsers: string[]; + forciblyFollowedUsers: string[]; hiddenTags: string[]; blockedHosts: string[]; sensitiveWords: string[]; @@ -9484,6 +9486,8 @@ export type operations = { 'application/json': { disableRegistration?: boolean | null; pinnedUsers?: string[] | null; + defaultFollowedUsers?: string[] | null; + forciblyFollowedUsers?: string[] | null; hiddenTags?: string[] | null; blockedHosts?: string[] | null; sensitiveWords?: string[] | null;