completely re-implement note visibility as NoteVisibilityService

This commit is contained in:
Hazelnoot 2025-08-12 23:40:49 -04:00
parent d1912362e0
commit 85ca2269e4
27 changed files with 925 additions and 420 deletions

View file

@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoin('(select "host" from "instance" where "isBubbled" = true)', 'bubbleInstance', '"bubbleInstance"."host" = "note"."userHost"')
.andWhere('"bubbleInstance" IS NOT NULL');
this.queryService
.leftJoin(query, 'note.userInstance', 'userInstance', '"userInstance"."host" = "bubbleInstance"."host"');
.leftJoin(query, 'note.userInstance', 'userInstance');
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);

View file

@ -18,6 +18,7 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -261,6 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private noteCreateService: NoteCreateService,
private readonly noteVisibilityService: NoteVisibilityService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.text && ps.text.length > this.config.maxNoteLength) {
@ -303,7 +305,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (isRenote(renote) && !isQuote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
} else if (!await this.noteEntityService.isVisibleForMe(renote, me.id)) {
} else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(renote, me)).accessible) {
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
}
@ -351,7 +353,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchReplyTarget);
} else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) {
} else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);

View file

@ -17,6 +17,7 @@ import { NoteEditService } from '@/core/NoteEditService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -311,6 +312,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private noteEditService: NoteEditService,
private readonly noteVisibilityService: NoteVisibilityService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.text && ps.text.length > this.config.maxNoteLength) {
@ -408,7 +410,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchReplyTarget);
} else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) {
} else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);

View file

@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { hasText } from '@/models/Note.js';
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -84,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
private readonly cacheService: CacheService,
private readonly loggerService: ApiLoggerService,
private readonly noteVisibilityService: NoteVisibilityService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
@ -91,7 +93,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
if (!(await this.noteEntityService.isVisibleForMe(note, me?.id ?? null, { me }))) {
const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(note, me);
if (!accessible) {
throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
}

View file

@ -220,7 +220,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateSuspendedUserQueryForNote(query, true);
this.queryService.generateSilencedUserQueryForNotes(query, me, true);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
this.queryService.generateMutedUserQueryForNotes(query, me, true);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}

View file

