From 97300774c04e6eb0bc24d50a18d7d9f05826c8f7 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 1 Oct 2025 11:37:38 -0400 Subject: [PATCH] add InstanceStatsService --- packages/backend/src/core/CoreModule.ts | 5 + .../backend/src/core/InstanceStatsService.ts | 117 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 packages/backend/src/core/InstanceStatsService.ts diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 951f9169fa..08ed47cc9f 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -18,6 +18,7 @@ import { FlashService } from '@/core/FlashService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { ApLogService } from '@/core/ApLogService.js'; import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js'; +import { InstanceStatsService } from '@/core/InstanceStatsService.js'; import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; @@ -396,6 +397,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp ChatService, RegistryApiService, ReversiService, + InstanceStatsService, NoteVisibilityService, ChartLoggerService, @@ -550,6 +552,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp $ChatService, $RegistryApiService, $ReversiService, + $InstanceStatsService, $NoteVisibilityService, $ChartLoggerService, @@ -706,6 +709,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp ChatService, RegistryApiService, ReversiService, + InstanceStatsService, NoteVisibilityService, FederationChart, @@ -858,6 +862,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp $ChatService, $RegistryApiService, $ReversiService, + $InstanceStatsService, $NoteVisibilityService, $FederationChart, diff --git a/packages/backend/src/core/InstanceStatsService.ts b/packages/backend/src/core/InstanceStatsService.ts new file mode 100644 index 0000000000..5eb7fbf291 --- /dev/null +++ b/packages/backend/src/core/InstanceStatsService.ts @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull, MoreThan } from 'typeorm'; +import { CacheManagementService, type ManagedMemorySingleCache } from '@/core/CacheManagementService.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { TimeService } from '@/core/TimeService.js'; + +export interface InstanceStats { + /** + * The number of local posts on the instance. + * Updated hourly. + */ + notesTotal: number; + + /** + * The number of local users currently registered on the instance. + * Updated hourly. + */ + usersTotal: number; + + /** + * The number of local users who have been active within the past month. + * Updated daily. + */ + usersActiveMonth: number; + + /** + * The number of local users who have been active within the past 6 months. + * Updated weekly. + */ + usersActiveSixMonths: number; +} + +@Injectable() +export class InstanceStatsService { + private readonly activeSixMonthsCache: ManagedMemorySingleCache; + private readonly activeMonthCache: ManagedMemorySingleCache; + private readonly localUsersCache: ManagedMemorySingleCache; + private readonly localPostsCache: ManagedMemorySingleCache; + + constructor( + @Inject(DI.usersRepository) + private readonly usersRepository: UsersRepository, + + private readonly notesChart: NotesChart, + private readonly usersChart: UsersChart, + private readonly timeService: TimeService, + + cacheManagementService: CacheManagementService, + ) { + this.localPostsCache = cacheManagementService.createMemorySingleCache(1000 * 60 * 60); // 1h + this.localUsersCache = cacheManagementService.createMemorySingleCache(1000 * 60 * 60); // 1h + this.activeMonthCache = cacheManagementService.createMemorySingleCache(1000 * 60 * 60 * 24); // 1d + this.activeSixMonthsCache = cacheManagementService.createMemorySingleCache(1000 * 60 * 60 * 24 * 7); // 1w + } + + @bindThis + public async fetch(): Promise { + const [notesTotal, usersTotal, usersActiveMonth, usersActiveSixMonths] = await Promise.all([ + this.fetchLocalPosts(), + this.fetchLocalUsers(), + this.fetchActiveMonth(), + this.fetchActiveSixMonths(), + ]); + return { notesTotal, usersTotal, usersActiveMonth, usersActiveSixMonths }; + } + + @bindThis + private async fetchActiveSixMonths(): Promise { + return await this.activeSixMonthsCache.fetch(async () => { + const now = this.timeService.now; + const halfYearAgo = new Date(now - 15552000000); + return await this.usersRepository.countBy({ + host: IsNull(), + isBot: false, + lastActiveDate: MoreThan(halfYearAgo), + }); + }); + } + + @bindThis + private async fetchActiveMonth(): Promise { + return await this.activeMonthCache.fetch(async () => { + const now = this.timeService.now; + const halfYearAgo = new Date(now - 2592000000); + return await this.usersRepository.countBy({ + host: IsNull(), + isBot: false, + lastActiveDate: MoreThan(halfYearAgo), + }); + }); + } + + @bindThis + private async fetchLocalUsers(): Promise { + return await this.localUsersCache.fetch(async () => { + const chart = await this.usersChart.getChart('hour', 1, null); + return chart.local.total[0]; + }); + } + + @bindThis + private async fetchLocalPosts(): Promise { + return await this.localPostsCache.fetch(async () => { + const chart = await this.notesChart.getChart('hour', 1, null); + return chart.local.total[0]; + }); + } +}