Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 特定ユーザーからのリアクションをブロックする機能の追加 #14992

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3dd5af3
Add: BlockingテーブルにisReactionBlockカラムを追加し、blocking-reaction-userエンドポイン…
sakuhanight Nov 18, 2024
37627bb
Add: リアクションブロックの設定画面を追加
sakuhanight Nov 18, 2024
51a2a7d
Add: フロントエンドのユーザーメニューにリアクションブロックを追加
sakuhanight Nov 18, 2024
1b0ac28
fix
sakuhanight Nov 18, 2024
9ef2dbb
Mod: ログ出力を英語に変更
sakuhanight Nov 18, 2024
202fcee
fix: as -> satisfies
sakuhanight Nov 18, 2024
3ea69b6
Mod: isReactionBlockからenumに変更
sakuhanight Nov 18, 2024
24792e0
Add: リアクションのブロック判定にblockingReactionUserService.checkBlockedを追加
sakuhanight Nov 17, 2024
2665294
fix: code styleの修正
sakuhanight Nov 18, 2024
0301e86
Mod: Migrationファイルを再作成
sakuhanight Nov 18, 2024
da94dbe
Mod: UserReactionBlockingServiceとUserBlockingServiceを統合
sakuhanight Nov 18, 2024
34da11f
fix: import周りの諸々修正
sakuhanight Nov 18, 2024
292809a
fix: SPDXつけ忘れ
sakuhanight Nov 18, 2024
a96ae92
fix: ReactionService
sakuhanight Nov 18, 2024
ac95b12
Merge branch 'develop' into misskey-dev/blocking-reaction-user
sakuhanight Nov 18, 2024
fd9b7ed
fix: クエリとか修正
sakuhanight Nov 19, 2024
6236c93
fix: code style の修正
sakuhanight Nov 19, 2024
65ff7b1
Merge branch 'misskey-dev/develop' into misskey-dev/blocking-reaction…
sakuhanight Nov 22, 2024
8dad086
fix
sakuhanight Nov 24, 2024
314fbc3
fix
sakuhanight Nov 24, 2024
52c18ae
Merge branch 'develop' into misskey-dev/blocking-reaction-user
sakuhanight Nov 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,14 @@ export interface Locale extends ILocale {
* ブロック解除
*/
"unblock": string;
/**
* リアクションをブロック
*/
"blockReactionUser": string;
/**
* リアクションのブロックを解除
*/
"unblockReactionUser": string;
/**
* 凍結
*/
Expand All @@ -622,6 +630,14 @@ export interface Locale extends ILocale {
* ブロック解除しますか?
*/
"unblockConfirm": string;
/**
* リアクションをブロックしますか?
*/
"blockReactionUserConfirm": string;
/**
* リアクションのブロックを解除しますか?
*/
"unblockReactionUserConfirm": string;
/**
* 凍結しますか?
*/
Expand Down Expand Up @@ -994,6 +1010,10 @@ export interface Locale extends ILocale {
* ブロックしたユーザー
*/
"blockedUsers": string;
/**
* リアクションをブロックしたユーザー
*/
"reactionBlockedUsers": string;
/**
* ユーザーはいません
*/
Expand Down
5 changes: 5 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,14 @@ renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
block: "ブロック"
unblock: "ブロック解除"
blockReactionUser: "リアクションをブロック"
unblockReactionUser: "リアクションのブロックを解除"
suspend: "凍結"
unsuspend: "解凍"
blockConfirm: "ブロックしますか?"
unblockConfirm: "ブロック解除しますか?"
blockReactionUserConfirm: "リアクションをブロックしますか?"
unblockReactionUserConfirm: "リアクションのブロックを解除しますか?"
suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
Expand Down Expand Up @@ -244,6 +248,7 @@ federationAllowedHostsDescription: "連合を許可するサーバーのホス
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
reactionBlockedUsers: "リアクションをブロックしたユーザー"
noUsers: "ユーザーはいません"
editProfile: "プロフィールを編集"
noteDeleteConfirm: "このノートを削除しますか?"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
23 changes: 21 additions & 2 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,6 +25,8 @@ export class CacheService implements OnApplicationShutdown {
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public userReactionBlockingCache: RedisKVCache<Set<string>>; // NOTE: リアクションBlockキャッシュ
public userReactionBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」リアクションBlockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;

Expand Down Expand Up @@ -80,15 +83,31 @@ export class CacheService implements OnApplicationShutdown {
this.userBlockingCache = new RedisKVCache<Set<string>>(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)),
});

this.userBlockedCache = new RedisKVCache<Set<string>>(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<Set<string>>(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<Set<string>>(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)),
});
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/GlobalEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions packages/backend/src/core/QueryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -72,7 +73,8 @@ export class QueryService {
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, 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 });

// 投稿の作者にブロックされていない かつ
// 投稿の返信先の作者にブロックされていない かつ
Expand All @@ -97,7 +99,8 @@ export class QueryService {
public generateBlockQueryForUsers(q: SelectQueryBuilder<any>, 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')
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/core/ReactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Expand Down
102 changes: 96 additions & 6 deletions packages/backend/src/core/UserBlockingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -169,6 +189,7 @@ 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.
// But we don't need to do this because we are not using them in this function.
blocking.blocker = blocker;
blocking.blockee = blockee;

Expand All @@ -193,4 +214,73 @@ export class UserBlockingService implements OnModuleInit {
public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このServiceは通常のブロック向けかと思いますが、ここでリアクションブロック向けの機能を更新しているのは特殊な理由がありますか…?


this.globalEventService.publishInternalEvent('blockingReactionDeleted', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらも、配信するイベントが変わっています

blockerId: blocker.id,
blockeeId: blockee.id,
});
}

@bindThis
public async checkReactionBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
return (await this.cacheService.userReactionBlockingCache.fetch(blockerId)).has(blockeeId);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class BlockingEntityService {
blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
schema: 'UserDetailedNotMe',
}),
blockType: blocking.blockType,
});
}

Expand Down
Loading
Loading