summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/FeaturedService.ts
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-10-06 14:24:25 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-10-06 14:24:25 +0900
commitdab205edb87ea9cdaee5b6564aa11dfcea245d7b (patch)
treee29a7874da5395a67a516c2164a82c050071dd2d /packages/backend/src/core/FeaturedService.ts
parentchore(backend): response isHibernated in admin/show-user (diff)
downloadsharkey-dab205edb87ea9cdaee5b6564aa11dfcea245d7b.tar.gz
sharkey-dab205edb87ea9cdaee5b6564aa11dfcea245d7b.tar.bz2
sharkey-dab205edb87ea9cdaee5b6564aa11dfcea245d7b.zip
enhance(backend): improve featured system
Diffstat (limited to 'packages/backend/src/core/FeaturedService.ts')
-rw-r--r--packages/backend/src/core/FeaturedService.ts126
1 files changed, 126 insertions, 0 deletions
diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts
new file mode 100644
index 0000000000..89b86b2e30
--- /dev/null
+++ b/packages/backend/src/core/FeaturedService.ts
@@ -0,0 +1,126 @@
+/*
+ * 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';
+
+@Injectable()
+export class FeaturedService {
+ constructor(
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+ ) {
+ }
+
+ @bindThis
+ private getCurrentPerUserFriendRankingWindow(): number {
+ const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
+ return Math.floor(passed / (1000 * 60 * 60 * 24 * 7)); // 1週間ごと
+ }
+
+ @bindThis
+ private getCurrentGlobalNotesRankingWindow(): number {
+ const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
+ return Math.floor(passed / (1000 * 60 * 60 * 24 * 3)); // 3日ごと
+ }
+
+ @bindThis
+ public async updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
+ // TODO: フォロワー数の多い人が常にランキング上位になるのを防ぎたい
+ const currentWindow = this.getCurrentGlobalNotesRankingWindow();
+ const redisTransaction = this.redisClient.multi();
+ redisTransaction.zincrby(
+ `featuredGlobalNotesRanking:${currentWindow}`,
+ score.toString(),
+ noteId);
+ redisTransaction.expire(
+ `featuredGlobalNotesRanking:${currentWindow}`,
+ 60 * 60 * 24 * 9, // 9日間保持
+ 'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
+ await redisTransaction.exec();
+ }
+
+ @bindThis
+ public async updateInChannelNotesRanking(noteId: MiNote['id'], channelId: MiNote['channelId'], score = 1): Promise<void> {
+ const currentWindow = this.getCurrentGlobalNotesRankingWindow();
+ const redisTransaction = this.redisClient.multi();
+ redisTransaction.zincrby(
+ `featuredInChannelNotesRanking:${channelId}:${currentWindow}`,
+ score.toString(),
+ noteId);
+ redisTransaction.expire(
+ `featuredInChannelNotesRanking:${channelId}:${currentWindow}`,
+ 60 * 60 * 24 * 9, // 9日間保持
+ 'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
+ await redisTransaction.exec();
+ }
+
+ @bindThis
+ public async getGlobalNotesRanking(limit: number): Promise<MiNote['id'][]> {
+ const currentWindow = this.getCurrentGlobalNotesRankingWindow();
+ const previousWindow = currentWindow - 1;
+
+ const [currentRankingResult, previousRankingResult] = await Promise.all([
+ this.redisClient.zrange(
+ `featuredGlobalNotesRanking:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'),
+ this.redisClient.zrange(
+ `featuredGlobalNotesRanking:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'),
+ ]);
+
+ const ranking = new Map<MiNote['id'], 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
+ public async getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise<MiNote['id'][]> {
+ const currentWindow = this.getCurrentGlobalNotesRankingWindow();
+ const previousWindow = currentWindow - 1;
+
+ const [currentRankingResult, previousRankingResult] = await Promise.all([
+ this.redisClient.zrange(
+ `featuredInChannelNotesRanking:${channelId}:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'),
+ this.redisClient.zrange(
+ `featuredInChannelNotesRanking:${channelId}:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'),
+ ]);
+
+ const ranking = new Map<MiNote['id'], 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());
+ }
+}