2023-10-09 11:31:39 +00:00
|
|
|
/*
|
|
|
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
|
|
import * as Redis from 'ioredis';
|
|
|
|
import { DI } from '@/di-symbols.js';
|
|
|
|
import { bindThis } from '@/decorators.js';
|
|
|
|
import { IdService } from '@/core/IdService.js';
|
|
|
|
|
2023-12-04 05:38:21 +00:00
|
|
|
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
|
2023-12-04 08:56:48 +00:00
|
|
|
| `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id.
|
2023-12-04 05:38:21 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2023-10-09 11:31:39 +00:00
|
|
|
@Injectable()
|
2023-11-26 01:02:22 +00:00
|
|
|
export class FanoutTimelineService {
|
2023-10-09 11:31:39 +00:00
|
|
|
constructor(
|
|
|
|
@Inject(DI.redisForTimelines)
|
|
|
|
private redisForTimelines: Redis.Redis,
|
|
|
|
|
|
|
|
private idService: IdService,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
@bindThis
|
2023-12-04 05:38:21 +00:00
|
|
|
public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
|
2023-10-09 11:31:39 +00:00
|
|
|
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
|
|
|
|
// 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する
|
|
|
|
if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) {
|
|
|
|
pipeline.lpush('list:' + tl, id);
|
|
|
|
if (Math.random() < 0.1) { // 10%の確率でトリム
|
|
|
|
pipeline.ltrim('list:' + tl, 0, maxlen - 1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// 末尾のIDを取得
|
|
|
|
this.redisForTimelines.lindex('list:' + tl, -1).then(lastId => {
|
|
|
|
if (lastId == null || (this.idService.parse(id).date.getTime() > this.idService.parse(lastId).date.getTime())) {
|
|
|
|
this.redisForTimelines.lpush('list:' + tl, id);
|
|
|
|
} else {
|
|
|
|
Promise.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@bindThis
|
2023-12-04 05:38:21 +00:00
|
|
|
public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) {
|
2023-10-09 11:31:39 +00:00
|
|
|
if (untilId && sinceId) {
|
|
|
|
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
2023-10-09 12:23:07 +00:00
|
|
|
.then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1));
|
2023-10-09 11:31:39 +00:00
|
|
|
} else if (untilId) {
|
|
|
|
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
2023-10-09 12:23:07 +00:00
|
|
|
.then(ids => ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1));
|
2023-10-09 11:31:39 +00:00
|
|
|
} else if (sinceId) {
|
|
|
|
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
2023-10-09 12:23:07 +00:00
|
|
|
.then(ids => ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1));
|
2023-10-09 11:31:39 +00:00
|
|
|
} else {
|
|
|
|
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
2023-10-09 12:52:31 +00:00
|
|
|
.then(ids => ids.sort((a, b) => a > b ? -1 : 1));
|
2023-10-09 11:31:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@bindThis
|
2023-12-04 05:38:21 +00:00
|
|
|
public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
|
2023-10-09 11:31:39 +00:00
|
|
|
const pipeline = this.redisForTimelines.pipeline();
|
|
|
|
for (const n of name) {
|
|
|
|
pipeline.lrange('list:' + n, 0, -1);
|
|
|
|
}
|
|
|
|
return pipeline.exec().then(res => {
|
|
|
|
if (res == null) return [];
|
|
|
|
const tls = res.map(r => r[1] as string[]);
|
|
|
|
return tls.map(ids =>
|
|
|
|
(untilId && sinceId)
|
2023-10-09 12:23:07 +00:00
|
|
|
? ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)
|
2023-10-09 11:31:39 +00:00
|
|
|
: untilId
|
2023-10-09 12:23:07 +00:00
|
|
|
? ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1)
|
2023-10-09 11:31:39 +00:00
|
|
|
: sinceId
|
2023-10-09 12:23:07 +00:00
|
|
|
? ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1)
|
2023-10-09 12:52:31 +00:00
|
|
|
: ids.sort((a, b) => a > b ? -1 : 1),
|
2023-10-09 11:31:39 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
2023-10-18 03:26:16 +00:00
|
|
|
|
|
|
|
@bindThis
|
2023-12-04 05:38:21 +00:00
|
|
|
public purge(name: FanoutTimelineName) {
|
2023-10-18 03:26:16 +00:00
|
|
|
return this.redisForTimelines.del('list:' + name);
|
|
|
|
}
|
2023-10-09 11:31:39 +00:00
|
|
|
}
|