From 51bc83c3e772f3d7debedf0dfa779e54c50fd9b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B4=87=E5=B3=B0=20=E6=9C=94=E8=8F=AF?= Date: Sun, 17 Nov 2024 21:50:02 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=E7=89=B9=E5=AE=9A=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=8B=E3=82=89=E3=81=AE=E3=83=AA=E3=82=A2?= =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E3=83=96=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=81=99=E3=82=8B=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BlockingReactionUserエンティティの追加 - blocking-reaction-userエンドポイントの追加 - 関連するフロントエンドの追加 Migrationがあります。 --- locales/en-US.yml | 5 + locales/index.d.ts | 20 ++++ locales/ja-JP.yml | 5 + locales/ja-KS.yml | 5 + .../1731566099974-addBlockingReactionUser.js | 34 ++++++ .../src/core/BlockingReactionUserService.ts | 110 +++++++++++++++++ packages/backend/src/core/CacheService.ts | 33 +++++- packages/backend/src/core/CoreModule.ts | 11 ++ .../backend/src/core/GlobalEventService.ts | 2 + packages/backend/src/core/ReactionService.ts | 5 +- .../BlockingReactionUserEntityService.ts | 58 +++++++++ .../src/core/entities/UserEntityService.ts | 38 ++++++ packages/backend/src/di-symbols.ts | 1 + .../src/models/BlockingReactionUser.ts | 41 +++++++ .../backend/src/models/RepositoryModule.ts | 9 ++ packages/backend/src/models/_.ts | 3 + packages/backend/src/postgres.ts | 2 + .../backend/src/server/api/EndpointsModule.ts | 12 ++ packages/backend/src/server/api/endpoints.ts | 6 + .../blocking-reaction-user/create.ts | 110 +++++++++++++++++ .../blocking-reaction-user/delete.ts | 111 ++++++++++++++++++ .../endpoints/blocking-reaction-user/list.ts | 61 ++++++++++ .../src/pages/settings/mute-block.vue | 48 ++++++++ .../frontend/src/scripts/get-user-menu.ts | 14 +++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 35 +++++- packages/misskey-js/src/autogen/endpoint.ts | 9 ++ packages/misskey-js/src/autogen/entities.ts | 6 + packages/misskey-js/src/autogen/models.ts | 1 + packages/misskey-js/src/autogen/types.ts | 6 +- 29 files changed, 796 insertions(+), 5 deletions(-) create mode 100644 packages/backend/migration/1731566099974-addBlockingReactionUser.js create mode 100644 packages/backend/src/core/BlockingReactionUserService.ts create mode 100644 packages/backend/src/core/entities/BlockingReactionUserEntityService.ts create mode 100644 packages/backend/src/models/BlockingReactionUser.ts create mode 100644 packages/backend/src/server/api/endpoints/blocking-reaction-user/create.ts create mode 100644 packages/backend/src/server/api/endpoints/blocking-reaction-user/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/blocking-reaction-user/list.ts diff --git a/locales/en-US.yml b/locales/en-US.yml index 6703d1cf3a..7e4bab06ad 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -147,10 +147,14 @@ renoteMute: "Mute Renotes" renoteUnmute: "Unmute Renotes" block: "Block" unblock: "Unblock" +blockReactionUser: "Reaction Block" +unblockReactionUser: "Unblock Reaction" suspend: "Suspend" unsuspend: "Unsuspend" blockConfirm: "Are you sure that you want to block this account?" unblockConfirm: "Are you sure that you want to unblock this account?" +blockReactionUserConfirm: "Are you sure that you want to block reactions from this account?" +unblockReactionUserConfirm: "Are you sure that you want to unblock reactions from this account?" suspendConfirm: "Are you sure that you want to suspend this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?" selectList: "Select a list" @@ -244,6 +248,7 @@ federationAllowedHostsDescription: "Specify the hostnames of the servers you wan muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" +reactionBlockedUsers: "Reaction blocked users" noUsers: "There are no users" editProfile: "Edit profile" noteDeleteConfirm: "Are you sure you want to delete this note?" diff --git a/locales/index.d.ts b/locales/index.d.ts index 24613419ce..b52c313208 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -606,6 +606,14 @@ export interface Locale extends ILocale { * ブロック解除 */ "unblock": string; + /** + * リアクションをブロック + */ + "blockReactionUser": string; + /** + * リアクションのブロックを解除 + */ + "unblockReactionUser": string; /** * 凍結 */ @@ -622,6 +630,14 @@ export interface Locale extends ILocale { * ブロック解除しますか? */ "unblockConfirm": string; + /** + * リアクションをブロックしますか? + */ + "blockReactionUserConfirm": string; + /** + * リアクションのブロックを解除しますか? + */ + "unblockReactionUserConfirm": string; /** * 凍結しますか? */ @@ -994,6 +1010,10 @@ export interface Locale extends ILocale { * ブロックしたユーザー */ "blockedUsers": string; + /** + * リアクションをブロックしたユーザー + */ + "reactionBlockedUsers": string; /** * ユーザーはいません */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9f32969a79..4c48ed3c3d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -147,10 +147,14 @@ renoteMute: "リノートをミュート" renoteUnmute: "リノートのミュートを解除" block: "ブロック" unblock: "ブロック解除" +blockReactionUser: "リアクションをブロック" +unblockReactionUser: "リアクションのブロックを解除" suspend: "凍結" unsuspend: "解凍" blockConfirm: "ブロックしますか?" unblockConfirm: "ブロック解除しますか?" +blockReactionUserConfirm: "リアクションをブロックしますか?" +unblockReactionUserConfirm: "リアクションのブロックを解除しますか?" suspendConfirm: "凍結しますか?" unsuspendConfirm: "解凍しますか?" selectList: "リストを選択" @@ -244,6 +248,7 @@ federationAllowedHostsDescription: "連合を許可するサーバーのホス muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" +reactionBlockedUsers: "リアクションをブロックしたユーザー" noUsers: "ユーザーはいません" editProfile: "プロフィールを編集" noteDeleteConfirm: "このノートを削除しますか?" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 50132c0645..b147b1ed4d 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -147,10 +147,14 @@ renoteMute: "リノートは見いひん" renoteUnmute: "リノートもやっぱ見るわ" block: "ブロック" unblock: "ブロックやめたる" +blockReactionUser: "リアクションをブロックしたる" +unblockReactionUser: "リアクションのブロックをやめたる" suspend: "凍結" unsuspend: "溶かす" blockConfirm: "ブロックしてもええんか?" unblockConfirm: "ブロックやめたるってほんまか?" +blockReactionUserConfirm: "リアクションをブロックしてもええんか?" +unblockReactionUserConfirm: "リアクションのブロックをやめたるってほんまか?" suspendConfirm: "凍結してしもうてええか?" unsuspendConfirm: "解凍するけどええか?" selectList: "リストを選ぶ" @@ -244,6 +248,7 @@ federationAllowedHostsDescription: "連合してもいいサーバーのホス muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしとるユーザー" blockedUsers: "ブロックしとるユーザー" +reactionBlockedUsers: "リアクションブロックしとるユーザー" noUsers: "ユーザーはおらん" editProfile: "プロフィールをいじる" noteDeleteConfirm: "このノートをほかしてええか?" diff --git a/packages/backend/migration/1731566099974-addBlockingReactionUser.js b/packages/backend/migration/1731566099974-addBlockingReactionUser.js new file mode 100644 index 0000000000..7e06b2311f --- /dev/null +++ b/packages/backend/migration/1731566099974-addBlockingReactionUser.js @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: sakuhanight and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MakeNotesHiddenBefore1729486255072 { + name = 'AddBlockingReactionUser1731566099974' + async up(queryRunner) { + await queryRunner.query(` + CREATE TABLE "blocking_reaction_user"( + id varchar(32) NULL, + blockeeId varchar(32) NULL, + blockerId varchar(32) NULL, + CONSTRAINT "PK_blocking_reaction_user" PRIMARY KEY (id), + CONSTRAINT "FK_blocking_reaction_user_blockeeid" FOREIGN KEY (blockeeid) REFERENCES "user" (id) ON DELETE CASCADE, + CONSTRAINT "FK_blocking_reaction_user_blockerid" FOREIGN KEY (blockerid) REFERENCES "user" (id) ON DELETE CASCADE); + `); + await queryRunner.query(`CREATE INDEX "IDX_blocking_reaction_user_id" ON "blocking_reaction_user" (id);`); + await queryRunner.query(`CREATE INDEX "IDX_blocking_reaction_user_blockeeid" ON "blocking_reaction_user" (blockeeid);`); + await queryRunner.query(`CREATE INDEX "IDX_blocking_reaction_user_blockerid" ON "blocking_reaction_user" (blockerid);`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_blocking_reaction_user_blockeeid_blockerid" ON "blocking_reaction_user" (blockeeid, blockerid);`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "IDX_blocking_reaction_user_blockeeid_blockerid";`); + await queryRunner.query(`DROP INDEX "IDX_blocking_reaction_user_blockerid";`); + await queryRunner.query(`DROP INDEX "IDX_blocking_reaction_user_blockeeid";`); + await queryRunner.query(`DROP INDEX "IDX_blocking_reaction_user_id";`); + await queryRunner.query(`ALTER TABLE "blocking_reaction_user" DROP CONSTRAINT "FK_blocking_reaction_user_blockerid";`); + await queryRunner.query(`ALTER TABLE "blocking_reaction_user" DROP CONSTRAINT "FK_blocking_reaction_user_blockeeid";`); + await queryRunner.query(`ALTER TABLE "blocking_reaction_user" DROP CONSTRAINT "PK_blocking_reaction_user";`); + await queryRunner.query(`DROP TABLE "blocking_reaction_user";`); + } +} diff --git a/packages/backend/src/core/BlockingReactionUserService.ts b/packages/backend/src/core/BlockingReactionUserService.ts new file mode 100644 index 0000000000..c20a7617a6 --- /dev/null +++ b/packages/backend/src/core/BlockingReactionUserService.ts @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { IdService } from '@/core/IdService.js'; +import type { MiUser } from '@/models/User.js'; +import { QueueService } from '@/core/QueueService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import type { + FollowRequestsRepository, + BlockingsRepository, + UserListsRepository, + UserListMembershipsRepository, + BlockingReactionUsersRepository +} from '@/models/_.js'; +import Logger from '@/logger.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { bindThis } from '@/decorators.js'; +import { CacheService } from '@/core/CacheService.js'; +import {MiBlockingReactionUser} from "@/models/_.js"; + +@Injectable() +export class BlockingReactionUserService implements OnModuleInit { + private logger: Logger; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.blockingReactionUsersRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + + private cacheService: CacheService, + private userEntityService: UserEntityService, + private idService: IdService, + private queueService: QueueService, + private globalEventService: GlobalEventService, + private webhookService: UserWebhookService, + private apRendererService: ApRendererService, + private loggerService: LoggerService, + ) { + this.logger = this.loggerService.getLogger('user-block'); + } + + onModuleInit() { + } + + @bindThis + public async block(blocker: MiUser, blockee: MiUser, silent = false) { + await Promise.all([ + ]); + + const blocking = { + id: this.idService.gen(), + blocker, + blockerId: blocker.id, + blockee, + blockeeId: blockee.id, + } as MiBlockingReactionUser; + + await this.blockingReactionUsersRepository.insert(blocking); + + this.cacheService.blockingReactionUserCache.refresh(blocker.id); + this.cacheService.blockedReactionUserCache.refresh(blockee.id); + + this.globalEventService.publishInternalEvent('blockingReactionUserCreated', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + } + + @bindThis + public async unblock(blocker: MiUser, blockee: MiUser) { + const blocking = await this.blockingReactionUsersRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, + }); + + if (blocking == null) { + this.logger.warn('ブロック解除がリクエストされましたがブロックしていませんでした'); + return; + } + + // Since we already have the blocker and blockee, we do not need to fetch + // them in the query above and can just manually insert them here. + blocking.blocker = blocker; + blocking.blockee = blockee; + + await this.blockingReactionUsersRepository.delete(blocking.id); + + this.cacheService.blockingReactionUserCache.refresh(blocker.id); + this.cacheService.blockedReactionUserCache.refresh(blockee.id); + + this.globalEventService.publishInternalEvent('blockingReactionUserDeleted', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + } + + @bindThis + public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise { + return (await this.cacheService.blockingReactionUserCache.fetch(blockerId)).has(blockeeId); + } +} diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 6725ebe75b..fffbe84b7b 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -5,7 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; +import type { + BlockingsRepository, + FollowingsRepository, + MutingsRepository, + RenoteMutingsRepository, + MiUserProfile, + UserProfilesRepository, + UsersRepository, + MiFollowing, + BlockingReactionUsersRepository +} from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; @@ -24,6 +34,8 @@ export class CacheService implements OnApplicationShutdown { public userMutingsCache: RedisKVCache>; public userBlockingCache: RedisKVCache>; public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ + public blockingReactionUserCache: RedisKVCache>; // NOTE: リアクションブロックするユーザーのキャッシュ + public blockedReactionUserCache: RedisKVCache>; // NOTE: リアクションブロックされるユーザーのキャッシュ public renoteMutingsCache: RedisKVCache>; public userFollowingsCache: RedisKVCache | undefined>>; @@ -46,6 +58,9 @@ export class CacheService implements OnApplicationShutdown { @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, + @Inject(DI.blockingReactionUsersRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, @@ -93,6 +108,22 @@ export class CacheService implements OnApplicationShutdown { fromRedisConverter: (value) => new Set(JSON.parse(value)), }); + this.blockingReactionUserCache = new RedisKVCache>(this.redisClient, 'blockingReactionUser', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.blockingReactionUsersRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + + this.blockedReactionUserCache = new RedisKVCache>(this.redisClient, 'blockedReactionUser', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.blockingReactionUsersRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + this.renoteMutingsCache = new RedisKVCache>(this.redisClient, 'renoteMutings', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 734d135648..89486f5350 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -15,6 +15,8 @@ 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 { BlockingReactionUserEntityService } from '@/core/entities/BlockingReactionUserEntityService.js'; +import { BlockingReactionUserService } from '@/core/BlockingReactionUserService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; @@ -166,6 +168,7 @@ const $AntennaService: Provider = { provide: 'AntennaService', useExisting: Ante const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService }; +const $BlockingReactionUserService: Provider = { provide: 'BlockingReactionUserService', useExisting: BlockingReactionUserService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; @@ -250,6 +253,7 @@ const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useEx const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService }; const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService }; const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService }; +const $BlockingReactionUserEntityService: Provider = { provide: 'BlockingReactionUserEntityService', useExisting: BlockingReactionUserEntityService }; const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService }; const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService }; const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; @@ -317,6 +321,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppLockService, AchievementService, AvatarDecorationService, + BlockingReactionUserService, CaptchaService, CreateSystemUserService, CustomEmojiService, @@ -401,6 +406,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppEntityService, AuthSessionEntityService, BlockingEntityService, + BlockingReactionUserEntityService, ChannelEntityService, ClipEntityService, DriveFileEntityService, @@ -464,6 +470,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppLockService, $AchievementService, $AvatarDecorationService, + $BlockingReactionUserService, $CaptchaService, $CreateSystemUserService, $CustomEmojiService, @@ -548,6 +555,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppEntityService, $AuthSessionEntityService, $BlockingEntityService, + $BlockingReactionUserEntityService, $ChannelEntityService, $ClipEntityService, $DriveFileEntityService, @@ -612,6 +620,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppLockService, AchievementService, AvatarDecorationService, + BlockingReactionUserService, CaptchaService, CreateSystemUserService, CustomEmojiService, @@ -695,6 +704,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppEntityService, AuthSessionEntityService, BlockingEntityService, + BlockingReactionUserEntityService, ChannelEntityService, ClipEntityService, DriveFileEntityService, @@ -840,6 +850,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppEntityService, $AuthSessionEntityService, $BlockingEntityService, + $BlockingReactionUserEntityService, $ChannelEntityService, $ClipEntityService, $DriveFileEntityService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 03646ff566..7e44f544c0 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -223,6 +223,8 @@ export interface InternalEventTypes { unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; + blockingReactionUserCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; + blockingReactionUserDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; policiesUpdated: MiRole['policies']; roleCreated: MiRole; roleDeleted: MiRole; diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 6f9fe53937..72c027233a 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -30,6 +30,7 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; +import { BlockingReactionUserService } from '@/core/BlockingReactionUserService.js'; const FALLBACK = '\u2764'; @@ -91,6 +92,7 @@ export class ReactionService { private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, + private blockingReactionUserService: BlockingReactionUserService, private reactionsBufferingService: ReactionsBufferingService, private idService: IdService, private featuredService: FeaturedService, @@ -107,7 +109,8 @@ export class ReactionService { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); - if (blocked) { + const reactionBlocked = await this.blockingReactionUserService.checkBlocked(note.userId, user.id); + if (blocked || reactionBlocked) { throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); } } diff --git a/packages/backend/src/core/entities/BlockingReactionUserEntityService.ts b/packages/backend/src/core/entities/BlockingReactionUserEntityService.ts new file mode 100644 index 0000000000..1219f9c64c --- /dev/null +++ b/packages/backend/src/core/entities/BlockingReactionUserEntityService.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: sakuhanight and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { BlockingReactionUsersRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { MiUser } from '@/models/User.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { MiBlockingReactionUser } from '@/models/BlockingReactionUser.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class BlockingReactionUserEntityService { + constructor( + @Inject(DI.blockingReactionUsersRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async pack( + src: MiBlockingReactionUser['id'] | MiBlockingReactionUser, + me?: { id: MiUser['id'] } | null | undefined, + hint?: { + blockee?: Packed<'UserDetailedNotMe'>, + }, + ): Promise> { + const blocking = typeof src === 'object' ? src : await this.blockingReactionUsersRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: blocking.id, + createdAt: this.idService.parse(blocking.id).date.toISOString(), + blockeeId: blocking.blockeeId, + blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, { + schema: 'UserDetailedNotMe', + }), + }); + } + + @bindThis + public async packMany( + blockings: MiBlockingReactionUser[], + me: { id: MiUser['id'] }, + ) { + const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId); + const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' }) + .then(users => new Map(users.map(u => [u.id, u]))); + return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) }))); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index d3c087a153..50c6f321da 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -25,6 +25,7 @@ import { } from '@/models/User.js'; import type { BlockingsRepository, + BlockingReactionUsersRepository, FollowingsRepository, FollowRequestsRepository, MiFollowing, @@ -76,6 +77,8 @@ export type UserRelation = { hasPendingFollowRequestToYou: boolean isBlocking: boolean isBlocked: boolean + isReactionBlocking: boolean + isReactionBlocked: boolean isMuted: boolean isRenoteMuted: boolean } @@ -116,6 +119,9 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, + @Inject(DI.blockingReactionUsersRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -169,6 +175,8 @@ export class UserEntityService implements OnModuleInit { hasPendingFollowRequestToYou, isBlocking, isBlocked, + isReactionBlocking, + isReactionBlocked, isMuted, isRenoteMuted, ] = await Promise.all([ @@ -206,6 +214,18 @@ export class UserEntityService implements OnModuleInit { blockeeId: me, }, }), + this.blockingReactionUsersRepository.exists({ + where: { + blockerId: me, + blockeeId: target, + }, + }), + this.blockingReactionUsersRepository.exists({ + where: { + blockerId: target, + blockeeId: me, + }, + }), this.mutingsRepository.exists({ where: { muterId: me, @@ -229,6 +249,8 @@ export class UserEntityService implements OnModuleInit { hasPendingFollowRequestToYou, isBlocking, isBlocked, + isReactionBlocking, + isReactionBlocked, isMuted, isRenoteMuted, }; @@ -243,6 +265,8 @@ export class UserEntityService implements OnModuleInit { followeesRequests, blockers, blockees, + reactionBlockers, + reactionBlockees, muters, renoteMuters, ] = await Promise.all([ @@ -273,6 +297,16 @@ export class UserEntityService implements OnModuleInit { .where('b.blockeeId = :me', { me }) .getRawMany<{ b_blockerId: string }>() .then(it => it.map(it => it.b_blockerId)), + this.blockingReactionUsersRepository.createQueryBuilder('bru') + .select('bru.blockeeId') + .where('bru.blockerId = :me', { me }) + .getRawMany<{ bru_blockeeId: string }>() + .then(it => it.map(it => it.bru_blockeeId)), + this.blockingReactionUsersRepository.createQueryBuilder('bru') + .select('bru.blockerId') + .where('bru.blockeeId = :me', { me }) + .getRawMany<{ bru_blockerId: string }>() + .then(it => it.map(it => it.bru_blockerId)), this.mutingsRepository.createQueryBuilder('m') .select('m.muteeId') .where('m.muterId = :me', { me }) @@ -300,6 +334,8 @@ export class UserEntityService implements OnModuleInit { hasPendingFollowRequestToYou: followeesRequests.includes(target), isBlocking: blockers.includes(target), isBlocked: blockees.includes(target), + isReactionBlocking: reactionBlockers.includes(target), + isReactionBlocked: reactionBlockees.includes(target), isMuted: muters.includes(target), isRenoteMuted: renoteMuters.includes(target), }, @@ -638,6 +674,8 @@ export class UserEntityService implements OnModuleInit { hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou, isBlocking: relation.isBlocking, isBlocked: relation.isBlocked, + isReactionBlocking: relation.isReactionBlocking, + isReactionBlocked: relation.isReactionBlocked, isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, notify: relation.following?.notify ?? 'none', diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index e599fc7b37..31574931c7 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -48,6 +48,7 @@ export const DI = { mutingsRepository: Symbol('mutingsRepository'), renoteMutingsRepository: Symbol('renoteMutingsRepository'), blockingsRepository: Symbol('blockingsRepository'), + blockingReactionUsersRepository: Symbol('blockingReactionUsersRepository'), swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), hashtagsRepository: Symbol('hashtagsRepository'), abuseUserReportsRepository: Symbol('abuseUserReportsRepository'), diff --git a/packages/backend/src/models/BlockingReactionUser.ts b/packages/backend/src/models/BlockingReactionUser.ts new file mode 100644 index 0000000000..9e4ebd4397 --- /dev/null +++ b/packages/backend/src/models/BlockingReactionUser.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('blocking_reaction_user') +@Index(['blockerId', 'blockeeId'], { unique: true }) +export class MiBlockingReactionUser { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The blockee user ID.', + }) + public blockeeId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public blockee: MiUser | null; + + @Index() + @Column({ + ...id(), + comment: 'The blocker user ID.', + }) + public blockerId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public blocker: MiUser | null; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index ea0f88baba..f0cabfc3a1 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -18,6 +18,7 @@ import { MiAuthSession, MiAvatarDecoration, MiBlocking, + MiBlockingReactionUser, MiBubbleGameRecord, MiChannel, MiChannelFavorite, @@ -279,6 +280,12 @@ const $blockingsRepository: Provider = { inject: [DI.db], }; +const $blockingReactionUsersRepository: Provider = { + provide: DI.blockingReactionUsersRepository, + useFactory: (db: DataSource) => db.getRepository(MiBlockingReactionUser), + inject: [DI.db], +}; + const $swSubscriptionsRepository: Provider = { provide: DI.swSubscriptionsRepository, useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository), @@ -531,6 +538,7 @@ const $reversiGamesRepository: Provider = { $mutingsRepository, $renoteMutingsRepository, $blockingsRepository, + $blockingReactionUsersRepository, $swSubscriptionsRepository, $hashtagsRepository, $abuseUserReportsRepository, @@ -602,6 +610,7 @@ const $reversiGamesRepository: Provider = { $mutingsRepository, $renoteMutingsRepository, $blockingsRepository, + $blockingReactionUsersRepository, $swSubscriptionsRepository, $hashtagsRepository, $abuseUserReportsRepository, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index c72bdaa727..526f959cfd 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -21,6 +21,7 @@ import { MiApp } from '@/models/App.js'; import { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; import { MiAuthSession } from '@/models/AuthSession.js'; import { MiBlocking } from '@/models/Blocking.js'; +import { MiBlockingReactionUser } from '@/models/BlockingReactionUser.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; import { MiClip } from '@/models/Clip.js'; @@ -136,6 +137,7 @@ export { MiAvatarDecoration, MiAuthSession, MiBlocking, + MiBlockingReactionUser, MiChannelFollowing, MiChannelFavorite, MiClip, @@ -207,6 +209,7 @@ export type AppsRepository = Repository & MiRepository; export type AvatarDecorationsRepository = Repository & MiRepository; export type AuthSessionsRepository = Repository & MiRepository; export type BlockingsRepository = Repository & MiRepository; +export type BlockingReactionUsersRepository = Repository & MiRepository; export type ChannelFollowingsRepository = Repository & MiRepository; export type ChannelFavoritesRepository = Repository & MiRepository; export type ClipsRepository = Repository & MiRepository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 251a03c303..768bf1d45c 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -82,6 +82,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import {MiBlockingReactionUser} from "@/models/_.js"; pg.types.setTypeParser(20, Number); @@ -152,6 +153,7 @@ export const entities = [ MiMuting, MiRenoteMuting, MiBlocking, + MiBlockingReactionUser, MiNote, MiNoteFavorite, MiNoteReaction, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5bb194313d..05737dc399 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -114,6 +114,9 @@ import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js' import * as ep___blocking_create from './endpoints/blocking/create.js'; import * as ep___blocking_delete from './endpoints/blocking/delete.js'; import * as ep___blocking_list from './endpoints/blocking/list.js'; +import * as ep___blocking_reaction_user_create from './endpoints/blocking-reaction-user/create.js'; +import * as ep___blocking_reaction_user_delete from './endpoints/blocking-reaction-user/delete.js'; +import * as ep___blocking_reaction_user_list from './endpoints/blocking-reaction-user/list.js'; import * as ep___channels_create from './endpoints/channels/create.js'; import * as ep___channels_featured from './endpoints/channels/featured.js'; import * as ep___channels_follow from './endpoints/channels/follow.js'; @@ -502,6 +505,9 @@ const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', us const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default }; const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default }; const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default }; +const $blocking_reaction_user_create: Provider = { provide: 'ep:blocking-reaction-user/create', useClass: ep___blocking_reaction_user_create.default }; +const $blocking_reaction_user_delete: Provider = { provide: 'ep:blocking-reaction-user/delete', useClass: ep___blocking_reaction_user_delete.default }; +const $blocking_reaction_user_list: Provider = { provide: 'ep:blocking-reaction-user/list', useClass: ep___blocking_reaction_user_list.default }; const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default }; const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default }; const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default }; @@ -894,6 +900,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $blocking_create, $blocking_delete, $blocking_list, + $blocking_reaction_user_create, + $blocking_reaction_user_delete, + $blocking_reaction_user_list, $channels_create, $channels_featured, $channels_follow, @@ -1280,6 +1289,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $blocking_create, $blocking_delete, $blocking_list, + $blocking_reaction_user_create, + $blocking_reaction_user_delete, + $blocking_reaction_user_list, $channels_create, $channels_featured, $channels_follow, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 15809b2678..d015536610 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -120,6 +120,9 @@ import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js' import * as ep___blocking_create from './endpoints/blocking/create.js'; import * as ep___blocking_delete from './endpoints/blocking/delete.js'; import * as ep___blocking_list from './endpoints/blocking/list.js'; +import * as ep___blocking_reaction_user_create from './endpoints/blocking-reaction-user/create.js'; +import * as ep___blocking_reaction_user_delete from './endpoints/blocking-reaction-user/delete.js'; +import * as ep___blocking_reaction_user_list from './endpoints/blocking-reaction-user/list.js'; import * as ep___channels_create from './endpoints/channels/create.js'; import * as ep___channels_featured from './endpoints/channels/featured.js'; import * as ep___channels_follow from './endpoints/channels/follow.js'; @@ -506,6 +509,9 @@ const eps = [ ['blocking/create', ep___blocking_create], ['blocking/delete', ep___blocking_delete], ['blocking/list', ep___blocking_list], + ['blocking-reaction-user/create', ep___blocking_reaction_user_create], + ['blocking-reaction-user/delete', ep___blocking_reaction_user_delete], + ['blocking-reaction-user/list', ep___blocking_reaction_user_list], ['channels/create', ep___channels_create], ['channels/featured', ep___channels_featured], ['channels/follow', ep___channels_follow], diff --git a/packages/backend/src/server/api/endpoints/blocking-reaction-user/create.ts b/packages/backend/src/server/api/endpoints/blocking-reaction-user/create.ts new file mode 100644 index 0000000000..4d80a851ae --- /dev/null +++ b/packages/backend/src/server/api/endpoints/blocking-reaction-user/create.ts @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository, BlockingReactionUsersRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +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 {BlockingReactionUserService} from "@/core/BlockingReactionUserService.js"; + +export const meta = { + tags: ['account'], + + limit: { + duration: ms('1hour'), + max: 20, + }, + + requireCredential: true, + + kind: 'write:blocks', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e', + }, + + blockeeIsYourself: { + message: 'Blockee is yourself.', + code: 'BLOCKEE_IS_YOURSELF', + id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6', + }, + + alreadyBlocking: { + message: 'You are already blocking that user.', + code: 'ALREADY_BLOCKING', + id: '787fed64-acb9-464a-82eb-afbd745b9614', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserDetailedNotMe', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingsRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private blockingReactionUserService: BlockingReactionUserService, + ) { + super(meta, paramDef, async (ps, me) => { + const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); + + // 自分自身 + if (me.id === ps.userId) { + throw new ApiError(meta.errors.blockeeIsYourself); + } + + // 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); + throw err; + }); + + // Check if already blocking + const exist = await this.blockingReactionUsersRepository.exists({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + }, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyBlocking); + } + + await this.blockingReactionUserService.block(blocker, blockee); + + return await this.userEntityService.pack(blockee.id, blocker, { + schema: 'UserDetailedNotMe', + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/blocking-reaction-user/delete.ts b/packages/backend/src/server/api/endpoints/blocking-reaction-user/delete.ts new file mode 100644 index 0000000000..aebfcd39b3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/blocking-reaction-user/delete.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type {UsersRepository, BlockingsRepository, BlockingReactionUsersRepository} from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +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 {BlockingReactionUserService} from "@/core/BlockingReactionUserService.js"; + +export const meta = { + tags: ['account'], + + limit: { + duration: ms('1hour'), + max: 100, + }, + + requireCredential: true, + + kind: 'write:blocks', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '8621d8bf-c358-4303-a066-5ea78610eb3f', + }, + + blockeeIsYourself: { + message: 'Blockee is yourself.', + code: 'BLOCKEE_IS_YOURSELF', + id: '06f6fac6-524b-473c-a354-e97a40ae6eac', + }, + + notBlocking: { + message: 'You are not blocking that user.', + code: 'NOT_BLOCKING', + id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserDetailedNotMe', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.blockingReactionUsersRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private blockingReactionUserService: BlockingReactionUserService, + ) { + super(meta, paramDef, async (ps, me) => { + const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); + + // Check if the blockee is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.blockeeIsYourself); + } + + // 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); + throw err; + }); + + // Check not blocking + const exist = await this.blockingReactionUsersRepository.exists({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + }, + }); + + if (!exist) { + throw new ApiError(meta.errors.notBlocking); + } + + // Delete blocking + await this.blockingReactionUserService.unblock(blocker, blockee); + + return await this.userEntityService.pack(blockee.id, blocker, { + schema: 'UserDetailedNotMe', + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/blocking-reaction-user/list.ts b/packages/backend/src/server/api/endpoints/blocking-reaction-user/list.ts new file mode 100644 index 0000000000..8e1e59e8fa --- /dev/null +++ b/packages/backend/src/server/api/endpoints/blocking-reaction-user/list.ts @@ -0,0 +1,61 @@ +/* + * 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 { BlockingReactionUsersRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { BlockingReactionUserEntityService } from '@/core/entities/BlockingReactionUserEntityService.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'read:blocks', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Blocking', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.blockingReactionUsersRepository) + private blockingReactionUsersRepository: BlockingReactionUsersRepository, + + private blockingReactionUserEntityService: BlockingReactionUserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.blockingReactionUsersRepository.createQueryBuilder('blocking_reaction_user'), ps.sinceId, ps.untilId) + .andWhere('blocking_reaction_user.blockerId = :meId', { meId: me.id }); + + const blockings = await query + .limit(ps.limit) + .getMany(); + + return await this.blockingReactionUserEntityService.packMany(blockings, me); + }); + } +} diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 4d413d53ab..74e69e1d92 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -122,6 +122,39 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + + + @@ -157,6 +190,11 @@ const blockingPagination = { limit: 10, }; +const blockingReactionUserPagination = { + endpoint: 'blocking-reaction-user/list' as const, + limit: 10, +}; + const expandedRenoteMuteItems = ref([]); const expandedMuteItems = ref([]); const expandedBlockItems = ref([]); @@ -194,6 +232,16 @@ async function unblock(user, ev) { }], ev.currentTarget ?? ev.target); } +async function unblockReactionUser(user, ev) { + os.popupMenu([{ + text: i18n.ts.unblock, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('blocking-reaction-user/delete', { userId: user.id }); + }, + }], ev.currentTarget ?? ev.target); +} + async function toggleRenoteMuteItem(item) { if (expandedRenoteMuteItems.value.includes(item.id)) { expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index d15279d633..5cfb9b367b 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -84,6 +84,16 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } + async function toggleReactionBlock() { + if (!await getConfirmed(user.isReactionBlocking ? i18n.ts.unblockReactionUserConfirm : i18n.ts.blockReactionUserConfirm)) return; + + os.apiWithDialog(user.isReactionBlocking ? 'blocking-reaction-user/delete' : 'blocking-reaction-user/create', { + userId: user.id, + }).then(() => { + user.isReactionBlocking = !user.isReactionBlocking; + }); + } + async function toggleNotify() { os.apiWithDialog('following/update', { userId: user.id, @@ -373,6 +383,10 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, + }, { + icon: 'ti ti-ban', + text: user.isReactionBlocking ? i18n.ts.unblockReaction : i18n.ts.blockReaction, + action: toggleReactionBlock, }); if (user.isFollowed) { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 1837f3db4f..52a5824775 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1204,9 +1204,42 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:blocks* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request( diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index cb1f4dbe96..865d08588d 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -579,6 +579,12 @@ import type { ReversiSurrenderRequest, ReversiVerifyRequest, ReversiVerifyResponse, + BlockingReactionUserCreateRequest, + BlockingReactionUserCreateResponse, + BlockingReactionUserDeleteRequest, + BlockingReactionUserDeleteResponse, + BlockingReactionUserListRequest, + BlockingReactionUserListResponse, } from './entities.js'; export type Endpoints = { @@ -690,6 +696,9 @@ export type Endpoints = { 'blocking/create': { req: BlockingCreateRequest; res: BlockingCreateResponse }; 'blocking/delete': { req: BlockingDeleteRequest; res: BlockingDeleteResponse }; 'blocking/list': { req: BlockingListRequest; res: BlockingListResponse }; + 'blocking-reaction-user/create': { req: BlockingReactionUserCreateRequest; res: BlockingReactionUserCreateResponse }; + 'blocking-reaction-user/delete': { req: BlockingReactionUserDeleteRequest; res: BlockingReactionUserDeleteResponse }; + 'blocking-reaction-user/list': { req: BlockingReactionUserListRequest; res: BlockingReactionUserListResponse }; 'channels/create': { req: ChannelsCreateRequest; res: ChannelsCreateResponse }; 'channels/featured': { req: EmptyRequest; res: ChannelsFeaturedResponse }; 'channels/follow': { req: ChannelsFollowRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index a8f474c25c..40f8764394 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -159,6 +159,12 @@ export type BlockingDeleteRequest = operations['blocking___delete']['requestBody export type BlockingDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json']; export type BlockingListRequest = operations['blocking___list']['requestBody']['content']['application/json']; export type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json']; +export type BlockingReactionUserCreateRequest = operations['blocking___create']['requestBody']['content']['application/json']; +export type BlockingReactionUserCreateResponse = operations['blocking___create']['responses']['200']['content']['application/json']; +export type BlockingReactionUserDeleteRequest = operations['blocking___delete']['requestBody']['content']['application/json']; +export type BlockingReactionUserDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json']; +export type BlockingReactionUserListRequest = operations['blocking___list']['requestBody']['content']['application/json']; +export type BlockingReactionUserListResponse = operations['blocking___list']['responses']['200']['content']['application/json']; export type ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json']; export type ChannelsCreateResponse = operations['channels___create']['responses']['200']['content']['application/json']; export type ChannelsFeaturedResponse = operations['channels___featured']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 04574849d4..ec63fa9e34 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -21,6 +21,7 @@ export type Following = components['schemas']['Following']; export type Muting = components['schemas']['Muting']; export type RenoteMuting = components['schemas']['RenoteMuting']; export type Blocking = components['schemas']['Blocking']; +export type BlockingReactionUser = components['schemas']['Blocking']; export type Hashtag = components['schemas']['Hashtag']; export type InviteCode = components['schemas']['InviteCode']; export type Page = components['schemas']['Page']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 280abba727..97cb19e55c 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3823,8 +3823,10 @@ export type components = { isFollowed?: boolean; hasPendingFollowRequestFromYou?: boolean; hasPendingFollowRequestToYou?: boolean; - isBlocking?: boolean; - isBlocked?: boolean; + isBlocking?: boolean; + isBlocked?: boolean; + isReactionBlocking?: boolean; + isReactionBlocked?: boolean; isMuted?: boolean; isRenoteMuted?: boolean; /** @enum {string} */