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 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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,20 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class AddBlockingReactionUser1731898598469 {
name = 'AddBlockingReactionUser1731898598469'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "blocking" ADD "isReactionBlock" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."isReactionBlock" IS 'Whether the blockee is a reaction block.'`);
await queryRunner.query(`CREATE INDEX "IDX_7b0698c38d27a5554bed4858bd" ON "blocking" ("isReactionBlock") `);
}

async down(queryRunner) {
await queryRunner.query(`DELETE FROM blocking WHERE "isReactionBlock" = 'true'`); // blockingテーブルのisReactionBlockカラムがtrueの行を削除する
await queryRunner.query(`DROP INDEX "IDX_7b0698c38d27a5554bed4858bd"`);
await queryRunner.query(`ALTER TABLE "blocking" DROP COLUMN "isReactionBlock"`);
}
}
Copy link
Member

Choose a reason for hiding this comment

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


この変更は、blocking.blockeeIdblocking.blockerIdの組み合わせが2つ以上存在しうることを許容する前提のように見受けられますが、相違ございませんか?(通常のブロックで1レコード、リアクションブロックで1レコードずつ)

もし上記の状態を期待するのであれば…やり方を変える必要があると考えます。
以下の画像の通り、blocking.blockeeIdblocking.blockerIdのユニークインデックスが既に存在しているので重複する組み合わせは登録できないようになっています。
image

なので…blockingの1レコードでやりくりする必要があるかと思います。
 

isReactionBlockよりもblockTypeとしてenumを保持するようにすると後から手を入れやすいかも…?

22 changes: 20 additions & 2 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 +82,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, isReactionBlock: false }, 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, isReactionBlock: false }, 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, isReactionBlock: true }, 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, isReactionBlock: true }, 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
6 changes: 6 additions & 0 deletions packages/backend/src/core/CoreModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js';
import { WebAuthnService } from './WebAuthnService.js';
import { UserBlockingService } from './UserBlockingService.js';
import { UserReactionBlockingService } from './UserReactionBlockingService.js';
import { CacheService } from './CacheService.js';
import { UserService } from './UserService.js';
import { UserFollowingService } from './UserFollowingService.js';
Expand Down Expand Up @@ -202,6 +203,7 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $UserReactionBlockingService: Provider = { provide: 'UserReactionBlockingService', useExisting: UserReactionBlockingService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
Expand Down Expand Up @@ -353,6 +355,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService,
WebAuthnService,
UserBlockingService,
UserReactionBlockingService,
CacheService,
UserService,
UserFollowingService,
Expand Down Expand Up @@ -500,6 +503,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService,
$WebAuthnService,
$UserBlockingService,
$UserReactionBlockingService,
$CacheService,
$UserService,
$UserFollowingService,
Expand Down Expand Up @@ -648,6 +652,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService,
WebAuthnService,
UserBlockingService,
UserReactionBlockingService,
CacheService,
UserService,
UserFollowingService,
Expand Down Expand Up @@ -794,6 +799,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService,
$WebAuthnService,
$UserBlockingService,
$UserReactionBlockingService,
$CacheService,
$UserService,
$UserFollowingService,
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
21 changes: 9 additions & 12 deletions packages/backend/src/core/UserBlockingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class UserBlockingService implements OnModuleInit {
blockerId: blocker.id,
blockee,
blockeeId: blockee.id,
isReactionBlock: false,
} as MiBlocking;

await this.blockingsRepository.insert(blocking);
Expand Down Expand Up @@ -160,6 +161,7 @@ export class UserBlockingService implements OnModuleInit {
const blocking = await this.blockingsRepository.findOneBy({
blockerId: blocker.id,
blockeeId: blockee.id,
isReactionBlock: false,
});

if (blocking == null) {
Expand All @@ -169,28 +171,23 @@ 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);

this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.cacheService.userReactionBlockedCache.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('blockingDeleted', {
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,
});

// deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox, false);
}
Copy link
Member

Choose a reason for hiding this comment

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

ActivityPub向けにブロックイベントの配信をする処理なのですが、こちらも意図を確認させてください

}

@bindThis
public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
return (await this.cacheService.userReactionBlockingCache.fetch(blockerId)).has(blockeeId);
}
}
111 changes: 111 additions & 0 deletions packages/backend/src/core/UserReactionBlockingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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 type { MiBlocking } from '@/models/Blocking.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import type { BlockingsRepository, UserListsRepository } 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 { UserFollowingService } from '@/core/UserFollowingService.js';

@Injectable()
export class UserReactionBlockingService {
private logger: Logger;
private userFollowingService: UserFollowingService;

constructor(
private moduleRef: ModuleRef,

@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,

@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,

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');
}

@bindThis
public async block(blocker: MiUser, blockee: MiUser, silent = false) {
const blocking = {
id: this.idService.gen(),
blocker,
blockerId: blocker.id,
blockee,
blockeeId: blockee.id,
isReactionBlock: true,
} satisfies MiBlocking;

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 unblock(blocker: MiUser, blockee: MiUser) {
const blocking = await this.blockingsRepository.findOneBy({
blockerId: blocker.id,
blockeeId: blockee.id,
isReactionBlock: true,
});

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.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);

this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
});

// deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox, false);
}
Copy link
Member

Choose a reason for hiding this comment

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

もしかして通常のブロック側と逆になっています…?

Copy link
Author

Choose a reason for hiding this comment

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

すみません、完全に全て逆になっていたようです。修正しました。

}

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