diff --git a/locales/en-US.yml b/locales/en-US.yml index 6703d1cf3ab5..7e4bab06ad8f 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 24613419ce8b..b52c313208b5 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 9f32969a79c3..4c48ed3c3dbb 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 50132c064569..b147b1ed4db6 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 000000000000..7e06b2311f7c --- /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 000000000000..c20a7617a631 --- /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 6725ebe75b14..fffbe84b7b31 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 734d135648d5..89486f535090 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 03646ff56680..7e44f544c0a3 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 6f9fe53937bc..72c027233a60 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 000000000000..1219f9c64ccb --- /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 d3c087a15376..50c6f321da65 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 e599fc7b3737..31574931c73e 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 000000000000..9e4ebd4397c5 --- /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 ea0f88babaa7..f0cabfc3a171 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 c72bdaa72726..526f959cfd8e 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 251a03c303a7..768bf1d45cb3 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 5bb194313d2c..05737dc39925 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 15809b2678ad..d015536610e4 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 000000000000..4d80a851ae05 --- /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 000000000000..aebfcd39b35d --- /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 000000000000..8e1e59e8faab --- /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 4d413d53ab5d..74e69e1d9258 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 d15279d63339..5cfb9b367b0c 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 1837f3db4f39..52a582477575 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. - * + /** + * 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 cb1f4dbe96b5..865d08588df9 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 a8f474c25c25..40f876439445 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 04574849d44d..ec63fa9e3493 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 280abba72768..97cb19e55cbb 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} */