From 3a92471b68481b1046f73c458f4bb1a15d0bc286 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 1 Oct 2025 20:26:18 -0400 Subject: [PATCH] convert all remaining backend code to TimeService --- packages/backend/src/core/TimeService.ts | 2 + .../backend/src/core/WebhookTestService.ts | 73 +++++++++++-------- .../src/core/activitypub/ApRequestService.ts | 12 ++- .../backend/src/misc/generate-invite-code.ts | 4 +- packages/backend/src/misc/reset-db.ts | 2 + .../src/queue/QueueProcessorService.ts | 13 +++- .../ExportCustomEmojisProcessorService.ts | 2 +- .../api/endpoints/admin/invite/create.ts | 4 +- .../src/server/api/endpoints/invite/create.ts | 2 +- .../src/server/api/endpoints/reset-db.ts | 2 + packages/backend/test/unit/ap-request.ts | 4 +- 11 files changed, 74 insertions(+), 46 deletions(-) diff --git a/packages/backend/src/core/TimeService.ts b/packages/backend/src/core/TimeService.ts index 726ddd1742..c2fdb9d76e 100644 --- a/packages/backend/src/core/TimeService.ts +++ b/packages/backend/src/core/TimeService.ts @@ -145,6 +145,8 @@ export interface PromiseTimerHandle extends PromiseLike { @Injectable() export class NativeTimeService extends TimeService implements OnApplicationShutdown { public get now(): number { + // This is the one place that actually *should* have it + // eslint-disable-next-line no-restricted-properties return Date.now(); } diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index dd90d147c0..d3b2f09841 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -14,16 +14,17 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js'; import { QueueService } from '@/core/QueueService.js'; import { IdService } from '@/core/IdService.js'; +import { TimeService } from '@/core/TimeService.js'; import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; const oneDayMillis = 24 * 60 * 60 * 1000; -function generateDummyUser(override?: Partial): MiUser { +function generateDummyUser(now: number, override?: Partial): MiUser { return { id: 'dummy-user-1', - updatedAt: new Date(Date.now() - oneDayMillis * 7), - lastFetchedAt: new Date(Date.now() - oneDayMillis * 5), - lastActiveDate: new Date(Date.now() - oneDayMillis * 3), + updatedAt: new Date(now - oneDayMillis * 7), + lastFetchedAt: new Date(now - oneDayMillis * 5), + lastActiveDate: new Date(now - oneDayMillis * 3), hideOnlineStatus: false, username: 'dummy1', usernameLower: 'dummy1', @@ -132,31 +133,34 @@ function generateDummyNote(override?: Partial): MiNote { }; } -const dummyUser1 = generateDummyUser(); -const dummyUser2 = generateDummyUser({ - id: 'dummy-user-2', - updatedAt: new Date(Date.now() - oneDayMillis * 30), - lastFetchedAt: new Date(Date.now() - oneDayMillis), - lastActiveDate: new Date(Date.now() - oneDayMillis), - username: 'dummy2', - usernameLower: 'dummy2', - name: 'DummyUser2', - followersCount: 40, - followingCount: 50, - notesCount: 900, -}); -const dummyUser3 = generateDummyUser({ - id: 'dummy-user-3', - updatedAt: new Date(Date.now() - oneDayMillis * 15), - lastFetchedAt: new Date(Date.now() - oneDayMillis * 2), - lastActiveDate: new Date(Date.now() - oneDayMillis * 2), - username: 'dummy3', - usernameLower: 'dummy3', - name: 'DummyUser3', - followersCount: 60, - followingCount: 70, - notesCount: 15900, -}); +function makeDummyUsers(now: number) { + const dummyUser1 = generateDummyUser(now); + const dummyUser2 = generateDummyUser(now, { + id: 'dummy-user-2', + updatedAt: new Date(now - oneDayMillis * 30), + lastFetchedAt: new Date(now - oneDayMillis), + lastActiveDate: new Date(now - oneDayMillis), + username: 'dummy2', + usernameLower: 'dummy2', + name: 'DummyUser2', + followersCount: 40, + followingCount: 50, + notesCount: 900, + }); + const dummyUser3 = generateDummyUser(now, { + id: 'dummy-user-3', + updatedAt: new Date(now - oneDayMillis * 15), + lastFetchedAt: new Date(now - oneDayMillis * 2), + lastActiveDate: new Date(now - oneDayMillis * 2), + username: 'dummy3', + usernameLower: 'dummy3', + name: 'DummyUser3', + followersCount: 60, + followingCount: 70, + notesCount: 15900, + }); + return { dummyUser1, dummyUser2, dummyUser3 }; +} @Injectable() export class WebhookTestService { @@ -169,6 +173,7 @@ export class WebhookTestService { private systemWebhookService: SystemWebhookService, private queueService: QueueService, private readonly idService: IdService, + private readonly timeService: TimeService, ) { } @@ -207,6 +212,8 @@ export class WebhookTestService { this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 }); }; + const { dummyUser1, dummyUser2, dummyUser3 } = makeDummyUsers(this.timeService.now); + const dummyNote1 = generateDummyNote({ userId: dummyUser1.id, user: dummyUser1, @@ -311,6 +318,8 @@ export class WebhookTestService { this.queueService.systemWebhookDeliver(merged, type, contents, { attempts: 1 }); }; + const { dummyUser1, dummyUser2, dummyUser3 } = makeDummyUsers(this.timeService.now); + switch (params.type) { case 'abuseReport': { send('abuseReport', await this.generateAbuseReport({ @@ -396,13 +405,13 @@ export class WebhookTestService { return { id: note.id, threadId: note.threadId ?? note.id, - createdAt: new Date().toISOString(), + createdAt: this.timeService.date.toISOString(), deletedAt: null, text: note.text, cw: note.cw, userId: note.userId, userHost: note.userHost ?? null, - user: await this.toPackedUserLite(note.user ?? generateDummyUser()), + user: await this.toPackedUserLite(note.user ?? generateDummyUser(this.timeService.now)), replyId: note.replyId, renoteId: note.renoteId, isHidden: false, @@ -486,7 +495,7 @@ export class WebhookTestService { uri: null, movedTo: null, alsoKnownAs: [], - createdAt: new Date().toISOString(), + createdAt: this.timeService.date.toISOString(), updatedAt: user.updatedAt?.toISOString() ?? null, lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null, bannerUrl: user.bannerId == null ? null : user.bannerUrl, diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 7669ce9669..85f4481ed2 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -14,6 +14,7 @@ import { UserKeypairService } from '@/core/UserKeypairService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { TimeService } from '@/core/TimeService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; @@ -40,7 +41,7 @@ type PrivateKey = { }; export class ApRequestCreator { - static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record }): Signed { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record, now: Date | string | number }): Signed { const u = new URL(args.url); const digestHeader = args.digest ?? this.createDigest(args.body); @@ -48,7 +49,7 @@ export class ApRequestCreator { url: u.href, method: 'POST', headers: this.#objectAssignWithLcKey({ - 'Date': new Date().toUTCString(), + 'Date': new Date(args.now).toUTCString(), 'Host': u.host, 'Content-Type': 'application/activity+json', 'Digest': digestHeader, @@ -69,7 +70,7 @@ export class ApRequestCreator { return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; } - static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { + static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record, now: Date | string | number }): Signed { const u = new URL(args.url); const request: Request = { @@ -77,7 +78,7 @@ export class ApRequestCreator { method: 'GET', headers: this.#objectAssignWithLcKey({ 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Date': new Date().toUTCString(), + 'Date': new Date(args.now).toUTCString(), 'Host': new URL(args.url).host, }, args.additionalHeaders), }; @@ -150,6 +151,7 @@ export class ApRequestService { private httpRequestService: HttpRequestService, private loggerService: LoggerService, private readonly apUtilityService: ApUtilityService, + private readonly timeService: TimeService, ) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる @@ -171,6 +173,7 @@ export class ApRequestService { digest, additionalHeaders: { }, + now: this.timeService.now, }); await this.httpRequestService.send(url, { @@ -200,6 +203,7 @@ export class ApRequestService { url, additionalHeaders: { }, + now: this.timeService.now, }); const res = await this.httpRequestService.send(url, { diff --git a/packages/backend/src/misc/generate-invite-code.ts b/packages/backend/src/misc/generate-invite-code.ts index 006920cf0e..8a23175a65 100644 --- a/packages/backend/src/misc/generate-invite-code.ts +++ b/packages/backend/src/misc/generate-invite-code.ts @@ -7,13 +7,13 @@ import { secureRndstr } from './secure-rndstr.js'; const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // [0-9A-Z] w/o [01IO] (32 patterns) -export function generateInviteCode(): string { +export function generateInviteCode(now: number): string { const code = secureRndstr(8, { chars: CHARS, }); const uniqueId = []; - let n = Math.floor(Date.now() / 1000 / 60); + let n = Math.floor(now / 1000 / 60); while (true) { uniqueId.push(CHARS[n % CHARS.length]); const t = Math.floor(n / CHARS.length); diff --git a/packages/backend/src/misc/reset-db.ts b/packages/backend/src/misc/reset-db.ts index 75fb4c3e7b..3c1606df39 100644 --- a/packages/backend/src/misc/reset-db.ts +++ b/packages/backend/src/misc/reset-db.ts @@ -24,6 +24,8 @@ export async function resetDb(db: DataSource) { if (i === 3) { throw e; } else { + // Ignore rule - this is just testing code. + // eslint-disable-next-line no-restricted-globals await new Promise(resolve => setTimeout(resolve, 1000)); continue; } diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index f38f64fe68..f20c96218d 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; +import { TimeService } from '@/core/TimeService.js'; import { renderFullError } from '@/misc/render-full-error.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; @@ -61,10 +62,10 @@ function httpRelatedBackoff(attemptsMade: number) { return backoff; } -function getJobInfo(job: Bull.Job | undefined, increment = false): string { +function _getJobInfo(now: number, job: Bull.Job | undefined, increment = false): string { if (job == null) return '-'; - const age = Date.now() - job.timestamp; + const age = now - job.timestamp; const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m` : age > 10000 ? `${Math.floor(age / 1000)}s` @@ -134,9 +135,15 @@ export class QueueProcessorService implements OnApplicationShutdown { private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private cleanProcessorService: CleanProcessorService, private scheduleNotePostProcessorService: ScheduleNotePostProcessorService, + private readonly timeService: TimeService, ) { this.logger = this.queueLoggerService.logger; + // This is just to avoid modifying all the existing code. + const getJobInfo = (job: Bull.Job | undefined, increment = false) => { + return _getJobInfo(this.timeService.now, job, increment); + }; + //#region system { const processer = (job: Bull.Job) => { @@ -559,7 +566,7 @@ export class QueueProcessorService implements OnApplicationShutdown { // Render job if (job) { parts.push('job ['); - parts.push(getJobInfo(job)); + parts.push(_getJobInfo(this.timeService.now, job)); parts.push('] failed: '); } else { parts.push('job failed: '); diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index 2e53afaca9..5f3a30ed1c 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -78,7 +78,7 @@ export class ExportCustomEmojisProcessorService { }); }; - await writeMeta(`{"metaVersion":2,"host":"${this.config.host}","exportedAt":"${new Date().toString()}","emojis":[`); + await writeMeta(`{"metaVersion":2,"host":"${this.config.host}","exportedAt":"${this.timeService.date.toString()}","emojis":[`); const customEmojis = await this.emojisRepository.find({ where: { diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index e52b177e2b..885be61091 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -11,6 +11,7 @@ import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { generateInviteCode } from '@/misc/generate-invite-code.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import type { TimeService } from '@/core/TimeService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -57,6 +58,7 @@ export default class extends Endpoint { // eslint- private inviteCodeEntityService: InviteCodeEntityService, private idService: IdService, private moderationLogService: ModerationLogService, + private readonly timeService: TimeService, ) { super(meta, paramDef, async (ps, me) => { if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) { @@ -71,7 +73,7 @@ export default class extends Endpoint { // eslint- createdBy: me, createdById: me.id, expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, - code: generateInviteCode(), + code: generateInviteCode(this.timeService.now), })); } diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index 21bfaf9a5e..1e96157b55 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -79,7 +79,7 @@ export default class extends Endpoint { // eslint- createdBy: me, createdById: me.id, expiresAt: policies.inviteExpirationTime ? new Date(this.timeService.now + (policies.inviteExpirationTime * 1000 * 60)) : null, - code: generateInviteCode(), + code: generateInviteCode(this.timeService.now), }); return await this.inviteCodeEntityService.pack(ticket, me); diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index fe23160bb8..33189717e2 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -66,6 +66,8 @@ export default class extends Endpoint { // eslint- logger.info('---- Database reset complete.'); + // Ignore rule - this is just testing code. + // eslint-disable-next-line no-restricted-globals await new Promise(resolve => setTimeout(resolve, 1000)); }); } diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index 11364e1735..894caa1528 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -40,7 +40,7 @@ describe('ap-request', () => { 'User-Agent': 'UA', }; - const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers }); + const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers, now: Date.now() }); const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); @@ -56,7 +56,7 @@ describe('ap-request', () => { 'User-Agent': 'UA', }; - const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers }); + const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers, now: Date.now() }); const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');