Filter User / Instance Mutes in FanoutTimelineEndpointService (#12565)
* fix: unnecessary logging in FanoutTimelineEndpointService * chore: TimelineOptions * chore: add FanoutTimelineName type * chore: forbid specifying both withReplies and withFiles since it's not implemented correctly * chore: filter mutes, replies, renotes, files in FanoutTimelineEndpointService * revert unintended changes * use isReply in NoteCreateService * fix: excludePureRenotes is not implemented * fix: replies to me is excluded from local timeline * chore(frontend): forbid enabling both withReplies and withFiles * docs(changelog): インスタンスミュートが効かない問題の修正について言及
This commit is contained in:
parent
b2c4973cda
commit
18109fcef7
|
@ -55,6 +55,9 @@
|
||||||
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
|
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
|
||||||
- Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443
|
- Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443
|
||||||
- Fix: 「みつける」のなかにミュートしたユーザが現れてしまう問題を修正 #12383
|
- Fix: 「みつける」のなかにミュートしたユーザが現れてしまう問題を修正 #12383
|
||||||
|
- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題
|
||||||
|
- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題
|
||||||
|
- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題
|
||||||
|
|
||||||
## 2023.11.1
|
## 2023.11.1
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,29 @@ import type { MiNote } from '@/models/Note.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
|
|
||||||
|
type TimelineOptions = {
|
||||||
|
untilId: string | null,
|
||||||
|
sinceId: string | null,
|
||||||
|
limit: number,
|
||||||
|
allowPartial: boolean,
|
||||||
|
me?: { id: MiUser['id'] } | undefined | null,
|
||||||
|
useDbFallback: boolean,
|
||||||
|
redisTimelines: FanoutTimelineName[],
|
||||||
|
noteFilter?: (note: MiNote) => boolean,
|
||||||
|
alwaysIncludeMyNotes?: boolean;
|
||||||
|
ignoreAuthorFromMute?: boolean;
|
||||||
|
excludeNoFiles?: boolean;
|
||||||
|
excludeReplies?: boolean;
|
||||||
|
excludePureRenotes: boolean;
|
||||||
|
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FanoutTimelineEndpointService {
|
export class FanoutTimelineEndpointService {
|
||||||
|
@ -20,37 +42,18 @@ export class FanoutTimelineEndpointService {
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
private cacheService: CacheService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
async timeline(ps: {
|
async timeline(ps: TimelineOptions): Promise<Packed<'Note'>[]> {
|
||||||
untilId: string | null,
|
|
||||||
sinceId: string | null,
|
|
||||||
limit: number,
|
|
||||||
allowPartial: boolean,
|
|
||||||
me?: { id: MiUser['id'] } | undefined | null,
|
|
||||||
useDbFallback: boolean,
|
|
||||||
redisTimelines: string[],
|
|
||||||
noteFilter: (note: MiNote) => boolean,
|
|
||||||
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
|
||||||
}): Promise<Packed<'Note'>[]> {
|
|
||||||
return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me);
|
return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getMiNotes(ps: {
|
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||||
untilId: string | null,
|
|
||||||
sinceId: string | null,
|
|
||||||
limit: number,
|
|
||||||
allowPartial: boolean,
|
|
||||||
me?: { id: MiUser['id'] } | undefined | null,
|
|
||||||
useDbFallback: boolean,
|
|
||||||
redisTimelines: string[],
|
|
||||||
noteFilter: (note: MiNote) => boolean,
|
|
||||||
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
|
||||||
}): Promise<MiNote[]> {
|
|
||||||
let noteIds: string[];
|
let noteIds: string[];
|
||||||
let shouldFallbackToDb = false;
|
let shouldFallbackToDb = false;
|
||||||
|
|
||||||
|
@ -67,10 +70,57 @@ export class FanoutTimelineEndpointService {
|
||||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||||
|
|
||||||
if (!shouldFallbackToDb) {
|
if (!shouldFallbackToDb) {
|
||||||
|
let filter = ps.noteFilter ?? (_note => true);
|
||||||
|
|
||||||
|
if (ps.alwaysIncludeMyNotes && ps.me) {
|
||||||
|
const me = ps.me;
|
||||||
|
const parentFilter = filter;
|
||||||
|
filter = (note) => note.userId === me.id || parentFilter(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.excludeNoFiles) {
|
||||||
|
const parentFilter = filter;
|
||||||
|
filter = (note) => note.fileIds.length !== 0 && parentFilter(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.excludeReplies) {
|
||||||
|
const parentFilter = filter;
|
||||||
|
filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.excludePureRenotes) {
|
||||||
|
const parentFilter = filter;
|
||||||
|
filter = (note) => !isPureRenote(note) && parentFilter(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.me) {
|
||||||
|
const me = ps.me;
|
||||||
|
const [
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
userIdsWhoMeMutingRenotes,
|
||||||
|
userIdsWhoBlockingMe,
|
||||||
|
userMutedInstances,
|
||||||
|
] = 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(me.id).then(p => new Set(p.mutedInstances)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const parentFilter = filter;
|
||||||
|
filter = (note) => {
|
||||||
|
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromMute)) return false;
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||||
|
if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
|
||||||
|
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||||
|
|
||||||
|
return parentFilter(note);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const redisTimeline: MiNote[] = [];
|
const redisTimeline: MiNote[] = [];
|
||||||
let readFromRedis = 0;
|
let readFromRedis = 0;
|
||||||
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
||||||
let trialCount = 1;
|
|
||||||
|
|
||||||
while ((redisResultIds.length - readFromRedis) !== 0) {
|
while ((redisResultIds.length - readFromRedis) !== 0) {
|
||||||
const remainingToRead = ps.limit - redisTimeline.length;
|
const remainingToRead = ps.limit - redisTimeline.length;
|
||||||
|
@ -81,12 +131,10 @@ export class FanoutTimelineEndpointService {
|
||||||
|
|
||||||
readFromRedis += noteIds.length;
|
readFromRedis += noteIds.length;
|
||||||
|
|
||||||
const gotFromDb = await this.getAndFilterFromDb(noteIds, ps.noteFilter);
|
const gotFromDb = await this.getAndFilterFromDb(noteIds, filter);
|
||||||
redisTimeline.push(...gotFromDb);
|
redisTimeline.push(...gotFromDb);
|
||||||
lastSuccessfulRate = gotFromDb.length / noteIds.length;
|
lastSuccessfulRate = gotFromDb.length / noteIds.length;
|
||||||
|
|
||||||
console.log(`fanoutTimelineTrial#${trialCount++}: req: ${ps.limit}, tried: ${noteIds.length}, got: ${gotFromDb.length}, rate: ${lastSuccessfulRate}, total: ${redisTimeline.length}, fromRedis: ${redisResultIds.length}`);
|
|
||||||
|
|
||||||
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
|
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
|
||||||
// 十分Redisからとれた
|
// 十分Redisからとれた
|
||||||
return redisTimeline.slice(0, ps.limit);
|
return redisTimeline.slice(0, ps.limit);
|
||||||
|
@ -97,7 +145,6 @@ export class FanoutTimelineEndpointService {
|
||||||
const remainingToRead = ps.limit - redisTimeline.length;
|
const remainingToRead = ps.limit - redisTimeline.length;
|
||||||
const gotFromDb = await ps.dbFallback(noteIds[noteIds.length - 1], ps.sinceId, remainingToRead);
|
const gotFromDb = await ps.dbFallback(noteIds[noteIds.length - 1], ps.sinceId, remainingToRead);
|
||||||
redisTimeline.push(...gotFromDb);
|
redisTimeline.push(...gotFromDb);
|
||||||
console.log(`fanoutTimelineTrial#db: req: ${ps.limit}, tried: ${remainingToRead}, got: ${gotFromDb.length}, since: ${noteIds[noteIds.length - 1]}, until: ${ps.untilId}, total: ${redisTimeline.length}`);
|
|
||||||
return redisTimeline;
|
return redisTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,34 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
|
||||||
|
export type FanoutTimelineName =
|
||||||
|
// home timeline
|
||||||
|
| `homeTimeline:${string}`
|
||||||
|
| `homeTimelineWithFiles:${string}` // only notes with files are included
|
||||||
|
// local timeline
|
||||||
|
| `localTimeline` // replies are not included
|
||||||
|
| `localTimelineWithFiles` // only non-reply notes with files are included
|
||||||
|
| `localTimelineWithReplies` // only replies are included
|
||||||
|
|
||||||
|
// antenna
|
||||||
|
| `antennaTimeline:${string}`
|
||||||
|
|
||||||
|
// user timeline
|
||||||
|
| `userTimeline:${string}` // replies are not included
|
||||||
|
| `userTimelineWithFiles:${string}` // only non-reply notes with files are included
|
||||||
|
| `userTimelineWithReplies:${string}` // only replies are included
|
||||||
|
| `userTimelineWithChannel:${string}` // only channel notes are included, replies are included
|
||||||
|
|
||||||
|
// user list timelines
|
||||||
|
| `userListTimeline:${string}`
|
||||||
|
| `userListTimelineWithFiles:${string}` // only notes with files are included
|
||||||
|
|
||||||
|
// channel timelines
|
||||||
|
| `channelTimeline:${string}` // replies are included
|
||||||
|
|
||||||
|
// role timelines
|
||||||
|
| `roleTimeline:${string}` // any notes are included
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FanoutTimelineService {
|
export class FanoutTimelineService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -20,7 +48,7 @@ export class FanoutTimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
|
public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
|
||||||
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
|
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
|
||||||
// 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する
|
// 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する
|
||||||
if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) {
|
if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) {
|
||||||
|
@ -41,7 +69,7 @@ export class FanoutTimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public get(name: string, untilId?: string | null, sinceId?: string | null) {
|
public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) {
|
||||||
if (untilId && sinceId) {
|
if (untilId && sinceId) {
|
||||||
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
||||||
.then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1));
|
.then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1));
|
||||||
|
@ -58,7 +86,7 @@ export class FanoutTimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
|
public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
|
||||||
const pipeline = this.redisForTimelines.pipeline();
|
const pipeline = this.redisForTimelines.pipeline();
|
||||||
for (const n of name) {
|
for (const n of name) {
|
||||||
pipeline.lrange('list:' + n, 0, -1);
|
pipeline.lrange('list:' + n, 0, -1);
|
||||||
|
@ -79,7 +107,7 @@ export class FanoutTimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public purge(name: string) {
|
public purge(name: FanoutTimelineName) {
|
||||||
return this.redisForTimelines.del('list:' + name);
|
return this.redisForTimelines.del('list:' + name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -891,7 +892,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
|
if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
|
||||||
|
|
||||||
// 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合
|
// 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合
|
||||||
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) {
|
if (isReply(note, following.followerId)) {
|
||||||
if (!following.withReplies) continue;
|
if (!following.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -909,7 +910,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
) continue;
|
) continue;
|
||||||
|
|
||||||
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
|
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
|
||||||
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) {
|
if (isReply(note, userListMembership.userListUserId)) {
|
||||||
if (!userListMembership.withReplies) continue;
|
if (!userListMembership.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -927,7 +928,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自分自身以外への返信
|
// 自分自身以外への返信
|
||||||
if (note.replyId && note.replyUserId !== note.userId) {
|
if (isReply(note)) {
|
||||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
|
|
||||||
if (note.visibility === 'public' && note.userHost == null) {
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
|
|
10
packages/backend/src/misc/is-reply.ts
Normal file
10
packages/backend/src/misc/is-reply.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MiUser } from '@/models/User.js';
|
||||||
|
|
||||||
|
export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean {
|
||||||
|
return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId;
|
||||||
|
}
|
|
@ -4,15 +4,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js';
|
import type { ChannelsRepository, NotesRepository } from '@/models/_.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||||
|
@ -94,12 +92,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
|
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
|
||||||
userIdsWhoMeMuting,
|
|
||||||
] = me ? await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
]) : [new Set<string>()];
|
|
||||||
|
|
||||||
return await this.fanoutTimelineEndpointService.timeline({
|
return await this.fanoutTimelineEndpointService.timeline({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
@ -108,11 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
useDbFallback: true,
|
useDbFallback: true,
|
||||||
redisTimelines: [`channelTimeline:${channel.id}`],
|
redisTimelines: [`channelTimeline:${channel.id}`],
|
||||||
noteFilter: note => {
|
excludePureRenotes: false,
|
||||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => {
|
dbFallback: async (untilId, sinceId, limit) => {
|
||||||
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
|
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,9 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -43,6 +42,12 @@ export const meta = {
|
||||||
code: 'STL_DISABLED',
|
code: 'STL_DISABLED',
|
||||||
id: '620763f4-f621-4533-ab33-0577a1a3c342',
|
id: '620763f4-f621-4533-ab33-0577a1a3c342',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bothWithRepliesAndWithFiles: {
|
||||||
|
message: 'Specifying both withReplies and withFiles is not supported',
|
||||||
|
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
||||||
|
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -93,6 +98,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.stlDisabled);
|
throw new ApiError(meta.errors.stlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
||||||
|
|
||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
|
@ -114,17 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
let timelineConfig: FanoutTimelineName[];
|
||||||
userIdsWhoMeMuting,
|
|
||||||
userIdsWhoMeMutingRenotes,
|
|
||||||
userIdsWhoBlockingMe,
|
|
||||||
] = await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let timelineConfig: string[];
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
timelineConfig = [
|
timelineConfig = [
|
||||||
|
@ -152,21 +149,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
redisTimelines: timelineConfig,
|
redisTimelines: timelineConfig,
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
||||||
noteFilter: (note) => {
|
alwaysIncludeMyNotes: true,
|
||||||
if (note.userId === me.id) {
|
excludePureRenotes: !ps.withRenotes,
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
|
||||||
if (note.renoteId) {
|
|
||||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
|
||||||
if (ps.withRenotes === false) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { MiNote, NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
|
@ -13,7 +13,6 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
|
@ -39,6 +38,12 @@ export const meta = {
|
||||||
code: 'LTL_DISABLED',
|
code: 'LTL_DISABLED',
|
||||||
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
|
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bothWithRepliesAndWithFiles: {
|
||||||
|
message: 'Specifying both withReplies and withFiles is not supported',
|
||||||
|
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
||||||
|
id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -82,6 +87,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.ltlDisabled);
|
throw new ApiError(meta.errors.ltlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
||||||
|
|
||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
|
@ -102,16 +109,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
|
||||||
userIdsWhoMeMuting,
|
|
||||||
userIdsWhoMeMutingRenotes,
|
|
||||||
userIdsWhoBlockingMe,
|
|
||||||
] = me ? await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
|
||||||
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
|
||||||
|
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
@ -120,22 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
||||||
redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ['localTimeline', 'localTimelineWithReplies'],
|
redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ['localTimeline', 'localTimelineWithReplies'],
|
||||||
noteFilter: note => {
|
alwaysIncludeMyNotes: true,
|
||||||
if (me && (note.userId === me.id)) {
|
excludeReplies: !ps.withReplies,
|
||||||
return true;
|
excludePureRenotes: !ps.withRenotes,
|
||||||
}
|
|
||||||
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
|
|
||||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
|
||||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
|
||||||
if (note.renoteId) {
|
|
||||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
|
||||||
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
|
||||||
if (ps.withRenotes === false) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -98,14 +97,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const [
|
const [
|
||||||
followings,
|
followings,
|
||||||
userIdsWhoMeMuting,
|
|
||||||
userIdsWhoMeMutingRenotes,
|
|
||||||
userIdsWhoBlockingMe,
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const timeline = this.fanoutTimelineEndpointService.timeline({
|
const timeline = this.fanoutTimelineEndpointService.timeline({
|
||||||
|
@ -116,18 +109,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
||||||
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
|
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
|
||||||
|
alwaysIncludeMyNotes: true,
|
||||||
|
excludePureRenotes: !ps.withRenotes,
|
||||||
noteFilter: note => {
|
noteFilter: note => {
|
||||||
if (note.userId === me.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
|
||||||
if (note.renoteId) {
|
|
||||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
|
||||||
if (ps.withRenotes === false) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (note.reply && note.reply.visibility === 'followers') {
|
if (note.reply && note.reply.visibility === 'followers') {
|
||||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,17 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -84,7 +81,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
@ -121,18 +117,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
await this.noteEntityService.packMany(timeline, me);
|
await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
|
||||||
userIdsWhoMeMuting,
|
|
||||||
userIdsWhoMeMutingRenotes,
|
|
||||||
userIdsWhoBlockingMe,
|
|
||||||
userMutedInstances,
|
|
||||||
] = await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
|
||||||
this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
@ -141,22 +125,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
||||||
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
|
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
|
||||||
noteFilter: note => {
|
alwaysIncludeMyNotes: true,
|
||||||
if (note.userId === me.id) {
|
excludePureRenotes: !ps.withRenotes,
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
|
||||||
if (note.renoteId) {
|
|
||||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
|
||||||
if (ps.withRenotes === false) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, {
|
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, {
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
|
|
@ -11,11 +11,12 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||||
|
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'notes'],
|
tags: ['users', 'notes'],
|
||||||
|
@ -36,6 +37,12 @@ export const meta = {
|
||||||
code: 'NO_SUCH_USER',
|
code: 'NO_SUCH_USER',
|
||||||
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bothWithRepliesAndWithFiles: {
|
||||||
|
message: 'Specifying both withReplies and withFiles is not supported',
|
||||||
|
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
||||||
|
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -77,6 +84,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
|
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
const timeline = await this.getFromDb({
|
const timeline = await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
|
@ -91,13 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`];
|
||||||
userIdsWhoMeMuting,
|
|
||||||
] = me ? await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
]) : [new Set<string>()];
|
|
||||||
|
|
||||||
const redisTimelines = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`];
|
|
||||||
|
|
||||||
if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`);
|
if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`);
|
||||||
if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`);
|
if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`);
|
||||||
|
@ -112,18 +115,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
redisTimelines,
|
redisTimelines,
|
||||||
useDbFallback: true,
|
useDbFallback: true,
|
||||||
|
ignoreAuthorFromMute: true,
|
||||||
|
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
|
||||||
|
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
|
||||||
|
excludePureRenotes: !ps.withRenotes,
|
||||||
noteFilter: note => {
|
noteFilter: note => {
|
||||||
if (ps.withFiles && note.fileIds.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false;
|
|
||||||
|
|
||||||
if (note.renoteId) {
|
|
||||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
|
||||||
if (ps.withRenotes === false) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.channel?.isSensitive && !isSelf) return false;
|
if (note.channel?.isSensitive && !isSelf) return false;
|
||||||
if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
|
if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
|
||||||
if (note.visibility === 'followers' && !isFollowing && !isSelf) return false;
|
if (note.visibility === 'followers' && !isFollowing && !isSelf) return false;
|
||||||
|
|
|
@ -202,7 +202,7 @@ function focusDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchItem(item: MenuSwitch & { ref: any }) {
|
function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
if (item.disabled) return;
|
if (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value) return;
|
||||||
item.ref = !item.ref;
|
item.ref = !item.ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,17 +157,17 @@ const headerActions = $computed(() => {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.showRenotes,
|
text: i18n.ts.showRenotes,
|
||||||
icon: 'ti ti-repeat',
|
|
||||||
ref: $$(withRenotes),
|
ref: $$(withRenotes),
|
||||||
}, src === 'local' || src === 'social' ? {
|
}, src === 'local' || src === 'social' ? {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||||
ref: $$(withReplies),
|
ref: $$(withReplies),
|
||||||
|
disabled: $$(onlyFiles),
|
||||||
} : undefined, {
|
} : undefined, {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.fileAttachedOnly,
|
text: i18n.ts.fileAttachedOnly,
|
||||||
icon: 'ti ti-photo',
|
|
||||||
ref: $$(onlyFiles),
|
ref: $$(onlyFiles),
|
||||||
|
disabled: src === 'local' || src === 'social' ? $$(withReplies) : false,
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,7 +14,7 @@ export type MenuLabel = { type: 'label', text: string };
|
||||||
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
||||||
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
|
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
|
||||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
||||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
|
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean | Ref<boolean> };
|
||||||
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
|
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
|
||||||
export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
||||||
|
|
||||||
|
|
|
@ -120,10 +120,12 @@ const menu = [{
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||||
ref: $$(withReplies),
|
ref: $$(withReplies),
|
||||||
|
disabled: $$(onlyFiles),
|
||||||
} : undefined, {
|
} : undefined, {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.fileAttachedOnly,
|
text: i18n.ts.fileAttachedOnly,
|
||||||
ref: $$(onlyFiles),
|
ref: $$(onlyFiles),
|
||||||
|
disabled: props.column.tl === 'local' || props.column.tl === 'social' ? $$(withReplies) : false,
|
||||||
}];
|
}];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue