completely re-implement note visibility as NoteVisibilityService
This commit is contained in:
parent
d1912362e0
commit
85ca2269e4
27 changed files with 925 additions and 420 deletions
|
|
@ -20,6 +20,7 @@ import { EnvService } from '@/core/EnvService.js';
|
|||
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
||||
import { ApLogService } from '@/core/ApLogService.js';
|
||||
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AnnouncementService } from './AnnouncementService.js';
|
||||
|
|
@ -240,6 +241,7 @@ const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisti
|
|||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $TimeService: Provider = { provide: 'TimeService', useExisting: TimeService };
|
||||
const $EnvService: Provider = { provide: 'EnvService', useExisting: EnvService };
|
||||
const $NoteVisibilityService: Provider = { provide: 'NoteVisibilityService', useExisting: NoteVisibilityService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
|
|
@ -400,6 +402,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
ReversiService,
|
||||
TimeService,
|
||||
EnvService,
|
||||
NoteVisibilityService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
|
|
@ -556,6 +559,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
$ReversiService,
|
||||
$TimeService,
|
||||
$EnvService,
|
||||
$NoteVisibilityService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
|
|
@ -713,6 +717,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
ReversiService,
|
||||
TimeService,
|
||||
EnvService,
|
||||
NoteVisibilityService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
|
|
@ -867,6 +872,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
$ReversiService,
|
||||
$TimeService,
|
||||
$EnvService,
|
||||
$NoteVisibilityService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { NoteVisibilityService, PopulatedNote } from '@/core/NoteVisibilityService.js';
|
||||
|
||||
type TimelineOptions = {
|
||||
untilId: string | null,
|
||||
|
|
@ -56,6 +57,7 @@ export class FanoutTimelineEndpointService {
|
|||
private cacheService: CacheService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private utilityService: UtilityService,
|
||||
private readonly noteVisibilityService: NoteVisibilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -110,52 +112,14 @@ export class FanoutTimelineEndpointService {
|
|||
filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note);
|
||||
}
|
||||
|
||||
if (ps.me) {
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
userMutedInstances,
|
||||
myFollowings,
|
||||
myThreadMutings,
|
||||
myNoteMutings,
|
||||
me,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(ps.me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(ps.me.id),
|
||||
this.cacheService.userBlockedCache.fetch(ps.me.id),
|
||||
this.cacheService.userProfileCache.fetch(ps.me.id).then(p => new Set(p.mutedInstances)),
|
||||
this.cacheService.userFollowingsCache.fetch(ps.me.id).then(fs => new Set(fs.keys())),
|
||||
this.cacheService.threadMutingsCache.fetch(ps.me.id),
|
||||
this.cacheService.noteMutingsCache.fetch(ps.me.id),
|
||||
this.cacheService.findUserById(ps.me.id),
|
||||
]);
|
||||
{
|
||||
const me = ps.me ? await this.cacheService.findUserById(ps.me.id) : null;
|
||||
const data = await this.noteVisibilityService.populateData(me);
|
||||
|
||||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
|
||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||
|
||||
// Silenced users (when logged in)
|
||||
if (!ps.ignoreAuthorFromUserSilence && !myFollowings.has(note.userId)) {
|
||||
if (note.user?.isSilenced || note.user?.instance?.isSilenced) return false;
|
||||
if (note.reply?.user?.isSilenced || note.reply?.user?.instance?.isSilenced) return false;
|
||||
if (note.renote?.user?.isSilenced || note.renote?.user?.instance?.isSilenced) return false;
|
||||
}
|
||||
|
||||
// Muted threads / posts
|
||||
if (!ps.includeMutedNotes) {
|
||||
if (myThreadMutings.has(note.threadId ?? note.id) || myNoteMutings.has(note.id)) return false;
|
||||
if (note.replyId && myNoteMutings.has(note.replyId)) return false;
|
||||
if (note.renote && (myThreadMutings.has(note.renote.threadId ?? note.renote.id) || myNoteMutings.has(note.renote.id))) return false;
|
||||
}
|
||||
|
||||
// Invisible notes
|
||||
if (!this.noteEntityService.isVisibleForMeSync(note, me, myFollowings, userIdsWhoBlockingMe)) {
|
||||
return false;
|
||||
}
|
||||
const { accessible, silence } = this.noteVisibilityService.checkNoteVisibility(note as PopulatedNote, me, { data, filters: { includeSilencedAuthor: ps.ignoreAuthorFromUserSilence } });
|
||||
if (!accessible || silence) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
|
|
@ -191,36 +155,6 @@ export class FanoutTimelineEndpointService {
|
|||
};
|
||||
}
|
||||
|
||||
{
|
||||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
// Silenced users (when logged out)
|
||||
if (!ps.ignoreAuthorFromUserSilence && !ps.me) {
|
||||
if (note.user?.isSilenced || note.user?.instance?.isSilenced) return false;
|
||||
if (note.reply?.user?.isSilenced || note.reply?.user?.instance?.isSilenced) return false;
|
||||
if (note.renote?.user?.isSilenced || note.renote?.user?.instance?.isSilenced) return false;
|
||||
}
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
}
|
||||
|
||||
// This one MUST be last!
|
||||
{
|
||||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
// If this is a boost, then first run all checks for the boost target.
|
||||
if (isPureRenote(note) && note.renote) {
|
||||
if (!parentFilter(note.renote)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Either way, make sure to run the checks for the actual note too!
|
||||
return parentFilter(note);
|
||||
};
|
||||
}
|
||||
|
||||
const redisTimeline: MiNote[] = [];
|
||||
let readFromRedis = 0;
|
||||
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
||||
|
|
|
|||
420
packages/backend/src/core/NoteVisibilityService.ts
Normal file
420
packages/backend/src/core/NoteVisibilityService.ts
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import type { MiFollowing, NotesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
/**
|
||||
* Visibility level for a given user towards a given post.
|
||||
*/
|
||||
export interface NoteVisibilityResult {
|
||||
/**
|
||||
* Whether the user has access to view this post.
|
||||
*/
|
||||
accessible: boolean;
|
||||
|
||||
/**
|
||||
* If the user should be shown only a redacted version of the post.
|
||||
* (see NoteEntityService.hideNote() for details.)
|
||||
*/
|
||||
redact: boolean;
|
||||
|
||||
/**
|
||||
* If false, the note should be visible by default. (normal case)
|
||||
* If true, the note should be hidden by default. (Silences, mutes, etc.)
|
||||
* If "timeline", the note should be hidden in timelines only. (following w/o replies)
|
||||
*/
|
||||
silence: boolean;
|
||||
}
|
||||
|
||||
export interface NoteVisibilityFilters {
|
||||
/**
|
||||
* If false, exclude replies to other users unless the "include replies to others in timeline" has been enabled for the note's author.
|
||||
* If true (default), then replies are treated like any other post.
|
||||
*/
|
||||
includeReplies?: boolean;
|
||||
|
||||
/**
|
||||
* If true, treat the note's author as never being silenced. Does not apply to reply or renote targets, unless they're by the same author.
|
||||
* If false (default), then silence is enforced for all notes.
|
||||
*/
|
||||
includeSilencedAuthor?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NoteVisibilityService {
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private readonly notesRepository: NotesRepository,
|
||||
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly idService: IdService,
|
||||
private readonly federatedInstanceService: FederatedInstanceService,
|
||||
) {}
|
||||
|
||||
@bindThis
|
||||
public async checkNoteVisibilityAsync(note: MiNote | Packed<'Note'>, user: string | PopulatedUser, opts?: { filters?: NoteVisibilityFilters, hint?: Partial<NoteVisibilityData> }): Promise<NoteVisibilityResult> {
|
||||
if (typeof(user) === 'string') {
|
||||
user = await this.cacheService.findUserById(user);
|
||||
}
|
||||
|
||||
const populatedNote = await this.populateNote(note);
|
||||
const populatedData = await this.populateData(user, opts?.hint ?? {});
|
||||
|
||||
return this.checkNoteVisibility(populatedNote, user, { filters: opts?.filters, data: populatedData });
|
||||
}
|
||||
|
||||
private async populateNote(note: Packed<'Note'>, dive?: boolean): Promise<Packed<'Note'>>;
|
||||
private async populateNote(note: MiNote, dive?: boolean): Promise<PopulatedMiNote>;
|
||||
private async populateNote(note: MiNote | Packed<'Note'>, dive?: boolean): Promise<PopulatedNote>;
|
||||
private async populateNote(note: MiNote | Packed<'Note'>, dive = true): Promise<PopulatedNote> {
|
||||
// Packed<'Note'> is already fully loaded
|
||||
if (isPackedNote(note)) return note;
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
return await awaitAll({
|
||||
...note,
|
||||
user: this.getNoteUser(note),
|
||||
renote: dive ? this.getNoteRenote(note) : null,
|
||||
reply: dive ? this.getNoteReply(note) : null,
|
||||
});
|
||||
}
|
||||
|
||||
private async getNoteUser(note: MiNote): Promise<PopulatedMiNote['user']> {
|
||||
const user = note.user ?? await this.cacheService.findUserById(note.userId);
|
||||
return {
|
||||
...user,
|
||||
instance: user.instance ?? (user.host ? await this.federatedInstanceService.fetchOrRegister(user.host) : null),
|
||||
};
|
||||
}
|
||||
|
||||
private async getNoteRenote(note: MiNote): Promise<PopulatedMiNote['renote']> {
|
||||
if (!note.renoteId) return null;
|
||||
|
||||
const renote = note.renote ?? await this.notesRepository.findOneOrFail({
|
||||
where: { id: note.renoteId },
|
||||
relations: {
|
||||
user: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return await this.populateNote(renote, false);
|
||||
}
|
||||
|
||||
private async getNoteReply(note: MiNote): Promise<PopulatedMiNote['reply']> {
|
||||
if (!note.replyId) return null;
|
||||
|
||||
const reply = note.reply ?? await this.notesRepository.findOneOrFail({
|
||||
where: { id: note.replyId },
|
||||
relations: {
|
||||
user: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return await this.populateNote(reply, false);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async populateData(user: PopulatedUser, hint?: Partial<NoteVisibilityData>): Promise<NoteVisibilityData> {
|
||||
// noinspection ES6MissingAwait
|
||||
const [
|
||||
userBlockers,
|
||||
userFollowings,
|
||||
userMutedThreads,
|
||||
userMutedNotes,
|
||||
userMutedUsers,
|
||||
userMutedUserRenotes,
|
||||
userMutedInstances,
|
||||
] = await Promise.all([
|
||||
user ? (hint?.userBlockers ?? this.cacheService.userBlockedCache.fetch(user.id)) : null,
|
||||
user ? (hint?.userFollowings ?? this.cacheService.userFollowingsCache.fetch(user.id)) : null,
|
||||
user ? (hint?.userMutedThreads ?? this.cacheService.threadMutingsCache.fetch(user.id)) : null,
|
||||
user ? (hint?.userMutedNotes ?? this.cacheService.noteMutingsCache.fetch(user.id)) : null,
|
||||
user ? (hint?.userMutedUsers ?? this.cacheService.userMutingsCache.fetch(user.id)) : null,
|
||||
user ? (hint?.userMutedUserRenotes ?? this.cacheService.renoteMutingsCache.fetch(user.id)) : null,
|
||||
user ? (hint?.userMutedInstances ?? this.cacheService.userProfileCache.fetch(user.id).then(p => new Set(p.mutedInstances))) : null,
|
||||
]);
|
||||
|
||||
return {
|
||||
userBlockers,
|
||||
userFollowings,
|
||||
userMutedThreads,
|
||||
userMutedNotes,
|
||||
userMutedUsers,
|
||||
userMutedUserRenotes,
|
||||
userMutedInstances,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public checkNoteVisibility(note: PopulatedNote, user: PopulatedUser, opts: { filters?: NoteVisibilityFilters, data: NoteVisibilityData }): NoteVisibilityResult {
|
||||
// Copy note since we mutate it below
|
||||
note = {
|
||||
...note,
|
||||
renote: note.renote ? { ...note.renote } : null,
|
||||
reply: note.reply ? { ...note.reply } : null,
|
||||
} as PopulatedNote;
|
||||
|
||||
this.syncVisibility(note);
|
||||
|
||||
const accessible = this.isAccessible(note, user, opts.data);
|
||||
const redact = this.shouldRedact(note, user, opts.data);
|
||||
const silence = this.shouldSilence(note, user, opts.data, opts.filters);
|
||||
|
||||
const baseVisibility = { accessible, redact, silence };
|
||||
|
||||
// For boosts (pure renotes), we must recurse and pick the lowest common access level.
|
||||
if (isPopulatedBoost(note)) {
|
||||
const boostVisibility = this.checkNoteVisibility(note.renote, user, opts);
|
||||
return {
|
||||
accessible: baseVisibility.accessible && boostVisibility.accessible,
|
||||
redact: baseVisibility.redact || boostVisibility.redact,
|
||||
silence: baseVisibility.silence || boostVisibility.silence,
|
||||
};
|
||||
}
|
||||
|
||||
return baseVisibility;
|
||||
}
|
||||
|
||||
// Based on NoteEntityService.isVisibleForMe
|
||||
private isAccessible(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean {
|
||||
// We can always view our own notes
|
||||
if (user?.id === note.userId) return true;
|
||||
|
||||
// We can *never* view blocked notes
|
||||
if (data.userBlockers?.has(note.userId)) return false;
|
||||
|
||||
if (note.visibility === 'specified') {
|
||||
return this.isAccessibleDM(note, user);
|
||||
} else if (note.visibility === 'followers') {
|
||||
return this.isAccessibleFO(note, user, data);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private isAccessibleDM(note: PopulatedNote, user: PopulatedUser): boolean {
|
||||
// Must be logged in to view DM
|
||||
if (user == null) return false;
|
||||
|
||||
// Can be visible to me
|
||||
if (note.visibleUserIds?.includes(user.id)) return true;
|
||||
|
||||
// Otherwise invisible
|
||||
return false;
|
||||
}
|
||||
|
||||
private isAccessibleFO(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean {
|
||||
// Must be logged in to view FO
|
||||
if (user == null) return false;
|
||||
|
||||
// Can be a reply to me
|
||||
if (note.reply?.userId === user.id) return true;
|
||||
|
||||
// Can mention me
|
||||
if (note.mentions?.includes(user.id)) return true;
|
||||
|
||||
// Can be visible to me
|
||||
if (note.visibleUserIds?.includes(user.id)) return true;
|
||||
|
||||
// Can be followed by me
|
||||
if (data.userFollowings?.has(note.userId)) return true;
|
||||
|
||||
// Can be two remote users, since we can't verify remote->remote following.
|
||||
if (note.userHost != null && user.host != null) return true;
|
||||
|
||||
// Otherwise invisible
|
||||
return false;
|
||||
}
|
||||
|
||||
// Based on NoteEntityService.treatVisibility
|
||||
@bindThis
|
||||
public syncVisibility(note: PopulatedNote): void {
|
||||
// Make followers-only
|
||||
if (note.user.makeNotesFollowersOnlyBefore && note.visibility !== 'specified' && note.visibility !== 'followers') {
|
||||
const followersOnlyBefore = note.user.makeNotesFollowersOnlyBefore * 1000;
|
||||
const createdAt = 'createdAt' in note
|
||||
? new Date(note.createdAt).getTime()
|
||||
: this.idService.parse(note.id).date.getTime();
|
||||
|
||||
// I don't understand this logic, but I tried to break it out for readability
|
||||
const followersOnlyOpt1 = followersOnlyBefore <= 0 && (Date.now() - createdAt > 0 - followersOnlyBefore);
|
||||
const followersOnlyOpt2 = followersOnlyBefore > 0 && (createdAt < followersOnlyBefore);
|
||||
if (followersOnlyOpt1 || followersOnlyOpt2) {
|
||||
note.visibility = 'followers';
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse
|
||||
if (note.renote) {
|
||||
this.syncVisibility(note.renote);
|
||||
}
|
||||
if (note.reply) {
|
||||
this.syncVisibility(note.reply);
|
||||
}
|
||||
}
|
||||
|
||||
// Based on NoteEntityService.hideNote
|
||||
private shouldRedact(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean {
|
||||
// Never redact our own notes
|
||||
if (user?.id === note.userId) return false;
|
||||
|
||||
// Redact if sign-in required
|
||||
if (note.user.requireSigninToViewContents && !user) return true;
|
||||
|
||||
// Redact if note has expired
|
||||
if (note.user.makeNotesHiddenBefore) {
|
||||
const hiddenBefore = note.user.makeNotesHiddenBefore * 1000;
|
||||
const createdAt = 'createdAt' in note
|
||||
? new Date(note.createdAt).getTime()
|
||||
: this.idService.parse(note.id).date.getTime();
|
||||
|
||||
// I don't understand this logic, but I tried to break it out for readability
|
||||
const hiddenOpt1 = hiddenBefore <= 0 && (Date.now() - createdAt > 0 - hiddenBefore);
|
||||
const hiddenOpt2 = hiddenBefore > 0 && (createdAt < hiddenBefore);
|
||||
if (hiddenOpt1 || hiddenOpt2) return true;
|
||||
}
|
||||
|
||||
// Redact if inaccessible.
|
||||
// We have to repeat the check in case note visibility changed in treatVisibility!
|
||||
if (!this.isAccessible(note, user, data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise don't redact
|
||||
return false;
|
||||
}
|
||||
|
||||
// Based on inconsistent logic from all around the app
|
||||
private shouldSilence(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData, filters: NoteVisibilityFilters | undefined): boolean {
|
||||
if (this.shouldSilenceForMute(note, data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.shouldSilenceForSilence(note, user, data, filters?.includeSilencedAuthor ?? false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!filters?.includeReplies && this.shouldSilenceForFollowWithoutReplies(note, user, data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private shouldSilenceForMute(note: PopulatedNote, data: NoteVisibilityData): boolean {
|
||||
// Silence if we've muted the thread
|
||||
if (data.userMutedThreads?.has(note.threadId ?? note.id)) return true;
|
||||
|
||||
// Silence if we've muted the note
|
||||
if (data.userMutedNotes?.has(note.id)) return true;
|
||||
|
||||
// Silence if we've muted the user
|
||||
if (data.userMutedUsers?.has(note.userId)) return true;
|
||||
|
||||
// Silence if we've muted renotes from the user
|
||||
if (isPopulatedBoost(note) && data.userMutedUserRenotes?.has(note.userId)) return true;
|
||||
|
||||
// Silence if we've muted the instance
|
||||
if (note.userHost && data.userMutedInstances?.has(note.userHost)) return true;
|
||||
|
||||
// Otherwise don't silence
|
||||
return false;
|
||||
}
|
||||
|
||||
private shouldSilenceForSilence(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData, includeSilencedAuthor: boolean): boolean {
|
||||
// Don't silence if it's us
|
||||
if (note.userId === user?.id) return false;
|
||||
|
||||
// Don't silence if we're following
|
||||
if (data.userFollowings?.has(note.userId)) return false;
|
||||
|
||||
if (!includeSilencedAuthor) {
|
||||
// Silence if user is silenced
|
||||
if (note.user.isSilenced) return true;
|
||||
|
||||
// Silence if user instance is silenced
|
||||
if (note.user.instance?.isSilenced) return true;
|
||||
}
|
||||
|
||||
// Silence if renote is silenced
|
||||
if (note.renote && note.renote.userId !== note.userId && this.shouldSilenceForSilence(note.renote, user, data, false)) return true;
|
||||
|
||||
// Silence if reply is silenced
|
||||
if (note.reply && note.reply.userId !== note.userId && this.shouldSilenceForSilence(note.reply, user, data, false)) return true;
|
||||
|
||||
// Otherwise don't silence
|
||||
return false;
|
||||
}
|
||||
|
||||
private shouldSilenceForFollowWithoutReplies(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean {
|
||||
// Don't silence if it's not a reply
|
||||
if (!note.reply) return false;
|
||||
|
||||
// Don't silence if it's a self-reply
|
||||
if (note.reply.userId === note.userId) return false;
|
||||
|
||||
// Don't silence if it's a reply to us
|
||||
if (note.reply.userId === user?.id) return false;
|
||||
|
||||
// Don't silence if it's our post
|
||||
if (note.userId === user?.id) return false;
|
||||
|
||||
// Don't silence if we follow w/ replies
|
||||
if (user && data.userFollowings?.get(user.id)?.withReplies) return false;
|
||||
|
||||
// Silence otherwise
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NoteVisibilityData {
|
||||
userBlockers: Set<string> | null;
|
||||
userFollowings: Map<string, Omit<MiFollowing, 'isFollowerHibernated'>> | null;
|
||||
userMutedThreads: Set<string> | null;
|
||||
userMutedNotes: Set<string> | null;
|
||||
userMutedUsers: Set<string> | null;
|
||||
userMutedUserRenotes: Set<string> | null;
|
||||
userMutedInstances: Set<string> | null;
|
||||
}
|
||||
|
||||
export type PopulatedUser = Pick<MiUser, 'id' | 'host'> | null | undefined;
|
||||
|
||||
export type PopulatedNote = PopulatedMiNote | Packed<'Note'>;
|
||||
|
||||
type PopulatedMiNote = MiNote & {
|
||||
user: MiUser & {
|
||||
instance: MiInstance | null,
|
||||
}
|
||||
renote: PopulatedMiNote | null,
|
||||
reply: PopulatedMiNote | null,
|
||||
};
|
||||
|
||||
function isPopulatedBoost(note: PopulatedNote): note is PopulatedNote & { renote: PopulatedNote } {
|
||||
return note.renoteId != null
|
||||
&& note.replyId == null
|
||||
&& note.text == null
|
||||
&& note.cw == null
|
||||
&& (note.fileIds == null || note.fileIds.length === 0);
|
||||
}
|
||||
|
||||
function isPackedNote(note: MiNote | Packed<'Note'>): note is Packed<'Note'> {
|
||||
// Kind of a hack: determine whether it's packed by looking for property that doesn't exist in MiNote
|
||||
return 'isFavorited' in note;
|
||||
}
|
||||
|
|
@ -128,29 +128,44 @@ export class QueryService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedUserQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
// 投稿の作者をミュートしていない かつ
|
||||
// 投稿の返信先の作者をミュートしていない かつ
|
||||
// 投稿の引用元の作者をミュートしていない
|
||||
return this
|
||||
.andNotMutingUser(q, ':meId', 'note.userId', exclude)
|
||||
public generateMutedUserQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }, excludeAuthor = false): SelectQueryBuilder<E> {
|
||||
if (!excludeAuthor) {
|
||||
this
|
||||
// muted user
|
||||
.andNotMutingUser(q, ':meId', 'note.userId')
|
||||
// muted host
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userHost IS NULL');
|
||||
this.orFollowingUser(qb, ':meId', 'note.userId');
|
||||
this.orNotMutingInstance(qb, ':meId', 'note.userHost');
|
||||
}));
|
||||
}
|
||||
|
||||
return q
|
||||
// muted reply user
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingUser(qb, ':meId', 'note.replyUserId', exclude)
|
||||
.orNotMutingUser(qb, ':meId', 'note.replyUserId')
|
||||
.orWhere('note.replyUserId = note.userId')
|
||||
.orWhere('note.replyUserId IS NULL')))
|
||||
// muted renote user
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingUser(qb, ':meId', 'note.renoteUserId', exclude)
|
||||
.orNotMutingUser(qb, ':meId', 'note.renoteUserId')
|
||||
.orWhere('note.renoteUserId = note.userId')
|
||||
.orWhere('note.renoteUserId IS NULL')))
|
||||
// TODO exclude should also pass a host to skip these instances
|
||||
// mute instances
|
||||
.andWhere(new Brackets(qb => this
|
||||
.andNotMutingInstance(qb, ':meId', 'note.userHost')
|
||||
.orWhere('note.userHost IS NULL')))
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingInstance(qb, ':meId', 'note.replyUserHost')
|
||||
.orWhere('note.replyUserHost IS NULL')))
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingInstance(qb, ':meId', 'note.renoteUserHost')
|
||||
.orWhere('note.renoteUserHost IS NULL')))
|
||||
// muted reply host
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.replyUserHost IS NULL');
|
||||
qb.orWhere('note.replyUserHost = note.userHost');
|
||||
this.orFollowingUser(qb, ':meId', 'note.replyUserId');
|
||||
this.orNotMutingInstance(qb, ':meId', 'note.replyUserHost');
|
||||
}))
|
||||
// muted renote host
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NULL');
|
||||
qb.orWhere('note.renoteUserHost = note.userHost');
|
||||
this.orFollowingUser(qb, ':meId', 'note.renoteUserId');
|
||||
this.orNotMutingInstance(qb, ':meId', 'note.renoteUserHost');
|
||||
}))
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
|
|
@ -238,10 +253,10 @@ export class QueryService {
|
|||
|
||||
@bindThis
|
||||
public generateSilencedUserQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me?: { id: MiUser['id'] } | null, excludeAuthor = false): SelectQueryBuilder<E> {
|
||||
const checkFor = (key: 'user' | 'replyUser' | 'renoteUser') => {
|
||||
const checkFor = (key: 'user' | 'replyUser' | 'renoteUser', userKey: 'note.user' | 'reply.user' | 'renote.user') => {
|
||||
// These are de-duplicated, since most call sites already provide some of them.
|
||||
this.leftJoin(q, `note.${key}Instance`, `${key}Instance`); // note->instance
|
||||
this.leftJoin(q, `note.${key}`, key); // note->user
|
||||
this.leftJoin(q, userKey, key); // note->user
|
||||
|
||||
q.andWhere(new Brackets(qb => {
|
||||
// case 1: user does not exist (note is not reply/renote)
|
||||
|
|
@ -270,10 +285,10 @@ export class QueryService {
|
|||
}
|
||||
|
||||
if (!excludeAuthor) {
|
||||
checkFor('user');
|
||||
checkFor('user', 'note.user');
|
||||
}
|
||||
checkFor('replyUser');
|
||||
checkFor('renoteUser');
|
||||
checkFor('replyUser', 'reply.user');
|
||||
checkFor('renoteUser', 'renote.user');
|
||||
|
||||
return q;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js';
|
|||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
const FALLBACK = '\u2764';
|
||||
|
|
@ -108,6 +109,7 @@ export class ReactionService {
|
|||
private notificationService: NotificationService,
|
||||
private perUserReactionsChart: PerUserReactionsChart,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly noteVisibilityService: NoteVisibilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +124,8 @@ export class ReactionService {
|
|||
}
|
||||
|
||||
// check visibility
|
||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id, { me: user })) {
|
||||
const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(note, user);
|
||||
if (!accessible) {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -401,6 +401,7 @@ export class WebhookTestService {
|
|||
text: note.text,
|
||||
cw: note.cw,
|
||||
userId: note.userId,
|
||||
userHost: note.userHost ?? null,
|
||||
user: await this.toPackedUserLite(note.user ?? generateDummyUser()),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import FederationChart from '@/core/chart/charts/federation.js';
|
|||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost, isActivity, IObjectWithId } from './type.js';
|
||||
import { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
|
|
@ -100,6 +101,7 @@ export class ApInboxService {
|
|||
private readonly federationChart: FederationChart,
|
||||
private readonly updateInstanceQueue: UpdateInstanceQueue,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly noteVisibilityService: NoteVisibilityService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
|
@ -367,7 +369,8 @@ export class ApInboxService {
|
|||
const renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) });
|
||||
if (renote == null) return 'announce target is null';
|
||||
|
||||
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id, { me: actor })) {
|
||||
const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(renote, actor);
|
||||
if (!accessible) {
|
||||
return 'skip: invalid actor for this activity';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ import { DebounceLoader } from '@/misc/loader.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import type { NoteVisibilityData } from '@/core/NoteVisibilityService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { CacheService } from '../CacheService.js';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
|
|
@ -101,6 +102,9 @@ export class NoteEntityService implements OnModuleInit {
|
|||
@Inject(DI.noteFavoritesRepository)
|
||||
private noteFavoritesRepository: NoteFavoritesRepository,
|
||||
|
||||
// This is public to avoid weaving a whole new service through the Channel class hierarchy.
|
||||
public readonly noteVisibilityService: NoteVisibilityService,
|
||||
|
||||
private readonly queryService: QueryService,
|
||||
//private userEntityService: UserEntityService,
|
||||
//private driveFileEntityService: DriveFileEntityService,
|
||||
|
|
@ -121,6 +125,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||
this.idService = this.moduleRef.get('IdService');
|
||||
}
|
||||
|
||||
// Implementation moved to NoteVisibilityService
|
||||
/*
|
||||
@bindThis
|
||||
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
|
||||
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
|
||||
|
|
@ -136,24 +142,31 @@ export class NoteEntityService implements OnModuleInit {
|
|||
}
|
||||
return packedNote.visibility;
|
||||
}
|
||||
*/
|
||||
|
||||
@bindThis
|
||||
public async hideNotes(notes: Packed<'Note'>[], meId: string | null): Promise<void> {
|
||||
const myFollowing = meId ? new Map(await this.cacheService.userFollowingsCache.fetch(meId)) : new Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>();
|
||||
const myBlockers = meId ? new Set(await this.cacheService.userBlockedCache.fetch(meId)) : new Set<string>();
|
||||
public async hideNotes(notes: Packed<'Note'>[], meId: string | null, hint?: Partial<NoteVisibilityData>): Promise<void> {
|
||||
const me = meId ? await this.cacheService.findUserById(meId) : null;
|
||||
const data = await this.noteVisibilityService.populateData(me, hint);
|
||||
|
||||
// This shouldn't actually await, but we have to wrap it anyway because hideNote() is async
|
||||
await Promise.all(notes.map(note => this.hideNote(note, meId, {
|
||||
myFollowing,
|
||||
myBlockers,
|
||||
})));
|
||||
for (const note of notes) {
|
||||
this.hideNote(note, me, data);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: {
|
||||
myFollowing?: ReadonlyMap<string, Omit<MiFollowing, 'isFollowerHibernated'>> | ReadonlySet<string>,
|
||||
myBlockers?: ReadonlySet<string>,
|
||||
}): Promise<void> {
|
||||
public async hideNoteAsync(packedNote: Packed<'Note'>, me: string | Pick<MiUser, 'id' | 'host'> | null, hint?: Partial<NoteVisibilityData>): Promise<void> {
|
||||
const { redact } = await this.noteVisibilityService.checkNoteVisibilityAsync(packedNote, me, { hint });
|
||||
|
||||
if (redact) {
|
||||
this.redactNoteContents(packedNote);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public hideNote(packedNote: Packed<'Note'>, me: Pick<MiUser, 'id' | 'host'> | null, data: NoteVisibilityData): void {
|
||||
// Implementation moved to NoteVisibilityService
|
||||
/*
|
||||
if (meId === packedNote.userId) return;
|
||||
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
|
|
@ -232,8 +245,17 @@ export class NoteEntityService implements OnModuleInit {
|
|||
|
||||
if (isBlocked) hide = true;
|
||||
}
|
||||
*/
|
||||
|
||||
const hide = this.noteVisibilityService.checkNoteVisibility(packedNote, me, { data }).redact;
|
||||
|
||||
if (hide) {
|
||||
this.redactNoteContents(packedNote);
|
||||
}
|
||||
}
|
||||
|
||||
private redactNoteContents(packedNote: Packed<'Note'>) {
|
||||
{
|
||||
packedNote.visibleUserIds = undefined;
|
||||
packedNote.fileIds = [];
|
||||
packedNote.files = [];
|
||||
|
|
@ -477,6 +499,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// Implementation moved to NoteVisibilityService
|
||||
/*
|
||||
@bindThis
|
||||
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null, hint?: {
|
||||
myFollowing?: ReadonlySet<string>,
|
||||
|
|
@ -493,7 +517,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public isVisibleForMeSync(note: MiNote, me: Pick<MiUser, 'id' | 'host'> | null, myFollowings: ReadonlySet<string> | null, myBlockers: ReadonlySet<string> | null): boolean {
|
||||
public isVisibleForMeSync(note: MiNote | Packed<'Note'>, me: Pick<MiUser, 'id' | 'host'> | null, myFollowings: ReadonlySet<string> | null, myBlockers: ReadonlySet<string> | null): boolean {
|
||||
// We can always view our own notes
|
||||
if (me?.id === note.userId) {
|
||||
return true;
|
||||
|
|
@ -509,6 +533,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||
if (note.visibility === 'specified') {
|
||||
if (me == null) {
|
||||
return false;
|
||||
} else if (!note.visibleUserIds) {
|
||||
return false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
return note.visibleUserIds.includes(me.id);
|
||||
|
|
@ -522,9 +548,13 @@ export class NoteEntityService implements OnModuleInit {
|
|||
} else if (note.reply && (me.id === note.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
return true;
|
||||
} else if (!note.mentions) {
|
||||
return false;
|
||||
} else if (note.mentions.includes(me.id)) {
|
||||
// 自分へのメンション
|
||||
return true;
|
||||
} else if (!note.visibleUserIds) {
|
||||
return false;
|
||||
} else if (note.visibleUserIds.includes(me.id)) {
|
||||
// Explicitly visible to me
|
||||
return true;
|
||||
|
|
@ -533,19 +563,19 @@ export class NoteEntityService implements OnModuleInit {
|
|||
const following = myFollowings?.has(note.userId);
|
||||
const userHost = me.host;
|
||||
|
||||
/* If we know the following, everyhting is fine.
|
||||
|
||||
But if we do not know the following, it might be that both the
|
||||
author of the note and the author of the like are remote users,
|
||||
in which case we can never know the following. Instead we have
|
||||
to assume that the users are following each other.
|
||||
*/
|
||||
// If we know the following, everyhting is fine.
|
||||
//
|
||||
// But if we do not know the following, it might be that both the
|
||||
// author of the note and the author of the like are remote users,
|
||||
// in which case we can never know the following. Instead we have
|
||||
// to assume that the users are following each other.
|
||||
return following || (note.userHost != null && userHost != null);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
|
||||
@bindThis
|
||||
public async packAttachedFiles(fileIds: MiNote['fileIds'], packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>): Promise<Packed<'DriveFile'>[]> {
|
||||
|
|
@ -649,6 +679,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
createdAt: this.idService.parse(note.id).date.toISOString(),
|
||||
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
||||
userId: note.userId,
|
||||
userHost: note.userHost,
|
||||
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
|
||||
text: text,
|
||||
cw: note.cw,
|
||||
|
|
@ -719,12 +750,14 @@ export class NoteEntityService implements OnModuleInit {
|
|||
} : {}),
|
||||
});
|
||||
|
||||
this.treatVisibility(packed);
|
||||
this.noteVisibilityService.syncVisibility(packed);
|
||||
|
||||
if (!opts.skipHide) {
|
||||
await this.hideNote(packed, meId, meId == null ? undefined : {
|
||||
myFollowing: opts._hint_?.userFollowings.get(meId),
|
||||
myBlockers: opts._hint_?.userBlockers.get(meId),
|
||||
await this.hideNoteAsync(packed, meId, {
|
||||
userFollowings: meId ? opts._hint_?.userFollowings.get(meId) : null,
|
||||
userBlockers: meId ? opts._hint_?.userBlockers.get(meId) : null,
|
||||
userMutedNotes: opts._hint_?.mutedNotes,
|
||||
userMutedThreads: opts._hint_?.mutedThreads,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ export const packedNoteSchema = {
|
|||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
userHost: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue