Merge commit from fork

* Tighten security in `HashtagChannel`

* Add isNoteVisibleForMe in stream channel

Co-Authored-By: Julia Johannesen <julia@insertdomain.name>

* Tighten note visibility checks in WebSocket (No.1)

* refactor

* Fix main channel

Co-Authored-By: Julia Johannesen <julia@insertdomain.name>

* fix typo

* fix missing lockdown (requireSigninToViewContents) checks

* fix(backend): streamingでのロックダウン挙動修正

* fix: 引用リノートを無条件で隠していた問題を修正

* fix: 引用リノートを単純にリノート場合に内容が見えることがある問題を修正

* refac

* fix

* fix

* fix

* Update docs

---------

Co-authored-by: Julia Johannesen <julia@insertdomain.name>
Co-authored-by: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com>
This commit is contained in:
かっこかり
2026-03-09 08:15:31 +09:00
committed by GitHub
parent a07dc589e7
commit b361a10c48
15 changed files with 346 additions and 103 deletions

View File

@@ -259,7 +259,7 @@ export class QueryService {
@bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
// This code must always be synchronized with the checks in NoteEntityService.isVisibleForMe and Stream abstract class Channel.isNoteVisibleForMe.
if (me == null) {
q.andWhere(new Brackets(qb => {
qb

View File

@@ -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';
@@ -66,6 +67,7 @@ export class NoteEntityService implements OnModuleInit {
private reactionService: ReactionService;
private reactionsBufferingService: ReactionsBufferingService;
private idService: IdService;
private cacheService: CacheService;
private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor(
@@ -101,6 +103,7 @@ export class NoteEntityService implements OnModuleInit {
//private reactionService: ReactionService,
//private reactionsBufferingService: ReactionsBufferingService,
//private idService: IdService,
//private cacheService: CacheService,
) {
}
@@ -111,6 +114,7 @@ export class NoteEntityService implements OnModuleInit {
this.reactionService = this.moduleRef.get('ReactionService');
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
this.idService = this.moduleRef.get('IdService');
this.cacheService = this.moduleRef.get('CacheService');
}
@bindThis
@@ -125,66 +129,57 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
if (meId === packedNote.userId) return;
public async shouldHideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<boolean> {
if (meId === packedNote.userId) return false;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
return true;
}
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
hide = true;
}
return true;
}
// visibility が specified かつ自分が指定されていなかったら非表示
if (!hide) {
if (packedNote.visibility === 'specified') {
if (meId == null) {
hide = true;
return true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (!specified) {
hide = true;
}
return true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (!hide) {
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
return true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
return false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
hide = false;
return false;
} else {
// フォロワーかどうか
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.userId,
followerId: meId,
},
});
hide = !isFollowing;
const followings = await this.cacheService.userFollowingsCache.fetch(meId);
if (!Object.hasOwn(followings, packedNote.userId)) {
return true;
}
}
}
if (hide) {
return false;
}
@bindThis
public hideNote(packedNote: Packed<'Note'>): void {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
@@ -194,7 +189,6 @@ export class NoteEntityService implements OnModuleInit {
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
}
@bindThis
private async populatePoll(note: MiNote, meId: MiUser['id'] | null) {
@@ -278,7 +272,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
// This code must always be synchronized with the checks in generateVisibilityQuery.
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
@@ -468,8 +462,8 @@ export class NoteEntityService implements OnModuleInit {
this.treatVisibility(packed);
if (!opts.skipHide) {
await this.hideNote(packed, meId);
if (!opts.skipHide && await this.shouldHideNote(packed, meId)) {
this.hideNote(packed);
}
return packed;

View File

@@ -49,6 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
import { ReversiChannel } from './api/stream/channels/reversi.js';
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
@@ -98,6 +99,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
QueueStatsChannel,
ServerStatsChannel,
UserListChannel,
NoteStreamingHidingService,
OpenApiServerService,
OAuth2ProviderService,
],

View File

@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiUser } from '@/models/User.js';
type HiddenLayer = 'note' | 'renote' | 'renoteRenote';
type LockdownCheckResult =
| { shouldSkip: true }
| { shouldSkip: false; hiddenLayers: Set<HiddenLayer> };
/** Streamにおいて、ートを隠すhideNoteを適用するためのService */
@Injectable()
export class NoteStreamingHidingService {
constructor(
private noteEntityService: NoteEntityService,
) {}
/**
* ノートの可視性を判定する
*
* @param note - 判定対象のノート
* @param meId - 閲覧者のユーザーID未ログインの場合はnull
* @returns shouldSkip: true の場合はートを流さない、false の場合は hiddenLayers に基づいて隠す
*/
@bindThis
public async shouldHide(
note: Packed<'Note'>,
meId: MiUser['id'] | null,
): Promise<LockdownCheckResult> {
const hiddenLayers = new Set<HiddenLayer>();
// 1階層目: note自体
const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId);
if (shouldHideThisNote) {
if (isRenotePacked(note) && isQuotePacked(note)) {
// 引用リノートの場合、内容を隠して流す
hiddenLayers.add('note');
} else if (isRenotePacked(note)) {
// 純粋リノートの場合、流さない
return { shouldSkip: true };
} else {
// 通常ノートの場合、内容を隠して流す
hiddenLayers.add('note');
}
}
// 2階層目: note.renote
if (isRenotePacked(note) && note.renote) {
const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, meId);
if (shouldHideRenote) {
if (isQuotePacked(note)) {
// noteが引用リートの場合、renote部分だけ隠す
hiddenLayers.add('renote');
} else {
// noteが純粋リートの場合、流さない
return { shouldSkip: true };
}
}
}
// 3階層目: note.renote.renote
if (isRenotePacked(note) && note.renote &&
isRenotePacked(note.renote) && note.renote.renote) {
const shouldHideRenoteRenote = await this.noteEntityService.shouldHideNote(note.renote.renote, meId);
if (shouldHideRenoteRenote) {
if (isQuotePacked(note.renote)) {
// note.renoteが引用リートの場合、renote.renote部分だけ隠す
hiddenLayers.add('renoteRenote');
} else {
// note.renoteが純粋リートの場合、note.renoteの意味がなくなるので流さない
return { shouldSkip: true };
}
}
}
return { shouldSkip: false, hiddenLayers };
}
/**
* hiddenLayersに基づいてートの内容を隠す。
*
* この処理は渡された `note` を直接変更します。
*
* @param note - 処理対象のノート
* @param hiddenLayers - 隠す階層のセット
*/
@bindThis
public applyHiding(
note: Packed<'Note'>,
hiddenLayers: Set<HiddenLayer>,
): void {
if (hiddenLayers.has('note')) {
this.noteEntityService.hideNote(note);
}
if (hiddenLayers.has('renote') && note.renote) {
this.noteEntityService.hideNote(note.renote);
}
if (hiddenLayers.has('renoteRenote') && note.renote && note.renote.renote) {
this.noteEntityService.hideNote(note.renote.renote);
}
}
/**
* ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。
*
* この処理は渡された `note` を直接変更します。
*
* @param note - 処理対象のノート(必要に応じて内容が隠される)
* @param meId - 閲覧者のユーザーID未ログインの場合はnull
* @returns shouldSkip: true の場合はノートを流さない
*/
@bindThis
public async processHiding(
note: Packed<'Note'>,
meId: MiUser['id'] | null,
): Promise<{ shouldSkip: boolean }> {
const result = await this.shouldHide(note, meId);
if (result.shouldSkip) {
return { shouldSkip: true };
}
this.applyHiding(note, result.hiddenLayers);
return { shouldSkip: false };
}
}

View File

@@ -64,6 +64,43 @@ export default abstract class Channel {
return this.connection.subscriber;
}
protected isNoteVisibleForMe(note: Packed<'Note'>): boolean {
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
const meId = this.connection.user?.id ?? null;
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
return note.visibleUserIds?.some(id => meId === id) ?? false;
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
return Object.hasOwn(this.following, note.userId);
}
}
return true;
}
/*
* ミュートとブロックされてるを処理する
*/

View File

@@ -5,7 +5,9 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
@@ -24,6 +26,7 @@ export class AntennaChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onEvent = this.onEvent.bind(this);
@@ -43,8 +46,21 @@ export class AntennaChannel extends Channel {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
} else {
this.send(data.type, data.body);

View File

@@ -6,6 +6,7 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@@ -26,6 +27,7 @@ export class ChannelChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -48,14 +50,20 @@ export class ChannelChannel extends Channel {
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@@ -29,6 +30,7 @@ export class GlobalTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -60,12 +62,17 @@ export class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}

View File

@@ -7,12 +7,12 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class HashtagChannel extends Channel {
public readonly chName = 'hashtag';
@@ -25,6 +25,7 @@ export class HashtagChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -33,7 +34,11 @@ export class HashtagChannel extends Channel {
@bindThis
public async init(params: JsonObject) {
if (!Array.isArray(params.q)) return;
if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
if (!params.q.every((x): x is string[] => (
Array.isArray(x) &&
x.length >= 1 &&
x.every(y => typeof y === 'string')
))) return;
this.q = params.q;
// Subscribe stream
@@ -46,14 +51,23 @@ export class HashtagChannel extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
if (!this.isNoteVisibleForMe(note)) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}

View File

@@ -6,6 +6,7 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
@@ -26,6 +27,7 @@ export class HomeTimelineChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -55,11 +57,7 @@ export class HomeTimelineChannel extends Channel {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
}
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (note.reply) {
const reply = note.reply;
@@ -84,12 +82,17 @@ export class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@@ -31,6 +32,7 @@ export class HybridTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -75,12 +77,7 @@ export class HybridTimelineChannel extends Channel {
}
}
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
@@ -104,12 +101,17 @@ export class HybridTimelineChannel extends Channel {
}
}
if (this.user && note.renoteId && !note.text) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
@@ -30,6 +31,7 @@ export class LocalTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -70,12 +72,17 @@ export class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}

View File

@@ -47,8 +47,8 @@ export class MainChannel extends Channel {
}
case 'mention': {
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (this.userIdsWhoMeMuting.has(data.body.userId)) return;
if (!this.isNoteVisibleForMe(data.body)) return;
if (this.isNoteMutedOrBlocked(data.body)) return;
if (data.body.isHidden) {
const note = await this.noteEntityService.pack(data.body.id, this.user, {
detail: true,

View File

@@ -7,6 +7,8 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
@@ -25,6 +27,7 @@ export class RoleTimelineChannel extends Channel {
private noteEntityService: NoteEntityService,
private roleservice: RoleService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -47,9 +50,24 @@ export class RoleTimelineChannel extends Channel {
return;
}
if (note.visibility !== 'public') return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
} else {
this.send(data.type, data.body);

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@@ -36,6 +37,7 @@ export class UserListChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.updateListUsers = this.updateListUsers.bind(this);
@@ -96,11 +98,7 @@ export class UserListChannel extends Channel {
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (note.reply) {
const reply = note.reply;
@@ -117,12 +115,17 @@ export class UserListChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
}