mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
fix(feedback): normalize anonymous rate limit timestamps
This commit is contained in:
@@ -194,6 +194,31 @@ describe('createAnonymousFeedbackService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('normalizes legacy date-like rate limit values before cooldown checks', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
;(repository.lastAcceptedAt as Instant | null | string) = '2026-03-08T09:00:00.000Z'
|
||||
;(repository.earliestAcceptedAtSince as Instant | null | Date) = new Date(
|
||||
'2026-03-08T09:00:00.000Z'
|
||||
)
|
||||
|
||||
const result = await service.submit({
|
||||
telegramUserId: '123',
|
||||
rawText: 'Please take the trash out tonight',
|
||||
telegramChatId: 'chat-1',
|
||||
telegramMessageId: 'message-1',
|
||||
telegramUpdateId: 'update-1',
|
||||
now: instantFromIso('2026-03-08T12:00:00.000Z')
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'rejected',
|
||||
reason: 'cooldown',
|
||||
nextAllowedAt: instantFromIso('2026-03-08T15:00:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
test('marks posted and failed submissions', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
@@ -2,7 +2,13 @@ import type {
|
||||
AnonymousFeedbackRejectionReason,
|
||||
AnonymousFeedbackRepository
|
||||
} from '@household/ports'
|
||||
import { nowInstant, type Instant, Temporal } from '@household/domain'
|
||||
import {
|
||||
nowInstant,
|
||||
instantFromDate,
|
||||
instantFromIso,
|
||||
type Instant,
|
||||
Temporal
|
||||
} from '@household/domain'
|
||||
|
||||
const MIN_MESSAGE_LENGTH = 12
|
||||
const MAX_MESSAGE_LENGTH = 500
|
||||
@@ -10,6 +16,37 @@ const COOLDOWN_HOURS = 6
|
||||
const DAILY_CAP = 3
|
||||
const BLOCKLIST = ['kill yourself', 'сука', 'тварь', 'идиот', 'idiot', 'hate you'] as const
|
||||
|
||||
function normalizeInstant(value: unknown): Instant | null {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (value instanceof Temporal.Instant) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return instantFromDate(value)
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return instantFromIso(value)
|
||||
}
|
||||
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'epochMilliseconds' in value &&
|
||||
typeof (value as { epochMilliseconds?: unknown }).epochMilliseconds === 'number'
|
||||
) {
|
||||
return Temporal.Instant.fromEpochMilliseconds(
|
||||
(value as { epochMilliseconds: number }).epochMilliseconds
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function collapseWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
@@ -160,9 +197,11 @@ export function createAnonymousFeedbackService(
|
||||
const now = input.now ?? nowInstant()
|
||||
const acceptedSince = now.subtract({ hours: 24 })
|
||||
const rateLimit = await repository.getRateLimitSnapshot(member.id, acceptedSince)
|
||||
const earliestAcceptedAtSince = normalizeInstant(rateLimit.earliestAcceptedAtSince)
|
||||
const lastAcceptedAt = normalizeInstant(rateLimit.lastAcceptedAt)
|
||||
|
||||
if (rateLimit.acceptedCountSince >= DAILY_CAP) {
|
||||
const nextAllowedAt =
|
||||
rateLimit.earliestAcceptedAtSince?.add({ hours: 24 }) ?? now.add({ hours: 24 })
|
||||
const nextAllowedAt = earliestAcceptedAtSince?.add({ hours: 24 }) ?? now.add({ hours: 24 })
|
||||
|
||||
return rejectSubmission(repository, {
|
||||
memberId: member.id,
|
||||
@@ -175,14 +214,14 @@ export function createAnonymousFeedbackService(
|
||||
})
|
||||
}
|
||||
|
||||
if (rateLimit.lastAcceptedAt) {
|
||||
if (lastAcceptedAt) {
|
||||
const cooldownBoundary = now.subtract({ hours: COOLDOWN_HOURS })
|
||||
if (Temporal.Instant.compare(rateLimit.lastAcceptedAt, cooldownBoundary) > 0) {
|
||||
if (Temporal.Instant.compare(lastAcceptedAt, cooldownBoundary) > 0) {
|
||||
return rejectSubmission(repository, {
|
||||
memberId: member.id,
|
||||
rawText: input.rawText,
|
||||
reason: 'cooldown',
|
||||
nextAllowedAt: rateLimit.lastAcceptedAt.add({ hours: COOLDOWN_HOURS }),
|
||||
nextAllowedAt: lastAcceptedAt.add({ hours: COOLDOWN_HOURS }),
|
||||
telegramChatId: input.telegramChatId,
|
||||
telegramMessageId: input.telegramMessageId,
|
||||
telegramUpdateId: input.telegramUpdateId
|
||||
|
||||
Reference in New Issue
Block a user