Compare commits
4 Commits
fada70b98a
...
e5989a53ca
| Author | SHA1 | Date |
|---|---|---|
|
|
e5989a53ca | |
|
|
65e51463c8 | |
|
|
39362f78a6 | |
|
|
05156ca854 |
|
|
@ -31,6 +31,7 @@
|
|||
- JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
|
||||
- 従来の実装(12 February 2022版・HTML Microformat形式)も引き続きサポートされます
|
||||
- Enhance: メモリ使用量を削減
|
||||
- Fix: `/admin/get-user-ips` エンドポイントのアクセス権限を管理者のみに修正
|
||||
|
||||
## 2025.12.2
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -101,6 +102,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
//private reactionService: ReactionService,
|
||||
//private reactionsBufferingService: ReactionsBufferingService,
|
||||
//private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -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('/')));
|
||||
|
||||
|
|
@ -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({
|
||||
|
|
@ -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(':', ''));
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
|
|
@ -35,6 +37,7 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||
//private noteEntityService: NoteEntityService,
|
||||
//private reactionService: ReactionService,
|
||||
//private idService: IdService,
|
||||
//private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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) }),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'read:admin:user-ips',
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,6 +227,16 @@ export function useNoteCapture(props: {
|
|||
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; } | null; }): 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);
|
||||
|
||||
|
|
@ -247,6 +257,15 @@ export function useNoteCapture(props: {
|
|||
function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; } | null; }): 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;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.body">
|
||||
<div :class="$style.top">
|
||||
<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="view-transition-name: navbar-serverIcon;"/>
|
||||
</button>
|
||||
<button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
|
||||
<i v-if="store.r.realtimeMode.value" class="ti ti-bolt ti-fw"></i>
|
||||
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="$style.middle">
|
||||
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
|
||||
<i :class="$style.itemIcon" class="ti ti-home ti-fw" style="viewTransitionName: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
|
||||
<i :class="$style.itemIcon" class="ti ti-home ti-fw" style="view-transition-name: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
|
||||
</MkA>
|
||||
<template v-for="item in prefer.r.menu.value">
|
||||
<div v-if="item === '-'" :class="$style.divider"></div>
|
||||
|
|
@ -43,14 +43,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
<div :class="$style.divider"></div>
|
||||
<MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
|
||||
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw" style="viewTransitionName: navbar-controlPanel;"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
|
||||
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw" style="view-transition-name: navbar-controlPanel;"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.item" @click="more">
|
||||
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw" style="viewTransitionName: navbar-more;"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
|
||||
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw" style="view-transition-name: navbar-more;"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
|
||||
<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||
</button>
|
||||
<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
|
||||
<i :class="$style.itemIcon" class="ti ti-settings ti-fw" style="viewTransitionName: navbar-settings;"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
|
||||
<i :class="$style.itemIcon" class="ti ti-settings ti-fw" style="view-transition-name: navbar-settings;"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
|
||||
</MkA>
|
||||
</div>
|
||||
<div :class="$style.bottom">
|
||||
|
|
@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
|
||||
</button>
|
||||
<button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
|
||||
<MkAvatar :user="$i" :class="$style.avatar" style="viewTransitionName: navbar-avatar;"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
|
||||
<MkAvatar :user="$i" :class="$style.avatar" style="view-transition-name: navbar-avatar;"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue