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

@ -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,

View file

@ -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をキャッシュする

View 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;
}

View file

@ -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;
}

View file

@ -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.');
}

View file

@ -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,

View file

@ -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';
}

View file

@ -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,
});
}

View file

@ -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',

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);

385
pnpm-lock.yaml generated
View file

@ -1508,8 +1508,8 @@ packages:
'@apidevtools/swagger-methods@3.0.2':
resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
'@asamuzakjp/css-color@3.1.1':
resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==}
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
@ -1965,28 +1965,28 @@ packages:
resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.2':
resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.4
'@csstools/css-tokenizer': ^3.0.3
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.0.8':
resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==}
'@csstools/css-color-parser@3.0.10':
resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.4
'@csstools/css-tokenizer': ^3.0.3
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-parser-algorithms@3.0.4':
resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==}
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.3
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.3':
resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@cypress/request@3.0.8':
@ -2168,6 +2168,12 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@ -3099,6 +3105,7 @@ packages:
'@readme/json-schema-ref-parser@1.2.0':
resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==}
deprecated: This package is no longer maintained. Please use `@apidevtools/json-schema-ref-parser` instead.
'@readme/openapi-parser@2.7.0':
resolution: {integrity: sha512-P8WSr8WTOxilnT89tcCRKWYsG/II4sAwt1a/DIWub8xTtkrG9cCBBy/IUcvc5X8oGWN82MwcTA3uEkDrXZd/7A==}
@ -4221,8 +4228,8 @@ packages:
'@types/pg@8.6.1':
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
'@types/proxy-addr@2.0.3':
resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==}
@ -4266,8 +4273,8 @@ packages:
'@types/sanitize-html@2.15.0':
resolution: {integrity: sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==}
'@types/scheduler@0.23.0':
resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==}
'@types/scheduler@0.26.0':
resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==}
'@types/seedrandom@3.0.8':
resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==}
@ -4365,10 +4372,26 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
'@typescript-eslint/project-service@8.39.1':
resolution: {integrity: sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.31.0':
resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.39.1':
resolution: {integrity: sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.39.1':
resolution: {integrity: sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.31.0':
resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -4380,12 +4403,22 @@ packages:
resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.39.1':
resolution: {integrity: sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.31.0':
resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
'@typescript-eslint/typescript-estree@8.39.1':
resolution: {integrity: sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.31.0':
resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -4393,10 +4426,21 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
'@typescript-eslint/utils@8.39.1':
resolution: {integrity: sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.31.0':
resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.39.1':
resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
@ -4649,6 +4693,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@7.1.0:
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
engines: {node: '>= 14'}
@ -5538,8 +5587,8 @@ packages:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
cssstyle@4.3.0:
resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==}
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
csstype@3.1.3:
@ -5630,6 +5679,15 @@ packages:
supports-color:
optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decamelize-keys@1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
engines: {node: '>=0.10.0'}
@ -5638,8 +5696,8 @@ packages:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decimal.js@10.5.0:
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-bmp@0.2.1:
resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==}
@ -6056,6 +6114,10 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.25.1:
resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -6070,6 +6132,10 @@ packages:
resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
@ -6354,6 +6420,7 @@ packages:
fluent-ffmpeg@2.1.3:
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
engines: {node: '>=18'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
@ -6538,10 +6605,12 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
global-dirs@3.0.1:
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
@ -6827,6 +6896,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -7494,6 +7564,7 @@ packages:
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
@ -7975,6 +8046,7 @@ packages:
multer@1.4.5-lts.2:
resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==}
engines: {node: '>= 6.0.0'}
deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.
mute-stream@2.0.0:
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
@ -8055,6 +8127,7 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
@ -8156,8 +8229,8 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
nwsapi@2.2.19:
resolution: {integrity: sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==}
nwsapi@2.2.21:
resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==}
oauth@0.10.2:
resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==}
@ -8466,6 +8539,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pid-port@1.0.2:
resolution: {integrity: sha512-Khqp07zX8IJpmIg56bHrLxS3M0iSL4cq6wnMq8YE7r/hSw3Kn4QxYS6QJg8Bs22Z7CSVj7eSsxFuigYVIFWmjg==}
engines: {node: '>=18'}
@ -9745,10 +9822,12 @@ packages:
superagent@9.0.2:
resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==}
engines: {node: '>=14.18.0'}
deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net
supertest@7.1.0:
resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==}
engines: {node: '>=14.18.0'}
deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
@ -9880,10 +9959,17 @@ packages:
tldts-core@6.1.63:
resolution: {integrity: sha512-H1XCt54xY+QPbwhTgmxLkepX0MVHu3USfMmejiCOdkMbRcP22Pn2FVF127r/GWXVDmXTRezyF3Ckvhn4Fs6j7Q==}
tldts-core@6.1.86:
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
tldts@6.1.63:
resolution: {integrity: sha512-YWwhsjyn9sB/1rOkSRYxvkN/wl5LFM1QDv6F2pVR+pb/jFne4EOBxHfkKVWvDIBEAw9iGOwwubHtQTm0WRT5sQ==}
hasBin: true
tldts@6.1.86:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
tmp@0.2.3:
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
engines: {node: '>=14.14'}
@ -9936,8 +10022,8 @@ packages:
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.0:
resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
tree-kill@1.2.2:
@ -10456,6 +10542,9 @@ packages:
vue-component-type-helpers@2.2.10:
resolution: {integrity: sha512-iDUO7uQK+Sab2tYuiP9D1oLujCWlhHELHMgV/cB13cuGbG4qwkLHvtfWb6FzvxrIOPDnU0oHsz2MlQjhYDeaHA==}
vue-component-type-helpers@3.0.5:
resolution: {integrity: sha512-uoNZaJ+a1/zppa/Vgmi8zIOP2PHXDN2rT8NyF+zQRK6ZG94lNB9prcV0GdLJbY9i9lrD47JOVIH92SaiA7oJ1A==}
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
@ -10654,6 +10743,18 @@ packages:
utf-8-validate:
optional: true
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xev@3.0.2:
resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==}
@ -10763,12 +10864,12 @@ snapshots:
'@apidevtools/swagger-methods@3.0.2': {}
'@asamuzakjp/css-color@3.1.1':
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
'@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
'@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
'@csstools/css-tokenizer': 3.0.3
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
optional: true
@ -11268,7 +11369,7 @@ snapshots:
'@babel/traverse': 7.24.7
'@babel/types': 7.27.1
convert-source-map: 2.0.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -11288,7 +11389,7 @@ snapshots:
'@babel/traverse': 7.24.7
'@babel/types': 7.25.7
convert-source-map: 2.0.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -11521,7 +11622,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.27.2
'@babel/types': 7.25.7
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -11636,26 +11737,26 @@ snapshots:
'@csstools/color-helpers@5.0.2':
optional: true
'@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
'@csstools/css-tokenizer': 3.0.3
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
optional: true
'@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
'@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.0.2
'@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
'@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
'@csstools/css-tokenizer': 3.0.3
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
optional: true
'@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)':
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.3
'@csstools/css-tokenizer': 3.0.4
optional: true
'@csstools/css-tokenizer@3.0.3':
'@csstools/css-tokenizer@3.0.4':
optional: true
'@cypress/request@3.0.8':
@ -11793,6 +11894,11 @@ snapshots:
eslint: 9.25.1
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.7.0(eslint@9.25.1)':
dependencies:
eslint: 9.25.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
'@eslint/compat@1.1.1': {}
@ -11800,7 +11906,7 @@ snapshots:
'@eslint/config-array@0.20.0':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -11814,7 +11920,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@ -13905,16 +14011,16 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.5.14(typescript@5.8.3)
vue-component-type-helpers: 2.2.10
vue-component-type-helpers: 3.0.5
'@stylistic/eslint-plugin@4.2.0(eslint@9.25.1)(typescript@5.8.3)':
dependencies:
'@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3)
'@typescript-eslint/utils': 8.39.1(eslint@9.25.1)(typescript@5.8.3)
eslint: 9.25.1
eslint-visitor-keys: 4.2.0
espree: 10.3.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
picomatch: 4.0.2
picomatch: 4.0.3
transitivePeerDependencies:
- supports-color
- typescript
@ -14065,7 +14171,7 @@ snapshots:
'@tokenizer/inflate@0.2.7':
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
fflate: 0.8.2
token-types: 6.0.0
transitivePeerDependencies:
@ -14306,7 +14412,7 @@ snapshots:
pg-protocol: 1.8.0
pg-types: 2.2.0
'@types/prop-types@15.7.14': {}
'@types/prop-types@15.7.15': {}
'@types/proxy-addr@2.0.3':
dependencies:
@ -14330,8 +14436,8 @@ snapshots:
'@types/react@18.0.28':
dependencies:
'@types/prop-types': 15.7.14
'@types/scheduler': 0.23.0
'@types/prop-types': 15.7.15
'@types/scheduler': 0.26.0
csstype: 3.1.3
'@types/readdir-glob@1.1.1':
@ -14348,7 +14454,7 @@ snapshots:
dependencies:
htmlparser2: 8.0.1
'@types/scheduler@0.23.0': {}
'@types/scheduler@0.26.0': {}
'@types/seedrandom@3.0.8': {}
@ -14455,22 +14561,40 @@ snapshots:
'@typescript-eslint/types': 8.31.0
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.31.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.25.1
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.39.1(typescript@5.8.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.8.3)
'@typescript-eslint/types': 8.39.1
debug: 4.4.1
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.31.0':
dependencies:
'@typescript-eslint/types': 8.31.0
'@typescript-eslint/visitor-keys': 8.31.0
'@typescript-eslint/scope-manager@8.39.1':
dependencies:
'@typescript-eslint/types': 8.39.1
'@typescript-eslint/visitor-keys': 8.39.1
'@typescript-eslint/tsconfig-utils@8.39.1(typescript@5.8.3)':
dependencies:
typescript: 5.8.3
'@typescript-eslint/type-utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3)
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.25.1
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
@ -14479,11 +14603,13 @@ snapshots:
'@typescript-eslint/types@8.31.0': {}
'@typescript-eslint/types@8.39.1': {}
'@typescript-eslint/typescript-estree@8.31.0(typescript@5.8.3)':
dependencies:
'@typescript-eslint/types': 8.31.0
'@typescript-eslint/visitor-keys': 8.31.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@ -14493,6 +14619,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.39.1(typescript@5.8.3)':
dependencies:
'@typescript-eslint/project-service': 8.39.1(typescript@5.8.3)
'@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.8.3)
'@typescript-eslint/types': 8.39.1
'@typescript-eslint/visitor-keys': 8.39.1
debug: 4.4.1
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.2
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1)
@ -14504,11 +14646,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.39.1(eslint@9.25.1)(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.25.1)
'@typescript-eslint/scope-manager': 8.39.1
'@typescript-eslint/types': 8.39.1
'@typescript-eslint/typescript-estree': 8.39.1(typescript@5.8.3)
eslint: 9.25.1
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.31.0':
dependencies:
'@typescript-eslint/types': 8.31.0
eslint-visitor-keys: 4.2.0
'@typescript-eslint/visitor-keys@8.39.1':
dependencies:
'@typescript-eslint/types': 8.39.1
eslint-visitor-keys: 4.2.1
'@ungap/structured-clone@1.2.0': {}
'@vitejs/plugin-vue@5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.14(typescript@5.8.3))':
@ -14520,7 +14678,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
@ -14874,13 +15032,19 @@ snapshots:
dependencies:
acorn: 8.14.1
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
acorn@7.4.1: {}
acorn@8.14.1: {}
acorn@8.15.0: {}
agent-base@7.1.0:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -15292,7 +15456,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
@ -15914,9 +16078,9 @@ snapshots:
dependencies:
css-tree: 2.2.1
cssstyle@4.3.0:
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.1.1
'@asamuzakjp/css-color': 3.2.0
rrweb-cssom: 0.8.0
optional: true
@ -16038,6 +16202,11 @@ snapshots:
ms: 2.1.3
optionalDependencies:
supports-color: 8.1.1
optional: true
debug@4.4.1:
dependencies:
ms: 2.1.3
decamelize-keys@1.1.1:
dependencies:
@ -16046,7 +16215,7 @@ snapshots:
decamelize@1.2.0: {}
decimal.js@10.5.0:
decimal.js@10.6.0:
optional: true
decode-bmp@0.2.1:
@ -16409,7 +16578,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.25.3):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
esbuild: 0.25.3
transitivePeerDependencies:
- supports-color
@ -16540,6 +16709,8 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint-visitor-keys@4.2.1: {}
eslint@9.25.1:
dependencies:
'@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1)
@ -16558,7 +16729,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@ -16586,6 +16757,12 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 4.2.0
espree@10.4.0:
dependencies:
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
esprima@4.0.1: {}
esquery@1.6.0:
@ -16758,7 +16935,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -16965,7 +17142,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@ -17019,7 +17196,7 @@ snapshots:
follow-redirects@1.15.9(debug@4.4.0):
optionalDependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
for-each@0.3.3:
dependencies:
@ -17434,7 +17611,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -17462,7 +17639,7 @@ snapshots:
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -17564,7 +17741,7 @@ snapshots:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
@ -17807,7 +17984,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@ -17816,7 +17993,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@ -18215,14 +18392,14 @@ snapshots:
jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
dependencies:
cssstyle: 4.3.0
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.5.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.19
nwsapi: 2.2.21
parse5: 7.3.0
rrweb-cssom: 0.8.0
saxes: 6.0.0
@ -18233,7 +18410,7 @@ snapshots:
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)
ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
@ -18845,7 +19022,7 @@ snapshots:
micromark@4.0.0:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@ -19229,7 +19406,7 @@ snapshots:
dependencies:
boolbase: 1.0.0
nwsapi@2.2.19:
nwsapi@2.2.21:
optional: true
oauth@0.10.2: {}
@ -19522,6 +19699,8 @@ snapshots:
picomatch@4.0.2: {}
picomatch@4.0.3: {}
pid-port@1.0.2:
dependencies:
execa: 8.0.1
@ -20211,7 +20390,7 @@ snapshots:
require-in-the-middle@7.3.0:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
module-details-from-path: 1.0.3
resolve: 1.22.8
transitivePeerDependencies:
@ -20291,7 +20470,7 @@ snapshots:
router@2.2.0:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@ -20411,7 +20590,7 @@ snapshots:
send@1.2.0:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -20673,7 +20852,7 @@ snapshots:
socks-proxy-agent@8.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
socks: 2.7.1
transitivePeerDependencies:
- supports-color
@ -20783,7 +20962,7 @@ snapshots:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0
@ -20960,7 +21139,7 @@ snapshots:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
fast-safe-stringify: 2.1.1
form-data: 4.0.2
formidable: 3.5.4
@ -21120,11 +21299,19 @@ snapshots:
tldts-core@6.1.63:
optional: true
tldts-core@6.1.86:
optional: true
tldts@6.1.63:
dependencies:
tldts-core: 6.1.63
optional: true
tldts@6.1.86:
dependencies:
tldts-core: 6.1.86
optional: true
tmp@0.2.3: {}
tmpl@1.0.5: {}
@ -21166,12 +21353,12 @@ snapshots:
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.63
tldts: 6.1.86
optional: true
tr46@0.0.3: {}
tr46@5.1.0:
tr46@5.1.1:
dependencies:
punycode: 2.3.1
optional: true
@ -21340,7 +21527,7 @@ snapshots:
app-root-path: 3.1.0
buffer: 6.0.3
dayjs: 1.11.13
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
dotenv: 16.5.0
glob: 10.4.5
reflect-metadata: 0.2.2
@ -21540,7 +21727,7 @@ snapshots:
vite-node@3.1.2(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
dependencies:
cac: 6.7.14
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
es-module-lexer: 1.6.0
pathe: 2.0.3
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
@ -21589,7 +21776,7 @@ snapshots:
'@vitest/spy': 3.1.2
'@vitest/utils': 3.1.2
chai: 5.2.0
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
@ -21661,6 +21848,8 @@ snapshots:
vue-component-type-helpers@2.2.10: {}
vue-component-type-helpers@3.0.5: {}
vue-demi@0.14.7(vue@3.5.14(typescript@5.8.3)):
dependencies:
vue: 3.5.14(typescript@5.8.3)
@ -21682,7 +21871,7 @@ snapshots:
vue-eslint-parser@10.1.3(eslint@9.25.1):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.25.1
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@ -21795,7 +21984,7 @@ snapshots:
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.0
tr46: 5.1.1
webidl-conversions: 7.0.0
optional: true
@ -21893,6 +22082,12 @@ snapshots:
bufferutil: 4.0.9
utf-8-validate: 6.0.5
ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5):
optionalDependencies:
bufferutil: 4.0.9
utf-8-validate: 6.0.5
optional: true
xev@3.0.2: {}
xml-js@1.6.11: