Skip to content

Commit

Permalink
Add: 特定ユーザーからのリアクションをブロックする機能
Browse files Browse the repository at this point in the history
- BlockingReactionUserエンティティの追加
- blocking-reaction-userエンドポイントの追加
- 関連するフロントエンドの追加
Migrationがあります。
  • Loading branch information
sakuhanight committed Nov 17, 2024
1 parent 413a2e9 commit a8f0627
Show file tree
Hide file tree
Showing 29 changed files with 798 additions and 7 deletions.
5 changes: 5 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,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"
Expand Down Expand Up @@ -245,6 +249,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?"
Expand Down
20 changes: 20 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,14 @@ export interface Locale extends ILocale {
* ブロック解除
*/
"unblock": string;
/**
* リアクションをブロック
*/
"blockReactionUser": string;
/**
* リアクションのブロックを解除
*/
"unblockReactionUser": string;
/**
* 凍結
*/
Expand All @@ -630,6 +638,14 @@ export interface Locale extends ILocale {
* ブロック解除しますか?
*/
"unblockConfirm": string;
/**
* リアクションをブロックしますか?
*/
"blockReactionUserConfirm": string;
/**
* リアクションのブロックを解除しますか?
*/
"unblockReactionUserConfirm": string;
/**
* 凍結しますか?
*/
Expand Down Expand Up @@ -1002,6 +1018,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 @@ -149,10 +149,14 @@ renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
block: "ブロック"
unblock: "ブロック解除"
blockReactionUser: "リアクションをブロック"
unblockReactionUser: "リアクションのブロックを解除"
suspend: "凍結"
unsuspend: "解凍"
blockConfirm: "ブロックしますか?"
unblockConfirm: "ブロック解除しますか?"
blockReactionUserConfirm: "リアクションをブロックしますか?"
unblockReactionUserConfirm: "リアクションのブロックを解除しますか?"
suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
Expand Down Expand Up @@ -246,6 +250,7 @@ federationAllowedHostsDescription: "連合を許可するサーバーのホス
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
reactionBlockedUsers: "リアクションをブロックしたユーザー"
noUsers: "ユーザーはいません"
editProfile: "プロフィールを編集"
noteDeleteConfirm: "このノートを削除しますか?"
Expand Down
5 changes: 5 additions & 0 deletions locales/ja-KS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,14 @@ renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
block: "ブロック"
unblock: "ブロックやめたる"
blockReactionUser: "リアクションをブロックしたる"
unblockReactionUser: "リアクションのブロックをやめたる"
suspend: "凍結"
unsuspend: "溶かす"
blockConfirm: "ブロックしてもええんか?"
unblockConfirm: "ブロックやめたるってほんまか?"
blockReactionUserConfirm: "リアクションをブロックしてもええんか?"
unblockReactionUserConfirm: "リアクションのブロックをやめたるってほんまか?"
suspendConfirm: "凍結してしもうてええか?"
unsuspendConfirm: "解凍するけどええか?"
selectList: "リストを選ぶ"
Expand Down Expand Up @@ -239,6 +243,7 @@ mediaSilencedInstancesDescription: "メディアサイレンスしたいサー
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしとるユーザー"
blockedUsers: "ブロックしとるユーザー"
reactionBlockedUsers: "リアクションブロックしとるユーザー"
noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートをほかしてええか?"
Expand Down
Original file line number Diff line number Diff line change
@@ -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";`);
}
}
110 changes: 110 additions & 0 deletions packages/backend/src/core/BlockingReactionUserService.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
return (await this.cacheService.blockingReactionUserCache.fetch(blockerId)).has(blockeeId);
}
}
33 changes: 32 additions & 1 deletion packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,6 +34,8 @@ export class CacheService implements OnApplicationShutdown {
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public blockingReactionUserCache: RedisKVCache<Set<string>>; // NOTE: リアクションブロックするユーザーのキャッシュ
public blockedReactionUserCache: RedisKVCache<Set<string>>; // NOTE: リアクションブロックされるユーザーのキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;

Expand All @@ -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,

Expand Down Expand Up @@ -93,6 +108,22 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});

this.blockingReactionUserCache = new RedisKVCache<Set<string>>(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<Set<string>>(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<Set<string>>(this.redisClient, 'renoteMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/core/CoreModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -170,6 +172,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 };
Expand Down Expand Up @@ -254,6 +257,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 };
Expand Down Expand Up @@ -322,6 +326,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService,
AchievementService,
AvatarDecorationService,
BlockingReactionUserService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
Expand Down Expand Up @@ -406,6 +411,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppEntityService,
AuthSessionEntityService,
BlockingEntityService,
BlockingReactionUserEntityService,
ChannelEntityService,
ClipEntityService,
DriveFileEntityService,
Expand Down Expand Up @@ -472,6 +478,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService,
$AchievementService,
$AvatarDecorationService,
$BlockingReactionUserService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
Expand Down Expand Up @@ -556,6 +563,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppEntityService,
$AuthSessionEntityService,
$BlockingEntityService,
$BlockingReactionUserEntityService,
$ChannelEntityService,
$ClipEntityService,
$DriveFileEntityService,
Expand Down Expand Up @@ -621,6 +629,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService,
AchievementService,
AvatarDecorationService,
BlockingReactionUserService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
Expand Down Expand Up @@ -704,6 +713,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppEntityService,
AuthSessionEntityService,
BlockingEntityService,
BlockingReactionUserEntityService,
ChannelEntityService,
ClipEntityService,
DriveFileEntityService,
Expand Down Expand Up @@ -852,6 +862,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppEntityService,
$AuthSessionEntityService,
$BlockingEntityService,
$BlockingReactionUserEntityService,
$ChannelEntityService,
$ClipEntityService,
$DriveFileEntityService,
Expand Down
Loading

0 comments on commit a8f0627

Please sign in to comment.