Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
80 changes: 76 additions & 4 deletions packages/backend/src/core/entities/NoteEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { CacheService } from '@/core/CacheService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
Expand Down Expand Up @@ -101,6 +102,7 @@ export class NoteEntityService implements OnModuleInit {
//private reactionService: ReactionService,
//private reactionsBufferingService: ReactionsBufferingService,
//private idService: IdService,
private cacheService: CacheService,
) {
}

Expand Down Expand Up @@ -376,7 +378,46 @@ export class NoteEntityService implements OnModuleInit {
: this.meta.enableReactionsBuffering
? await this.reactionsBufferingService.get(note.id)
: { deltas: {}, pairs: [] };
const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));

let reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));
if (meId) {
// ログインユーザーがいる場合のみ、ブロック/ミュートユーザーを除外して集計し直す
// 1. ブロック・ミュートリストを取得
const [mutedIds, blockedIds] = await Promise.all([
this.cacheService.userMutingsCache.fetch(meId),
this.cacheService.userBlockingCache.fetch(meId),
]);

// 2. DBとバッファから、フィルタリングに必要な全ユーザー/リアクションペアを取得
// DBからの全リアクションレコードを取得
const dbReactions = await this.noteReactionsRepository.findBy({ noteId: note.id });

// バッファリングされたペアを追加
const bufferedPairs = bufferedReactions.pairs ?? []; // pairs: ([MiUser['id'], string])[]

// 3. フィルタリングして再集計
const filteredReactions: Record<string, number> = {};

// 3a. DBからのリアクションをフィルタリング
for (const reaction of dbReactions) {
const isBlockedOrMuted = blockedIds.has(reaction.userId) || mutedIds.has(reaction.userId);
if (!isBlockedOrMuted) {
const reactionName = this.reactionService.convertLegacyReaction(reaction.reaction);
filteredReactions[reactionName] = (filteredReactions[reactionName] || 0) + 1;
}
}

// 3b. バッファからのリアクションをフィルタリング
for (const [userId, reactionName] of bufferedPairs) {
const isBlockedOrMuted = blockedIds.has(userId) || mutedIds.has(userId);
if (!isBlockedOrMuted) {
const normalizedReaction = this.reactionService.convertLegacyReaction(reactionName);
filteredReactions[normalizedReaction] = (filteredReactions[normalizedReaction] || 0) + 1;
}
}

reactions = filteredReactions;
}

const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));

Expand Down Expand Up @@ -600,7 +641,7 @@ export class NoteEntityService implements OnModuleInit {
}

