enhance(backend): make ftt db fallback configurable
This commit is contained in:
parent
838c70192e
commit
9d78a1a8b3
13 changed files with 430 additions and 330 deletions
|
@ -32,6 +32,7 @@
|
|||
- Fix: 特定の条件下でノートがnyaizeされない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: FTTのデータベースへのフォールバック処理を行うかどうかを設定可能に
|
||||
- Fix: トークンのないプラグインをアンインストールするときにエラーが出ないように
|
||||
- Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました
|
||||
- Fix: ユーザタイムラインの「ノート」選択時にリノートが混ざり込んでしまうことがある問題の修正 #12306
|
||||
|
|
2
locales/index.d.ts
vendored
2
locales/index.d.ts
vendored
|
@ -1285,6 +1285,8 @@ export interface Locale {
|
|||
"shortName": string;
|
||||
"shortNameDescription": string;
|
||||
"fanoutTimelineDescription": string;
|
||||
"fanoutTimelineDbFallback": string;
|
||||
"fanoutTimelineDbFallbackDescription": string;
|
||||
};
|
||||
"_accountMigration": {
|
||||
"moveFrom": string;
|
||||
|
|
|
@ -1272,6 +1272,8 @@ _serverSettings:
|
|||
shortName: "略称"
|
||||
shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。"
|
||||
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
|
||||
fanoutTimelineDbFallback: "データベースへのフォールバック"
|
||||
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
||||
|
||||
_accountMigration:
|
||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class EnableFanoutTimelineDbFallback1700096812223 {
|
||||
name = 'EnableFanoutTimelineDbFallback1700096812223'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimelineDbFallback" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimelineDbFallback"`);
|
||||
}
|
||||
}
|
|
@ -494,6 +494,11 @@ export class MiMeta {
|
|||
})
|
||||
public enableFanoutTimeline: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public enableFanoutTimelineDbFallback: boolean;
|
||||
|
||||
@Column('integer', {
|
||||
default: 300,
|
||||
})
|
||||
|
|
|
@ -295,6 +295,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableFanoutTimelineDbFallback: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
perLocalUserUserTimelineCacheMax: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
|
@ -424,6 +428,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
manifestJsonOverride: instance.manifestJsonOverride,
|
||||
enableFanoutTimeline: instance.enableFanoutTimeline,
|
||||
enableFanoutTimelineDbFallback: instance.enableFanoutTimelineDbFallback,
|
||||
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
|
||||
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
|
||||
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
||||
|
|
|
@ -121,6 +121,7 @@ export const paramDef = {
|
|||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||
manifestJsonOverride: { type: 'string' },
|
||||
enableFanoutTimeline: { type: 'boolean' },
|
||||
enableFanoutTimelineDbFallback: { type: 'boolean' },
|
||||
perLocalUserUserTimelineCacheMax: { type: 'integer' },
|
||||
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
|
||||
perUserHomeTimelineCacheMax: { type: 'integer' },
|
||||
|
@ -485,6 +486,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
set.enableFanoutTimeline = ps.enableFanoutTimeline;
|
||||
}
|
||||
|
||||
if (ps.enableFanoutTimelineDbFallback !== undefined) {
|
||||
set.enableFanoutTimelineDbFallback = ps.enableFanoutTimelineDbFallback;
|
||||
}
|
||||
|
||||
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
|
||||
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
|
||||
}
|
||||
|
|
|
@ -93,99 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
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 noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
if (ps.withFiles) {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimelineWithFiles:${me.id}`,
|
||||
'localTimelineWithFiles',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
} else if (ps.withReplies) {
|
||||
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||
} else {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||
}
|
||||
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
@ -197,6 +105,102 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
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 noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
if (ps.withFiles) {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimelineWithFiles:${me.id}`,
|
||||
'localTimelineWithFiles',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
} else if (ps.withReplies) {
|
||||
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||
} else {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||
}
|
||||
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -84,84 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
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>()];
|
||||
|
||||
let noteIds: string[];
|
||||
|
||||
if (ps.withFiles) {
|
||||
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||
} else {
|
||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
}
|
||||
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
@ -170,6 +93,87 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withReplies: ps.withReplies,
|
||||
}, 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>()];
|
||||
|
||||
let noteIds: string[];
|
||||
|
||||
if (ps.withFiles) {
|
||||
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||
} else {
|
||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
}
|
||||
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -76,77 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(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 (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
@ -158,6 +88,80 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(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 (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { MiNote, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import { Brackets } from 'typeorm';
|
||||
import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
|
@ -14,8 +15,9 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { Brackets } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'lists'],
|
||||
|
@ -81,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private idService: IdService,
|
||||
private funoutTimelineService: FunoutTimelineService,
|
||||
private queryService: QueryService,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
@ -96,6 +98,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb(list, {
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
|
@ -145,93 +162,119 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (redisTimeline.length > 0) {
|
||||
this.activeUsersChart.read(me);
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど投稿者自身への返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど自分宛ての返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = :meId', { meId: me.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけどwithRepliesがtrueの場合
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('userListMemberships.withReplies = true');
|
||||
}));
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb(list, {
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getFromDb(list: MiUserList, ps: {
|
||||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
limit: number,
|
||||
includeMyRenotes: boolean,
|
||||
includeRenotedMyNotes: boolean,
|
||||
includeLocalRenotes: boolean,
|
||||
withFiles: boolean,
|
||||
withRenotes: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど投稿者自身への返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど自分宛ての返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = :meId', { meId: me.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけどwithRepliesがtrueの場合
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('userListMemberships.withReplies = true');
|
||||
}));
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ describe('ActivityPub', () => {
|
|||
cacheRemoteFiles: true,
|
||||
cacheRemoteSensitiveFiles: true,
|
||||
enableFanoutTimeline: true,
|
||||
enableFanoutTimelineDbFallback: true,
|
||||
perUserHomeTimelineCacheMax: 100,
|
||||
perLocalUserUserTimelineCacheMax: 100,
|
||||
perRemoteUserUserTimelineCacheMax: 100,
|
||||
|
|
|
@ -95,6 +95,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="enableFanoutTimelineDbFallback">
|
||||
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template>
|
||||
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
|
||||
<template #label>perLocalUserUserTimelineCacheMax</template>
|
||||
</MkInput>
|
||||
|
@ -171,6 +176,7 @@ let enableServiceWorker: boolean = $ref(false);
|
|||
let swPublicKey: any = $ref(null);
|
||||
let swPrivateKey: any = $ref(null);
|
||||
let enableFanoutTimeline: boolean = $ref(false);
|
||||
let enableFanoutTimelineDbFallback: boolean = $ref(false);
|
||||
let perLocalUserUserTimelineCacheMax: number = $ref(0);
|
||||
let perRemoteUserUserTimelineCacheMax: number = $ref(0);
|
||||
let perUserHomeTimelineCacheMax: number = $ref(0);
|
||||
|
@ -192,6 +198,7 @@ async function init(): Promise<void> {
|
|||
swPublicKey = meta.swPublickey;
|
||||
swPrivateKey = meta.swPrivateKey;
|
||||
enableFanoutTimeline = meta.enableFanoutTimeline;
|
||||
enableFanoutTimelineDbFallback = meta.enableFanoutTimelineDbFallback;
|
||||
perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
|
||||
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
|
||||
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
|
||||
|
@ -214,6 +221,7 @@ async function save(): void {
|
|||
swPublicKey,
|
||||
swPrivateKey,
|
||||
enableFanoutTimeline,
|
||||
enableFanoutTimelineDbFallback,
|
||||
perLocalUserUserTimelineCacheMax,
|
||||
perRemoteUserUserTimelineCacheMax,
|
||||
perUserHomeTimelineCacheMax,
|
||||
|
|
Loading…
Reference in a new issue