@ -12,6 +12,7 @@ import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { deepClone } from '@/misc/clone.js';
import type Connection from '@/server/api/stream/Connection.js';
import { NoteVisibilityFilters } from '@/core/NoteVisibilityService.js';
/**
* Stream channel
@ -26,6 +27,10 @@ export default abstract class Channel {
public static readonly requireCredential: boolean;
public static readonly kind?: string | null;
protected get noteVisibilityService() {
return this.noteEntityService.noteVisibilityService;
}
protected get user() {
return this.connection.user;
}
@ -105,8 +110,14 @@ export default abstract class Channel {
return this.connection.myRecentFavorites;
}
protected async checkNoteVisibility(note: Packed<'Note'>, filters?: NoteVisibilityFilters) {
// Don't use any of the local cached data, because this does everything through CacheService which is just as fast with updated data.
return await this.noteVisibilityService.checkNoteVisibilityAsync(note, this.user, { filters });
}
/**
* Checks if a note is visible to the current user *excluding* blocks and mutes.
* @deprecated use isNoteHidden instead
*/
protected isNoteVisibleToMe(note: Packed<'Note'>): boolean {
if (note.visibility === 'public') return true;
@ -120,8 +131,9 @@ export default abstract class Channel {
return note.visibleUserIds.includes(this.user.id);
}
/*
/**
*
* @deprecated use isNoteHidden instead
*/
protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean {
// Ignore notes that require sign-in
@ -196,12 +208,11 @@ export default abstract class Channel {
// If we didn't clone the notes here, different connections would asynchronously write
// different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
const clonedNote = deepClone(note);
const notes = crawl(clonedNote);
// Hide notes before everything else, since this modifies fields that the other functions will check.
await this.noteEntityService.hideNotes(notes, this.user.id);
const notes = crawl(clonedNote);
const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings] = await Promise.all([
const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings, myFollowings] = await Promise.all([
this.noteEntityService.populateMyReactions(notes, this.user.id, {
myReactions: this.myRecentReactions,
}),
@ -213,13 +224,25 @@ export default abstract class Channel {
}),
this.noteEntityService.populateMyTheadMutings(notes, this.user.id),
this.noteEntityService.populateMyNoteMutings(notes, this.user.id),
this.cacheService.userFollowingsCache.fetch(this.user.id),
]);
note.myReaction = myReactions.get(note.id) ?? null;
note.isRenoted = myRenotes.has(note.id);
note.isFavorited = myFavorites.has(note.id);
note.isMutingThread = myThreadMutings.has(note.id);
note.isMutingNote = myNoteMutings.has(note.id);
for (const n of notes) {
// Sync visibility in case there's something like "makeNotesFollowersOnlyBefore" enabled
this.noteVisibilityService.syncVisibility(n);
n.myReaction = myReactions.get(n.id) ?? null;
n.isRenoted = myRenotes.has(n.id);
n.isFavorited = myFavorites.has(n.id);
n.isMutingThread = myThreadMutings.has(n.id);
n.isMutingNote = myNoteMutings.has(n.id);
n.user.bypassSilence = n.userId === this.user.id || myFollowings.has(n.userId);
}
// Hide notes *after* we sync visibility
await this.noteEntityService.hideNotes(notes, this.user.id, {
userFollowings: myFollowings,
});
return clonedNote;
}

View file

@ -41,7 +41,8 @@ class AntennaChannel extends Channel {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
if (this.isNoteMutedOrBlocked(note)) return;
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
if (!accessible || silence) return;
this.send('note', note);
} else {

View file

@ -8,9 +8,9 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import { UtilityService } from '@/core/UtilityService.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import Channel, { MiChannelService } from '../channel.js';
class BubbleTimelineChannel extends Channel {
@ -47,8 +47,6 @@ class BubbleTimelineChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user?.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (!this.withBots && note.user.isBot) return;
@ -56,27 +54,9 @@ class BubbleTimelineChannel extends Channel {
if (note.channelId != null) return;
if (!this.utilityService.isBubbledHost(note.user.host)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
}
}
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note);
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class ChannelChannel extends Channel {
@ -45,9 +46,9 @@ class ChannelChannel extends Channel {
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -48,36 +48,15 @@ class GlobalTimelineChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user?.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (!this.withBots && note.user.isBot) return;
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (!this.isNoteVisibleToMe(note)) return;
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
}
}
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note);
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -43,7 +43,8 @@ class HashtagChannel extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
if (!accessible || silence) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -50,28 +50,9 @@ class HomeTimelineChannel extends Channel {
if (!isMe && !this.following.has(note.userId)) return;
}
if (this.isNoteMutedOrBlocked(note)) return;
if (!this.isNoteVisibleToMe(note)) return;
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
}
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note);
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -67,28 +67,10 @@ class HybridTimelineChannel extends Channel {
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (!this.isNoteVisibleToMe(note)) return;
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies && !this.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
}
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note);
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
if (!this.withReplies && note.replyId != null) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -50,8 +50,6 @@ class LocalTimelineChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user?.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (!this.withBots && note.user.isBot) return;
@ -59,28 +57,10 @@ class LocalTimelineChannel extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (!this.isNoteVisibleToMe(note)) return;
// 関係ない返信は除外
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
}
}
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note);
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
if (!this.withReplies && note.replyId != null) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -35,24 +35,19 @@ class MainChannel extends Channel {
if (isUserFromMutedInstance(data.body, this.userMutedInstances)) return;
if (data.body.userId && this.userIdsWhoMeMuting.has(data.body.userId)) return;
if (data.body.note && data.body.note.isHidden) {
if (this.isNoteMutedOrBlocked(data.body.note)) return;
if (!this.isNoteVisibleToMe(data.body.id)) return;
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
detail: true,
});
data.body.note = note;
if (data.body.note) {
const { accessible, silence } = await this.checkNoteVisibility(data.body.note, { includeReplies: true });
if (!accessible || silence) return;
data.body.note = await this.rePackNote(data.body.note);
}
break;
}
case 'mention': {
if (this.isNoteMutedOrBlocked(data.body)) return;
if (data.body.isHidden) {
const note = await this.noteEntityService.pack(data.body.id, this.user, {
detail: true,
});
data.body = note;
}
const { accessible, silence } = await this.checkNoteVisibility(data.body, { includeReplies: true });
if (!accessible || silence) return;
data.body = await this.rePackNote(data.body);
break;
}
}

View file

@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import { isPackedPureRenote, isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class RoleTimelineChannel extends Channel {
@ -49,26 +49,8 @@ class RoleTimelineChannel extends Channel {
}
if (note.visibility !== 'public') return;
if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
}
}
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note);
if (!accessible || silence) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);

View file

@ -9,7 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -82,8 +82,6 @@ class UserListChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user?.id === note.userId;
// チャンネル投稿は無視する
if (note.channelId) return;
@ -91,28 +89,9 @@ class UserListChannel extends Channel {
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (!this.isNoteVisibleToMe(note)) return;
if (note.reply) {
const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
}
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (!this.isNoteVisibleToMe(reply)) return;
}
}
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
if (!accessible || silence) return;
if (!this.withRenotes && isPackedPureRenote(note)) return;
const clonedNote = await this.rePackNote(note);
this.send('note', clonedNote);