@bindThis
public async fetchDiffs(noteIds: MiNote['id'][]) {
public async fetchDiffs(noteIds: MiNote['id'][], meId: MiUser['id'] | null) {
if (noteIds.length === 0) return [];

const notes = await this.notesRepository.find({
Expand All @@ -617,12 +658,43 @@ export class NoteEntityService implements OnModuleInit {

const bufferedReactionsMap = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(noteIds) : null;

const packings = notes.map(note => {
const packings = notes.map(async note => {
const bufferedReactions = bufferedReactionsMap?.get(note.id);
//const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));

const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.deltas ?? {}));
let reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.deltas ?? {}));

if (meId) {
const [mutedIds, blockedIds] = await Promise.all([
this.cacheService.userMutingsCache.fetch(meId),
this.cacheService.userBlockingCache.fetch(meId),
]);

// 2. DBとバッファから、フィルタリングに必要な全ユーザー/リアクションペアを取得
const dbReactions = await this.noteReactionsRepository.findBy({ noteId: note.id });
const bufferedPairs = bufferedReactions?.pairs ?? [];

const filteredReactions: Record<string, number> = {};

// 3a. DBからのリアクションをフィルタリング
for (const reaction of dbReactions) {
const isBlockedOrMuted = blockedIds.has(reaction.userId) || mutedIds.has(reaction.userId);
if (!isBlockedOrMuted) {
const reactionName = this.reactionService.convertLegacyReaction(reaction.reaction);
filteredReactions[reactionName] = (filteredReactions[reactionName] || 0) + 1;
}
}
// 3b. バッファからのリアクションをフィルタリング
for (const [userId, reactionName] of bufferedPairs) {
const isBlockedOrMuted = blockedIds.has(userId) || mutedIds.has(userId);
if (!isBlockedOrMuted) {
const normalizedReaction = this.reactionService.convertLegacyReaction(reactionName);
filteredReactions[normalizedReaction] = (filteredReactions[normalizedReaction] || 0) + 1;
}
}

reactions = filteredReactions;
}
const reactionEmojiNames = Object.keys(reactions)
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
Expand Down
69 changes: 61 additions & 8 deletions packages/backend/src/core/entities/NoteReactionEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import type { NoteReactionsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { } from '@/models/Blocking.js';
import { CacheService } from '@/core/CacheService.js';
import type { MiUser } from '@/models/User.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js';
import type { OnModuleInit } from '@nestjs/common';
import type { } from '@/models/Blocking.js';
import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
Expand All @@ -24,6 +25,7 @@ export class NoteReactionEntityService implements OnModuleInit {
private noteEntityService: NoteEntityService;
private reactionService: ReactionService;
private idService: IdService;
private cacheService: CacheService;

constructor(
private moduleRef: ModuleRef,
Expand All @@ -35,6 +37,7 @@ export class NoteReactionEntityService implements OnModuleInit {
//private noteEntityService: NoteEntityService,
//private reactionService: ReactionService,
//private idService: IdService,
//private cacheService: CacheService,
) {
}

Expand All @@ -43,6 +46,7 @@ export class NoteReactionEntityService implements OnModuleInit {
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.reactionService = this.moduleRef.get('ReactionService');
this.idService = this.moduleRef.get('IdService');
this.cacheService = this.moduleRef.get('CacheService');
}

@bindThis
Expand Down Expand Up @@ -75,10 +79,30 @@ export class NoteReactionEntityService implements OnModuleInit {
): Promise<Packed<'NoteReaction'>[]> {
const opts = Object.assign({
}, options);
const _users = reactions.map(({ user, userId }) => user ?? userId);
const meId = me ? me.id : null;

// ログインユーザーがいる場合のみ、ブロック・ミュートリストを取得
let muted: Set<string> | null = null;
let blocked: Set<string> | null = null;
let newReactions: MiNoteReaction[] = reactions;

if (meId) {
[blocked, muted] = await Promise.all([
this.cacheService.userBlockingCache.fetch(meId), // 自分がブロックしたユーザー
this.cacheService.userMutingsCache.fetch(meId), // 自分がミュートしたユーザー
]);

const filteredReactions = reactions.filter(reaction => {
const isBlockedOrMuted = blocked!.has(reaction.userId) || muted!.has(reaction.userId);
return !isBlockedOrMuted;
});

newReactions = filteredReactions;
}
const _users = newReactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
return Promise.all(newReactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}

@bindThis
Expand All @@ -94,6 +118,22 @@ export class NoteReactionEntityService implements OnModuleInit {
}, options);

const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
const meId = me ? me.id : null;

// ログインユーザーがいる場合のみ、ブロック・ミュートリストを取得
let muted: Set<string> | null = null;
let blocked: Set<string> | null = null;

if (meId) {
[blocked, muted] = await Promise.all([
this.cacheService.userBlockingCache.fetch(meId), // 自分がブロックしたユーザー
this.cacheService.userMutingsCache.fetch(meId), // 自分がミュートしたユーザー
]);

if (reaction.userId && (blocked?.has(reaction.userId) || muted?.has(reaction.userId))) {
return {} as any; // ミュート・ブロックされている場合は空オブジェクトを返す
}
}

return {
id: reaction.id,
Expand All @@ -110,11 +150,24 @@ export class NoteReactionEntityService implements OnModuleInit {
me?: { id: MiUser['id'] } | null | undefined,
options?: object,
): Promise<Packed<'NoteReactionWithNote'>[]> {
const opts = Object.assign({
}, options);
const _users = reactions.map(({ user, userId }) => user ?? userId);
const opts = Object.assign({}, options);

// キャッシュからミュート・ブロック情報を取得
const blocked = me ? await this.cacheService.userBlockedCache.fetch(me.id) : null;
const muted = me ? await this.cacheService.userMutingsCache.fetch(me.id) : null;

// ミュート・ブロックされたユーザーのリアクションを除外
const filteredReactions = reactions.filter(reaction => {
if (!me) return true;
return !(blocked?.has(reaction.userId) || muted?.has(reaction.userId));
});

const _users = filteredReactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.packWithNote(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));

return Promise.all(filteredReactions.map(reaction =>
this.packWithNote(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) }),
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
) {
super(meta, paramDef, async (ps, me) => {
return await this.noteEntityService.fetchDiffs(ps.noteIds);
return await this.noteEntityService.fetchDiffs(ps.noteIds, me?.id ?? null);
});
}
}
25 changes: 22 additions & 3 deletions packages/frontend/src/composables/use-note-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ export function useNoteCapture(props: {
parentNote: Misskey.entities.Note | null;
mock?: boolean;
}): {
$note: Reactive<ReactiveNoteData>;
subscribe: () => void;
} {
$note: Reactive<ReactiveNoteData>;
subscribe: () => void;
} {
const { note, parentNote, mock } = props;

const $note = reactive<ReactiveNoteData>({
Expand Down Expand Up @@ -227,6 +227,16 @@ export function useNoteCapture(props: {
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
let normalizedName = ctx.reaction.replace(/^:(\w+):$/, ':$1@.:');
normalizedName = normalizedName.match('\u200d') ? normalizedName : normalizedName.replace(/\ufe0f/g, '');
const blockedIds: Set<string> = ($i as any)?.blockedIds ?? new Set();
const mutedIds: Set<string> = ($i as any)?.mutedIds ?? new Set();
const isBlocked = blockedIds.has(ctx.userId);
const isMuted = mutedIds.has(ctx.userId);

if (isBlocked || isMuted) {
// ブロック/ミュートユーザーからのリアクションは集計に含めず、処理を終了
return;
}

if (reactionUserMap.has(ctx.userId) && reactionUserMap.get(ctx.userId) === normalizedName) return;
reactionUserMap.set(ctx.userId, normalizedName);

Expand All @@ -247,6 +257,15 @@ export function useNoteCapture(props: {
function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
let normalizedName = ctx.reaction.replace(/^:(\w+):$/, ':$1@.:');
normalizedName = normalizedName.match('\u200d') ? normalizedName : normalizedName.replace(/\ufe0f/g, '');
const blockedIds: Set<string> = ($i as any)?.blockedIds ?? new Set();
const mutedIds: Set<string> = ($i as any)?.mutedIds ?? new Set();
const isBlocked = blockedIds.has(ctx.userId);
const isMuted = mutedIds.has(ctx.userId);

if (isBlocked || isMuted) {
// ブロック/ミュートユーザーによるリアクション削除は無視する
return;
}

// 確実に一度リアクションされて取り消されている場合のみ処理をとめる(APIで初回読み込み→Streamでアップデート等の場合、reactionUserMapに情報がないため)
if (reactionUserMap.has(ctx.userId) && reactionUserMap.get(ctx.userId) === noReaction) return;
Expand Down
Loading