diff --git a/locales/index.d.ts b/locales/index.d.ts index 0ae188f1f7..4353cf78aa 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 1b59708d85..397a10c8a0 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/packages/backend/migration/1731932268436-addBlockingReactionUser.js b/packages/backend/migration/1731932268436-addBlockingReactionUser.js new file mode 100644 index 0000000000..72067694fa --- /dev/null +++ b/packages/backend/migration/1731932268436-addBlockingReactionUser.js @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddBlockingReactionUser1731932268436 { + name = 'AddBlockingReactionUser1731932268436' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "blocking" ADD "blockType" character varying NOT NULL DEFAULT 'user'`); + await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockType" IS 'Block type.'`); + await queryRunner.query(`CREATE INDEX "IDX_cd38e7ea08163899a2d1f4427d" ON "blocking" ("blockType") `); + } + + async down(queryRunner) { + await queryRunner.query(`DELETE FROM blocking WHERE "blockType" = 'reaction'`); // blockingテーブルのblockTypeがreactionの行を削除 + await queryRunner.query(`DROP INDEX "public"."IDX_cd38e7ea08163899a2d1f4427d"`); + await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockType" IS 'Block type.'`); + await queryRunner.query(`ALTER TABLE "blocking" DROP COLUMN "blockType"`); + } +} diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 6725ebe75b..9d0ac56615 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -8,6 +8,7 @@ import * as Redis from 'ioredis'; import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; +import { MiBlockingType } from '@/models/Blocking.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -24,6 +25,8 @@ export class CacheService implements OnApplicationShutdown { public userMutingsCache: RedisKVCache>; public userBlockingCache: RedisKVCache>; public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ + public userReactionBlockingCache: RedisKVCache>; // NOTE: リアクションBlockキャッシュ + public userReactionBlockedCache: RedisKVCache>; // NOTE: 「被」リアクションBlockキャッシュ public renoteMutingsCache: RedisKVCache>; public userFollowingsCache: RedisKVCache | undefined>>; @@ -80,7 +83,7 @@ export class CacheService implements OnApplicationShutdown { this.userBlockingCache = new RedisKVCache>(this.redisClient, 'userBlocking', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))), + fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key, blockType: MiBlockingType.User }, 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)), }); @@ -88,7 +91,23 @@ export class CacheService implements OnApplicationShutdown { this.userBlockedCache = new RedisKVCache>(this.redisClient, 'userBlocked', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))), + fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key, blockType: MiBlockingType.User }, 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.userReactionBlockingCache = new RedisKVCache>(this.redisClient, 'userReactionBlocking', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key, blockType: MiBlockingType.Reaction }, 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.userReactionBlockedCache = new RedisKVCache>(this.redisClient, 'userReactionBlocked', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key, blockType: MiBlockingType.Reaction }, 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)), }); diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 03646ff566..d113848c20 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']; }; + blockingReactionCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; + blockingReactionDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; policiesUpdated: MiRole['policies']; roleCreated: MiRole; roleDeleted: MiRole; diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index c4feeaf971..a4b9db061c 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -10,6 +10,7 @@ import type { MiUser } from '@/models/User.js'; import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import { MiBlockingType } from '@/models/Blocking.js'; import type { SelectQueryBuilder } from 'typeorm'; @Injectable() @@ -72,7 +73,8 @@ export class QueryService { public generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') - .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); + .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }) + .andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User }); // 投稿の作者にブロックされていない かつ // 投稿の返信先の作者にブロックされていない かつ @@ -97,7 +99,8 @@ export class QueryService { public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockeeId') - .where('blocking.blockerId = :blockerId', { blockerId: me.id }); + .where('blocking.blockerId = :blockerId', { blockerId: me.id }) + .andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User }); const blockedQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 6f9fe53937..04cbc72107 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -107,7 +107,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.userBlockingService.checkReactionBlocked(note.userId, user.id); + if (blocked || reactionBlocked) { throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); } } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 2f1310b8ef..7611c7584c 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -8,10 +8,16 @@ import { ModuleRef } from '@nestjs/core'; import { IdService } from '@/core/IdService.js'; import type { MiUser } from '@/models/User.js'; import type { MiBlocking } from '@/models/Blocking.js'; +import { MiBlockingType } from '@/models/Blocking.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 } from '@/models/_.js'; +import type { + BlockingsRepository, + FollowRequestsRepository, + UserListMembershipsRepository, + UserListsRepository, +} from '@/models/_.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -67,14 +73,27 @@ export class UserBlockingService implements OnModuleInit { this.removeFromList(blockee, blocker), ]); - const blocking = { - id: this.idService.gen(), - blocker, + const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, - blockee, blockeeId: blockee.id, - } as MiBlocking; + }).then(blocking => { + if (blocking) { + return blocking; + } + return { + id: this.idService.gen(), + blocker, + blockerId: blocker.id, + blockee, + blockeeId: blockee.id, + blockType: MiBlockingType.User, + } as MiBlocking; + }); + if (blocking.blockType === MiBlockingType.Reaction) { + await this.reactionUnblock(blocker, blockee); + } + blocking.blockType = MiBlockingType.User; await this.blockingsRepository.insert(blocking); this.cacheService.userBlockingCache.refresh(blocker.id); @@ -160,6 +179,7 @@ export class UserBlockingService implements OnModuleInit { const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, + blockType: MiBlockingType.User, }); if (blocking == null) { @@ -169,8 +189,9 @@ export class UserBlockingService implements OnModuleInit { // 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; + // But we don't need to do this because we are not using them in this function. + // blocking.blocker = blocker; + // blocking.blockee = blockee; await this.blockingsRepository.delete(blocking.id); @@ -193,4 +214,73 @@ export class UserBlockingService implements OnModuleInit { public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise { return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId); } + + @bindThis + public async reactionBlock(blocker: MiUser, blockee: MiUser, silent = false) { + const blocking = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, + }).then(blocking => { + if (blocking) { + return blocking; + } + return { + id: this.idService.gen(), + blocker, + blockerId: blocker.id, + blockee, + blockeeId: blockee.id, + blockType: MiBlockingType.Reaction, + } as MiBlocking; + }); + + if (blocking.blockType === MiBlockingType.User) { + await this.unblock(blocker, blockee); + } + blocking.blockType = MiBlockingType.Reaction; + await this.blockingsRepository.insert(blocking); + + this.cacheService.userReactionBlockingCache.refresh(blocker.id); + this.cacheService.userReactionBlockedCache.refresh(blockee.id); + + this.globalEventService.publishInternalEvent('blockingReactionCreated', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + } + + @bindThis + public async reactionUnblock(blocker: MiUser, blockee: MiUser) { + const blocking = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, + blockType: MiBlockingType.Reaction, + }); + + if (blocking == null) { + this.logger.warn('Unblock requested, but the target was not blocked.'); + 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.blockingsRepository.delete(blocking.id); + + this.cacheService.userReactionBlockingCache.refresh(blocker.id); + this.cacheService.userReactionBlockedCache.refresh(blockee.id); + + this.globalEventService.publishInternalEvent('blockingReactionDeleted', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + } + + @bindThis + public async checkReactionBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise { + return (await this.cacheService.userReactionBlockingCache.fetch(blockerId)).has(blockeeId); + } } + diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index d3c087a153..df2c803691 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -39,6 +39,7 @@ import type { UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; +import { MiBlockingType } from '@/models/Blocking.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; @@ -76,6 +77,8 @@ export type UserRelation = { hasPendingFollowRequestToYou: boolean isBlocking: boolean isBlocked: boolean + isReactionBlocking: boolean + isReactionBlocked: boolean isMuted: boolean isRenoteMuted: boolean } @@ -169,6 +172,8 @@ export class UserEntityService implements OnModuleInit { hasPendingFollowRequestToYou, isBlocking, isBlocked, + isReactionBlocking, + isReactionBlocked, isMuted, isRenoteMuted, ] = await Promise.all([ @@ -198,12 +203,28 @@ export class UserEntityService implements OnModuleInit { where: { blockerId: me, blockeeId: target, + blockType: MiBlockingType.User, }, }), this.blockingsRepository.exists({ where: { blockerId: target, blockeeId: me, + blockType: MiBlockingType.User, + }, + }), + this.blockingsRepository.exists({ + where: { + blockerId: me, + blockeeId: target, + blockType: MiBlockingType.Reaction, + }, + }), + this.blockingsRepository.exists({ + where: { + blockerId: target, + blockeeId: me, + blockType: MiBlockingType.Reaction, }, }), this.mutingsRepository.exists({ @@ -229,6 +250,8 @@ export class UserEntityService implements OnModuleInit { hasPendingFollowRequestToYou, isBlocking, isBlocked, + isReactionBlocking, + isReactionBlocked, isMuted, isRenoteMuted, }; @@ -243,6 +266,8 @@ export class UserEntityService implements OnModuleInit { followeesRequests, blockers, blockees, + reactionBlockers, + reactionBlockees, muters, renoteMuters, ] = await Promise.all([ @@ -266,11 +291,25 @@ export class UserEntityService implements OnModuleInit { this.blockingsRepository.createQueryBuilder('b') .select('b.blockeeId') .where('b.blockerId = :me', { me }) + .andWhere('b.blockType = :type', { type: MiBlockingType.User }) .getRawMany<{ b_blockeeId: string }>() .then(it => it.map(it => it.b_blockeeId)), this.blockingsRepository.createQueryBuilder('b') .select('b.blockerId') .where('b.blockeeId = :me', { me }) + .andWhere('b.blockType = :type', { type: MiBlockingType.User }) + .getRawMany<{ b_blockerId: string }>() + .then(it => it.map(it => it.b_blockerId)), + this.blockingsRepository.createQueryBuilder('b') + .select('b.blockeeId') + .where('b.blockerId = :me', { me }) + .andWhere('b.blockType = :type', { type: MiBlockingType.Reaction }) + .getRawMany<{ b_blockeeId: string }>() + .then(it => it.map(it => it.b_blockeeId)), + this.blockingsRepository.createQueryBuilder('b') + .select('b.blockerId') + .where('b.blockeeId = :me', { me }) + .andWhere('b.blockType = :type', { type: MiBlockingType.Reaction }) .getRawMany<{ b_blockerId: string }>() .then(it => it.map(it => it.b_blockerId)), this.mutingsRepository.createQueryBuilder('m') @@ -300,6 +339,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 +679,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/models/Blocking.ts b/packages/backend/src/models/Blocking.ts index 34a6efe5a6..e19edd7935 100644 --- a/packages/backend/src/models/Blocking.ts +++ b/packages/backend/src/models/Blocking.ts @@ -7,6 +7,11 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { id } from './util/id.js'; import { MiUser } from './User.js'; +export enum MiBlockingType { + User = 'user', + Reaction = 'reaction', +} + @Entity('blocking') @Index(['blockerId', 'blockeeId'], { unique: true }) export class MiBlocking { @@ -38,4 +43,11 @@ export class MiBlocking { }) @JoinColumn() public blocker: MiUser | null; + + @Index() + @Column({ + comment: 'Block type.', + default: MiBlockingType.User, + }) + public blockType: MiBlockingType; } diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index c72bdaa727..5a9769760f 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -20,7 +20,7 @@ import { MiAntenna } from '@/models/Antenna.js'; 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 { MiBlocking, MiBlockingType } from '@/models/Blocking.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; import { MiClip } from '@/models/Clip.js'; @@ -136,6 +136,7 @@ export { MiAvatarDecoration, MiAuthSession, MiBlocking, + MiBlockingType, MiChannelFollowing, MiChannelFavorite, MiClip, diff --git a/packages/backend/src/models/json-schema/blocking.ts b/packages/backend/src/models/json-schema/blocking.ts index 2d02ba6a70..431832b28b 100644 --- a/packages/backend/src/models/json-schema/blocking.ts +++ b/packages/backend/src/models/json-schema/blocking.ts @@ -27,5 +27,10 @@ export const packedBlockingSchema = { optional: false, nullable: false, ref: 'UserDetailedNotMe', }, + blockType: { + type: 'string', + optional: false, nullable: false, + enum: ['user', 'reaction'], + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 38631f907d..18b80155c8 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -416,6 +416,14 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, + isReactionBlocking: { + type: 'boolean', + nullable: false, optional: true, + }, + isReactionBlocked: { + type: 'boolean', + nullable: false, optional: true, + }, isMuted: { type: 'boolean', nullable: false, optional: true, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5bb194313d..512f27e6a9 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -231,6 +231,9 @@ import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.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___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.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, 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..cfc0e90b02 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/blocking-reaction-user/create.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 { BlockingsRepository, UsersRepository } from '@/models/_.js'; +import { MiBlockingType } from '@/models/Blocking.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { ApiError } from '../../error.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 blockingsRepository: BlockingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userBlockingService: UserBlockingService, + ) { + 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.blockingsRepository.exists({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + blockType: MiBlockingType.Reaction, + }, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyBlocking); + } + + await this.userBlockingService.reactionBlock(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..45a8f6e5f1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/blocking-reaction-user/delete.ts @@ -0,0 +1,112 @@ +/* + * 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 } from '@/models/_.js'; +import { MiBlockingType } from '@/models/Blocking.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { ApiError } from '../../error.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.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userBlockingService: UserBlockingService, + ) { + 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.blockingsRepository.exists({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + blockType: MiBlockingType.Reaction, + }, + }); + + if (!exist) { + throw new ApiError(meta.errors.notBlocking); + } + + // Delete blocking + await this.userBlockingService.reactionUnblock(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..f4cb38ec1e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/blocking-reaction-user/list.ts @@ -0,0 +1,63 @@ +/* + * 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 { BlockingsRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; +import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { MiBlockingType } from '@/models/Blocking.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.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private blockingEntityService: BlockingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) + .andWhere('blocking.blockerId = :meId', { meId: me.id }) + .andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.Reaction }); + + const blockings = await query + .limit(ps.limit) + .getMany(); + + return await this.blockingEntityService.packMany(blockings, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 5066215749..4f775b24bb 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -6,7 +6,8 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; +import type { BlockingsRepository, UsersRepository } from '@/models/_.js'; +import { MiBlockingType } from '@/models/Blocking.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; @@ -92,6 +93,7 @@ export default class extends Endpoint { // eslint- where: { blockerId: blocker.id, blockeeId: blockee.id, + blockType: MiBlockingType.User, }, }); diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index cebb307338..e0d9f0939e 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -6,7 +6,8 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; +import type { BlockingsRepository, UsersRepository } from '@/models/_.js'; +import { MiBlockingType } from '@/models/Blocking.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; @@ -92,6 +93,7 @@ export default class extends Endpoint { // eslint- where: { blockerId: blocker.id, blockeeId: blockee.id, + blockType: MiBlockingType.User, }, }); diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 8431fa6b34..292bc0417e 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -9,6 +9,7 @@ import type { BlockingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js'; import { DI } from '@/di-symbols.js'; +import { MiBlockingType } from '@/models/Blocking.js'; export const meta = { tags: ['account'], @@ -49,7 +50,8 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) - .andWhere('blocking.blockerId = :meId', { meId: me.id }); + .andWhere('blocking.blockerId = :meId', { meId: me.id }) + .andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User }); const blockings = await query .limit(ps.limit) diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts index e4f42809f8..c5c69cdb74 100644 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -12,7 +12,7 @@ import { secureRndstr } from '@/misc/secure-rndstr.js'; import { genAidx } from '@/misc/id/aidx.js'; import { BlockingsRepository, - FollowingsRepository, FollowRequestsRepository, + FollowingsRepository, FollowRequestsRepository, MiBlockingType, MiUserProfile, MutingsRepository, RenoteMutingsRepository, UserMemoRepository, UserProfilesRepository, @@ -115,6 +115,16 @@ describe('UserEntityService', () => { id: genAidx(Date.now()), blockerId: blocker.id, blockeeId: blockee.id, + blockType: MiBlockingType.User, + }); + } + + async function blockReaction(blocker: MiUser, blockee: MiUser) { + await blockingRepository.insert({ + id: genAidx(Date.now()), + blockerId: blocker.id, + blockeeId: blockee.id, + blockType: MiBlockingType.Reaction, }); } @@ -260,6 +270,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } @@ -275,6 +287,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } @@ -290,6 +304,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } @@ -305,6 +321,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(true); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } @@ -320,6 +338,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(true); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } @@ -339,6 +359,41 @@ describe('UserEntityService', () => { expect(actual.isRenoteMuted).toBe(false); } + + // meがリアクションをブロックしてる人たち + const reactionBlockingYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of reactionBlockingYou) { + await blockReaction(me, who); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(true); + expect(actual.isReactionBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meのリアクションをブロックしてる人たち + const reactionBlockingMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of reactionBlockingMe) { + await blockReaction(who, me); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(true); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + // meがミュートしてる人たち const muters = await Promise.all(randomIntRange().map(() => createUser())); for (const who of muters) { @@ -350,6 +405,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(true); expect(actual.isRenoteMuted).toBe(false); } @@ -365,6 +422,8 @@ describe('UserEntityService', () => { expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); + expect(actual.isReactionBlocking).toBe(false); + expect(actual.isReactionBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(true); } 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..c9039bb927 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.unblockReactionUser : i18n.ts.blockReactionUser, + action: toggleReactionBlock, }); if (user.isFollowed) { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 01a3dbbb30..35cd112b6d 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -551,6 +551,24 @@ type BlockingListRequest = operations['blocking___list']['requestBody']['content // @public (undocumented) type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json']; +// @public (undocumented) +type BlockingReactionUserCreateRequest = operations['blocking-reaction-user___create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type BlockingReactionUserCreateResponse = operations['blocking-reaction-user___create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type BlockingReactionUserDeleteRequest = operations['blocking-reaction-user___delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type BlockingReactionUserDeleteResponse = operations['blocking-reaction-user___delete']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type BlockingReactionUserListRequest = operations['blocking-reaction-user___list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type BlockingReactionUserListResponse = operations['blocking-reaction-user___list']['responses']['200']['content']['application/json']; + // @public (undocumented) type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json']; @@ -1381,6 +1399,12 @@ declare namespace entities { BlockingDeleteResponse, BlockingListRequest, BlockingListResponse, + BlockingReactionUserCreateRequest, + BlockingReactionUserCreateResponse, + BlockingReactionUserDeleteRequest, + BlockingReactionUserDeleteResponse, + BlockingReactionUserListRequest, + BlockingReactionUserListResponse, ChannelsCreateRequest, ChannelsCreateResponse, ChannelsFeaturedResponse, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 1837f3db4f..314aa6adee 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1204,6 +1204,39 @@ 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. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index cb1f4dbe96..e9e01fbc61 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -156,6 +156,12 @@ import type { BlockingDeleteResponse, BlockingListRequest, BlockingListResponse, + BlockingReactionUserCreateRequest, + BlockingReactionUserCreateResponse, + BlockingReactionUserDeleteRequest, + BlockingReactionUserDeleteResponse, + BlockingReactionUserListRequest, + BlockingReactionUserListResponse, ChannelsCreateRequest, ChannelsCreateResponse, ChannelsFeaturedResponse, @@ -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..e01c6addf5 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-reaction-user___create']['requestBody']['content']['application/json']; +export type BlockingReactionUserCreateResponse = operations['blocking-reaction-user___create']['responses']['200']['content']['application/json']; +export type BlockingReactionUserDeleteRequest = operations['blocking-reaction-user___delete']['requestBody']['content']['application/json']; +export type BlockingReactionUserDeleteResponse = operations['blocking-reaction-user___delete']['responses']['200']['content']['application/json']; +export type BlockingReactionUserListRequest = operations['blocking-reaction-user___list']['requestBody']['content']['application/json']; +export type BlockingReactionUserListResponse = operations['blocking-reaction-user___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/types.ts b/packages/misskey-js/src/autogen/types.ts index 280abba727..4d2126a149 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -997,6 +997,33 @@ export type paths = { */ post: operations['blocking___list']; }; + '/blocking-reaction-user/create': { + /** + * blocking-reaction-user/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + post: operations['blocking-reaction-user___create']; + }; + '/blocking-reaction-user/delete': { + /** + * blocking-reaction-user/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + post: operations['blocking-reaction-user___delete']; + }; + '/blocking-reaction-user/list': { + /** + * blocking-reaction-user/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:blocks* + */ + post: operations['blocking-reaction-user___list']; + }; '/channels/create': { /** * channels/create @@ -3825,6 +3852,8 @@ export type components = { hasPendingFollowRequestToYou?: boolean; isBlocking?: boolean; isBlocked?: boolean; + isReactionBlocking?: boolean; + isReactionBlocked?: boolean; isMuted?: boolean; isRenoteMuted?: boolean; /** @enum {string} */ @@ -4486,6 +4515,8 @@ export type components = { /** Format: id */ blockeeId: string; blockee: components['schemas']['UserDetailedNotMe']; + /** @enum {string} */ + blockType: 'user' | 'reaction'; }; Hashtag: { /** @example misskey */ @@ -11705,6 +11736,184 @@ export type operations = { }; }; }; + /** + * blocking-reaction-user/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + 'blocking-reaction-user___create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailedNotMe']; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * blocking-reaction-user/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + 'blocking-reaction-user___delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailedNotMe']; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * blocking-reaction-user/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:blocks* + */ + 'blocking-reaction-user___list': { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Blocking'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * channels/create * @description No description provided.