egirlskey/packages/backend/src/core/FeaturedService.ts

95 lines
3.2 KiB
TypeScript
Raw Normal View History

/*
* 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 type { MiNote } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
2023-10-06 07:10:59 +00:00
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
@Injectable()
export class FeaturedService {
constructor(
@Inject(DI.redis)
2023-10-06 07:58:38 +00:00
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
) {
}
@bindThis
2023-10-06 07:58:38 +00:00
private getCurrentWindow(windowRange: number): number {
const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
2023-10-06 07:58:38 +00:00
return Math.floor(passed / windowRange);
}
@bindThis
2023-10-06 07:58:38 +00:00
private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const redisTransaction = this.redisClient.multi();
redisTransaction.zincrby(
2023-10-06 07:58:38 +00:00
`${name}:${currentWindow}`,
score,
element);
redisTransaction.expire(
2023-10-06 07:58:38 +00:00
`${name}:${currentWindow}`,
(windowRange * 3) / 1000,
'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
await redisTransaction.exec();
}
@bindThis
2023-10-06 07:58:38 +00:00
private async getRankingOf(name: string, windowRange: number, limit: number): Promise<string[]> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const [currentRankingResult, previousRankingResult] = await Promise.all([
this.redisClient.zrange(
2023-10-06 07:58:38 +00:00
`${name}:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'),
this.redisClient.zrange(
2023-10-06 07:58:38 +00:00
`${name}:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'),
]);
2023-10-06 07:58:38 +00:00
const ranking = new Map<string, number>();
for (let i = 0; i < currentRankingResult.length; i += 2) {
const noteId = currentRankingResult[i];
const score = parseInt(currentRankingResult[i + 1], 10);
ranking.set(noteId, score);
}
for (let i = 0; i < previousRankingResult.length; i += 2) {
const noteId = previousRankingResult[i];
const score = parseInt(previousRankingResult[i + 1], 10);
const exist = ranking.get(noteId);
if (exist != null) {
ranking.set(noteId, (exist + score) / 2);
} else {
ranking.set(noteId, score);
}
}
return Array.from(ranking.keys());
}
@bindThis
2023-10-06 08:01:06 +00:00
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
2023-10-06 07:58:38 +00:00
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
2023-10-06 07:58:38 +00:00
@bindThis
2023-10-06 08:01:06 +00:00
public updateInChannelNotesRanking(noteId: MiNote['id'], channelId: MiNote['channelId'], score = 1): Promise<void> {
2023-10-06 07:58:38 +00:00
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
2023-10-06 07:58:38 +00:00
@bindThis
2023-10-06 08:01:06 +00:00
public getGlobalNotesRanking(limit: number): Promise<MiNote['id'][]> {
2023-10-06 07:58:38 +00:00
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, limit);
}
2023-10-06 07:58:38 +00:00
@bindThis
2023-10-06 08:01:06 +00:00
public getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise<MiNote['id'][]> {
2023-10-06 07:58:38 +00:00
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, limit);
}
}