mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 16:14:02 +00:00
feat(bot): add reminders topic binding command
This commit is contained in:
@@ -65,6 +65,40 @@ function bindRejectionMessage(
|
||||
}
|
||||
}
|
||||
|
||||
function bindTopicUsageMessage(
|
||||
locale: BotLocale,
|
||||
role: 'purchase' | 'feedback' | 'reminders'
|
||||
): string {
|
||||
const t = getBotTranslations(locale).setup
|
||||
|
||||
switch (role) {
|
||||
case 'purchase':
|
||||
return t.useBindPurchaseTopicInGroup
|
||||
case 'feedback':
|
||||
return t.useBindFeedbackTopicInGroup
|
||||
case 'reminders':
|
||||
return t.useBindRemindersTopicInGroup
|
||||
}
|
||||
}
|
||||
|
||||
function bindTopicSuccessMessage(
|
||||
locale: BotLocale,
|
||||
role: 'purchase' | 'feedback' | 'reminders',
|
||||
householdName: string,
|
||||
threadId: string
|
||||
): string {
|
||||
const t = getBotTranslations(locale).setup
|
||||
|
||||
switch (role) {
|
||||
case 'purchase':
|
||||
return t.purchaseTopicSaved(householdName, threadId)
|
||||
case 'feedback':
|
||||
return t.feedbackTopicSaved(householdName, threadId)
|
||||
case 'reminders':
|
||||
return t.remindersTopicSaved(householdName, threadId)
|
||||
}
|
||||
}
|
||||
|
||||
function adminRejectionMessage(
|
||||
locale: BotLocale,
|
||||
reason: 'not_admin' | 'household_not_found' | 'pending_not_found'
|
||||
@@ -182,6 +216,63 @@ export function registerHouseholdSetupCommands(options: {
|
||||
miniAppUrl?: string
|
||||
logger?: Logger
|
||||
}): void {
|
||||
async function handleBindTopicCommand(
|
||||
ctx: Context,
|
||||
role: 'purchase' | 'feedback' | 'reminders'
|
||||
): Promise<void> {
|
||||
const locale = await resolveReplyLocale({
|
||||
ctx,
|
||||
repository: options.householdConfigurationRepository
|
||||
})
|
||||
|
||||
if (!isGroupChat(ctx)) {
|
||||
await ctx.reply(bindTopicUsageMessage(locale, role))
|
||||
return
|
||||
}
|
||||
|
||||
const actorIsAdmin = await isGroupAdmin(ctx)
|
||||
const telegramThreadId =
|
||||
isTopicMessage(ctx) && ctx.msg && 'message_thread_id' in ctx.msg
|
||||
? ctx.msg.message_thread_id?.toString()
|
||||
: undefined
|
||||
const result = await options.householdSetupService.bindTopic({
|
||||
actorIsAdmin,
|
||||
telegramChatId: ctx.chat.id.toString(),
|
||||
role,
|
||||
...(telegramThreadId
|
||||
? {
|
||||
telegramThreadId
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
if (result.status === 'rejected') {
|
||||
await ctx.reply(bindRejectionMessage(locale, result.reason))
|
||||
return
|
||||
}
|
||||
|
||||
options.logger?.info(
|
||||
{
|
||||
event: 'household_setup.topic_bound',
|
||||
role: result.binding.role,
|
||||
telegramChatId: result.household.telegramChatId,
|
||||
telegramThreadId: result.binding.telegramThreadId,
|
||||
householdId: result.household.householdId,
|
||||
actorTelegramUserId: ctx.from?.id?.toString()
|
||||
},
|
||||
'Household topic bound'
|
||||
)
|
||||
|
||||
await ctx.reply(
|
||||
bindTopicSuccessMessage(
|
||||
locale,
|
||||
role,
|
||||
result.household.householdName,
|
||||
result.binding.telegramThreadId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
options.bot.command('start', async (ctx) => {
|
||||
const fallbackLocale = await resolveReplyLocale({
|
||||
ctx,
|
||||
@@ -353,103 +444,15 @@ export function registerHouseholdSetupCommands(options: {
|
||||
})
|
||||
|
||||
options.bot.command('bind_purchase_topic', async (ctx) => {
|
||||
const locale = await resolveReplyLocale({
|
||||
ctx,
|
||||
repository: options.householdConfigurationRepository
|
||||
})
|
||||
const t = getBotTranslations(locale)
|
||||
|
||||
if (!isGroupChat(ctx)) {
|
||||
await ctx.reply(t.setup.useBindPurchaseTopicInGroup)
|
||||
return
|
||||
}
|
||||
|
||||
const actorIsAdmin = await isGroupAdmin(ctx)
|
||||
const telegramThreadId =
|
||||
isTopicMessage(ctx) && ctx.msg && 'message_thread_id' in ctx.msg
|
||||
? ctx.msg.message_thread_id?.toString()
|
||||
: undefined
|
||||
const result = await options.householdSetupService.bindTopic({
|
||||
actorIsAdmin,
|
||||
telegramChatId: ctx.chat.id.toString(),
|
||||
role: 'purchase',
|
||||
...(telegramThreadId
|
||||
? {
|
||||
telegramThreadId
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
if (result.status === 'rejected') {
|
||||
await ctx.reply(bindRejectionMessage(locale, result.reason))
|
||||
return
|
||||
}
|
||||
|
||||
options.logger?.info(
|
||||
{
|
||||
event: 'household_setup.topic_bound',
|
||||
role: result.binding.role,
|
||||
telegramChatId: result.household.telegramChatId,
|
||||
telegramThreadId: result.binding.telegramThreadId,
|
||||
householdId: result.household.householdId,
|
||||
actorTelegramUserId: ctx.from?.id?.toString()
|
||||
},
|
||||
'Household topic bound'
|
||||
)
|
||||
|
||||
await ctx.reply(
|
||||
t.setup.purchaseTopicSaved(result.household.householdName, result.binding.telegramThreadId)
|
||||
)
|
||||
await handleBindTopicCommand(ctx, 'purchase')
|
||||
})
|
||||
|
||||
options.bot.command('bind_feedback_topic', async (ctx) => {
|
||||
const locale = await resolveReplyLocale({
|
||||
ctx,
|
||||
repository: options.householdConfigurationRepository
|
||||
})
|
||||
const t = getBotTranslations(locale)
|
||||
await handleBindTopicCommand(ctx, 'feedback')
|
||||
})
|
||||
|
||||
if (!isGroupChat(ctx)) {
|
||||
await ctx.reply(t.setup.useBindFeedbackTopicInGroup)
|
||||
return
|
||||
}
|
||||
|
||||
const actorIsAdmin = await isGroupAdmin(ctx)
|
||||
const telegramThreadId =
|
||||
isTopicMessage(ctx) && ctx.msg && 'message_thread_id' in ctx.msg
|
||||
? ctx.msg.message_thread_id?.toString()
|
||||
: undefined
|
||||
const result = await options.householdSetupService.bindTopic({
|
||||
actorIsAdmin,
|
||||
telegramChatId: ctx.chat.id.toString(),
|
||||
role: 'feedback',
|
||||
...(telegramThreadId
|
||||
? {
|
||||
telegramThreadId
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
if (result.status === 'rejected') {
|
||||
await ctx.reply(bindRejectionMessage(locale, result.reason))
|
||||
return
|
||||
}
|
||||
|
||||
options.logger?.info(
|
||||
{
|
||||
event: 'household_setup.topic_bound',
|
||||
role: result.binding.role,
|
||||
telegramChatId: result.household.telegramChatId,
|
||||
telegramThreadId: result.binding.telegramThreadId,
|
||||
householdId: result.household.householdId,
|
||||
actorTelegramUserId: ctx.from?.id?.toString()
|
||||
},
|
||||
'Household topic bound'
|
||||
)
|
||||
|
||||
await ctx.reply(
|
||||
t.setup.feedbackTopicSaved(result.household.householdName, result.binding.telegramThreadId)
|
||||
)
|
||||
options.bot.command('bind_reminders_topic', async (ctx) => {
|
||||
await handleBindTopicCommand(ctx, 'reminders')
|
||||
})
|
||||
|
||||
options.bot.command('pending_members', async (ctx) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const enBotTranslations: BotTranslationCatalog = {
|
||||
setup: 'Register this group as a household',
|
||||
bind_purchase_topic: 'Bind the current topic as purchases',
|
||||
bind_feedback_topic: 'Bind the current topic as feedback',
|
||||
bind_reminders_topic: 'Bind the current topic as reminders',
|
||||
pending_members: 'List pending household join requests',
|
||||
approve_member: 'Approve a pending household member'
|
||||
},
|
||||
@@ -52,7 +53,7 @@ export const enBotTranslations: BotTranslationCatalog = {
|
||||
[
|
||||
`Household ${created ? 'created' : 'already registered'}: ${householdName}`,
|
||||
`Chat ID: ${telegramChatId}`,
|
||||
'Next: open the purchase topic and run /bind_purchase_topic, then open the feedback topic and run /bind_feedback_topic.',
|
||||
'Next: open the purchase topic and run /bind_purchase_topic, then open the feedback topic and run /bind_feedback_topic. If you want a dedicated reminders topic, open it and run /bind_reminders_topic.',
|
||||
'Members should open the bot chat from the button below and confirm the join request there.'
|
||||
].join('\n'),
|
||||
useBindPurchaseTopicInGroup: 'Use /bind_purchase_topic inside the household group topic.',
|
||||
@@ -61,6 +62,9 @@ export const enBotTranslations: BotTranslationCatalog = {
|
||||
useBindFeedbackTopicInGroup: 'Use /bind_feedback_topic inside the household group topic.',
|
||||
feedbackTopicSaved: (householdName, threadId) =>
|
||||
`Feedback topic saved for ${householdName} (thread ${threadId}).`,
|
||||
useBindRemindersTopicInGroup: 'Use /bind_reminders_topic inside the household group topic.',
|
||||
remindersTopicSaved: (householdName, threadId) =>
|
||||
`Reminders topic saved for ${householdName} (thread ${threadId}).`,
|
||||
usePendingMembersInGroup: 'Use /pending_members inside the household group.',
|
||||
useApproveMemberInGroup: 'Use /approve_member inside the household group.',
|
||||
approveMemberUsage: 'Usage: /approve_member <telegram_user_id>',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
|
||||
setup: 'Подключить эту группу как дом',
|
||||
bind_purchase_topic: 'Назначить текущий топик для покупок',
|
||||
bind_feedback_topic: 'Назначить текущий топик для анонимных сообщений',
|
||||
bind_reminders_topic: 'Назначить текущий топик для напоминаний',
|
||||
pending_members: 'Показать ожидающие заявки на вступление',
|
||||
approve_member: 'Подтвердить участника дома'
|
||||
},
|
||||
@@ -54,7 +55,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
|
||||
[
|
||||
`${created ? 'Дом создан' : 'Дом уже подключён'}: ${householdName}`,
|
||||
`ID чата: ${telegramChatId}`,
|
||||
'Дальше: откройте топик покупок и выполните /bind_purchase_topic, затем откройте топик обратной связи и выполните /bind_feedback_topic.',
|
||||
'Дальше: откройте топик покупок и выполните /bind_purchase_topic, затем откройте топик обратной связи и выполните /bind_feedback_topic. Если хотите отдельный топик для напоминаний, откройте его и выполните /bind_reminders_topic.',
|
||||
'Участники должны открыть чат с ботом по кнопке ниже и подтвердить заявку на вступление.'
|
||||
].join('\n'),
|
||||
useBindPurchaseTopicInGroup: 'Используйте /bind_purchase_topic внутри топика группы дома.',
|
||||
@@ -63,6 +64,9 @@ export const ruBotTranslations: BotTranslationCatalog = {
|
||||
useBindFeedbackTopicInGroup: 'Используйте /bind_feedback_topic внутри топика группы дома.',
|
||||
feedbackTopicSaved: (householdName, threadId) =>
|
||||
`Топик обратной связи сохранён для ${householdName} (тред ${threadId}).`,
|
||||
useBindRemindersTopicInGroup: 'Используйте /bind_reminders_topic внутри топика группы дома.',
|
||||
remindersTopicSaved: (householdName, threadId) =>
|
||||
`Топик напоминаний сохранён для ${householdName} (тред ${threadId}).`,
|
||||
usePendingMembersInGroup: 'Используйте /pending_members внутри группы дома.',
|
||||
useApproveMemberInGroup: 'Используйте /approve_member внутри группы дома.',
|
||||
approveMemberUsage: 'Использование: /approve_member <telegram_user_id>',
|
||||
|
||||
@@ -8,6 +8,7 @@ export type TelegramCommandName =
|
||||
| 'setup'
|
||||
| 'bind_purchase_topic'
|
||||
| 'bind_feedback_topic'
|
||||
| 'bind_reminders_topic'
|
||||
| 'pending_members'
|
||||
| 'approve_member'
|
||||
|
||||
@@ -19,6 +20,7 @@ export interface BotCommandDescriptions {
|
||||
setup: string
|
||||
bind_purchase_topic: string
|
||||
bind_feedback_topic: string
|
||||
bind_reminders_topic: string
|
||||
pending_members: string
|
||||
approve_member: string
|
||||
}
|
||||
@@ -73,6 +75,8 @@ export interface BotTranslationCatalog {
|
||||
purchaseTopicSaved: (householdName: string, threadId: string) => string
|
||||
useBindFeedbackTopicInGroup: string
|
||||
feedbackTopicSaved: (householdName: string, threadId: string) => string
|
||||
useBindRemindersTopicInGroup: string
|
||||
remindersTopicSaved: (householdName: string, threadId: string) => string
|
||||
usePendingMembersInGroup: string
|
||||
useApproveMemberInGroup: string
|
||||
approveMemberUsage: string
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
import type { ReminderJobResult, ReminderJobService } from '@household/application'
|
||||
import { Temporal } from '@household/domain'
|
||||
import type { ReminderTarget } from '@household/ports'
|
||||
|
||||
import { createReminderJobsHandler } from './reminder-jobs'
|
||||
@@ -10,9 +11,16 @@ const target: ReminderTarget = {
|
||||
householdName: 'Kojori House',
|
||||
telegramChatId: '-1001',
|
||||
telegramThreadId: '12',
|
||||
locale: 'ru'
|
||||
locale: 'ru',
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentDueDay: 20,
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3
|
||||
}
|
||||
|
||||
const fixedNow = Temporal.Instant.from('2026-03-03T09:00:00Z')
|
||||
|
||||
describe('createReminderJobsHandler', () => {
|
||||
test('returns per-household dispatch outcome with Telegram delivery metadata', async () => {
|
||||
const claimedResult: ReminderJobResult = {
|
||||
@@ -33,7 +41,8 @@ describe('createReminderJobsHandler', () => {
|
||||
listReminderTargets: async () => [target],
|
||||
releaseReminderDispatch: mock(async () => {}),
|
||||
sendReminderMessage,
|
||||
reminderService
|
||||
reminderService,
|
||||
now: () => fixedNow
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
@@ -73,6 +82,7 @@ describe('createReminderJobsHandler', () => {
|
||||
householdName: 'Kojori House',
|
||||
telegramChatId: '-1001',
|
||||
telegramThreadId: '12',
|
||||
period: '2026-03',
|
||||
dedupeKey: '2026-03:utilities',
|
||||
outcome: 'claimed',
|
||||
messageText: 'Напоминание по коммунальным платежам за 2026-03'
|
||||
@@ -101,7 +111,8 @@ describe('createReminderJobsHandler', () => {
|
||||
releaseReminderDispatch: mock(async () => {}),
|
||||
sendReminderMessage,
|
||||
reminderService,
|
||||
forceDryRun: true
|
||||
forceDryRun: true,
|
||||
now: () => fixedNow
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
@@ -146,7 +157,8 @@ describe('createReminderJobsHandler', () => {
|
||||
sendReminderMessage: mock(async () => {
|
||||
throw new Error('Telegram unavailable')
|
||||
}),
|
||||
reminderService
|
||||
reminderService,
|
||||
now: () => fixedNow
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
@@ -185,7 +197,8 @@ describe('createReminderJobsHandler', () => {
|
||||
handleJob: mock(async () => {
|
||||
throw new Error('should not be called')
|
||||
})
|
||||
}
|
||||
},
|
||||
now: () => fixedNow
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
@@ -202,4 +215,94 @@ describe('createReminderJobsHandler', () => {
|
||||
error: 'Invalid reminder type'
|
||||
})
|
||||
})
|
||||
|
||||
test('skips households whose configured reminder day does not match today when no period override is supplied', async () => {
|
||||
const reminderService: ReminderJobService = {
|
||||
handleJob: mock(async () => {
|
||||
throw new Error('should not be called')
|
||||
})
|
||||
}
|
||||
|
||||
const handler = createReminderJobsHandler({
|
||||
listReminderTargets: async () => [
|
||||
{
|
||||
...target,
|
||||
utilitiesReminderDay: 31
|
||||
}
|
||||
],
|
||||
releaseReminderDispatch: mock(async () => {}),
|
||||
sendReminderMessage: mock(async () => {}),
|
||||
reminderService,
|
||||
now: () => fixedNow
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
new Request('http://localhost/jobs/reminder/utilities', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ jobId: 'job-3' })
|
||||
}),
|
||||
'utilities'
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toMatchObject({
|
||||
ok: true,
|
||||
totals: {
|
||||
targets: 0
|
||||
},
|
||||
dispatches: []
|
||||
})
|
||||
})
|
||||
|
||||
test('honors explicit period overrides even when today is not the configured reminder day', async () => {
|
||||
const dryRunResult: ReminderJobResult = {
|
||||
status: 'dry-run',
|
||||
dedupeKey: '2026-03:rent-due',
|
||||
payloadHash: 'hash',
|
||||
reminderType: 'rent-due',
|
||||
period: '2026-03',
|
||||
messageText: 'Rent due reminder for 2026-03: please settle payment today.'
|
||||
}
|
||||
|
||||
const reminderService: ReminderJobService = {
|
||||
handleJob: mock(async () => dryRunResult)
|
||||
}
|
||||
|
||||
const handler = createReminderJobsHandler({
|
||||
listReminderTargets: async () => [
|
||||
{
|
||||
...target,
|
||||
rentDueDay: 20
|
||||
}
|
||||
],
|
||||
releaseReminderDispatch: mock(async () => {}),
|
||||
sendReminderMessage: mock(async () => {}),
|
||||
reminderService,
|
||||
now: () => fixedNow
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
new Request('http://localhost/jobs/reminder/rent-due', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ period: '2026-03', dryRun: true })
|
||||
}),
|
||||
'rent-due'
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toMatchObject({
|
||||
ok: true,
|
||||
totals: {
|
||||
targets: 1,
|
||||
'dry-run': 1
|
||||
},
|
||||
dispatches: [
|
||||
expect.objectContaining({
|
||||
householdId: 'household-1',
|
||||
period: '2026-03',
|
||||
outcome: 'dry-run'
|
||||
})
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReminderJobService } from '@household/application'
|
||||
import { BillingPeriod, nowInstant } from '@household/domain'
|
||||
import { BillingPeriod, Temporal, nowInstant } from '@household/domain'
|
||||
import type { Logger } from '@household/observability'
|
||||
import { REMINDER_TYPES, type ReminderTarget, type ReminderType } from '@household/ports'
|
||||
|
||||
@@ -32,6 +32,34 @@ function currentPeriod(): string {
|
||||
return BillingPeriod.fromInstant(nowInstant()).toString()
|
||||
}
|
||||
|
||||
function targetLocalDate(target: ReminderTarget, instant: Temporal.Instant) {
|
||||
return instant.toZonedDateTimeISO(target.timezone)
|
||||
}
|
||||
|
||||
function isReminderDueToday(
|
||||
target: ReminderTarget,
|
||||
reminderType: ReminderType,
|
||||
instant: Temporal.Instant
|
||||
): boolean {
|
||||
const currentDay = targetLocalDate(target, instant).day
|
||||
|
||||
switch (reminderType) {
|
||||
case 'utilities':
|
||||
return currentDay === target.utilitiesReminderDay
|
||||
case 'rent-warning':
|
||||
return currentDay === target.rentWarningDay
|
||||
case 'rent-due':
|
||||
return currentDay === target.rentDueDay
|
||||
}
|
||||
}
|
||||
|
||||
function targetPeriod(target: ReminderTarget, instant: Temporal.Instant): string {
|
||||
const localDate = targetLocalDate(target, instant)
|
||||
return BillingPeriod.fromString(
|
||||
`${localDate.year}-${String(localDate.month).padStart(2, '0')}`
|
||||
).toString()
|
||||
}
|
||||
|
||||
async function readBody(request: Request): Promise<ReminderJobRequestBody> {
|
||||
const text = await request.text()
|
||||
|
||||
@@ -56,6 +84,7 @@ export function createReminderJobsHandler(options: {
|
||||
sendReminderMessage: (target: ReminderTarget, text: string) => Promise<void>
|
||||
reminderService: ReminderJobService
|
||||
forceDryRun?: boolean
|
||||
now?: () => Temporal.Instant
|
||||
logger?: Logger
|
||||
}): {
|
||||
handle: (request: Request, rawReminderType: string) => Promise<Response>
|
||||
@@ -83,14 +112,19 @@ export function createReminderJobsHandler(options: {
|
||||
try {
|
||||
const body = await readBody(request)
|
||||
const schedulerJobName = request.headers.get('x-cloudscheduler-jobname')
|
||||
const period = BillingPeriod.fromString(body.period ?? currentPeriod()).toString()
|
||||
const requestedPeriod = body.period
|
||||
? BillingPeriod.fromString(body.period).toString()
|
||||
: null
|
||||
const defaultPeriod = requestedPeriod ?? currentPeriod()
|
||||
const dryRun = options.forceDryRun === true || body.dryRun === true
|
||||
const currentInstant = options.now?.() ?? nowInstant()
|
||||
const targets = await options.listReminderTargets()
|
||||
const dispatches: Array<{
|
||||
householdId: string
|
||||
householdName: string
|
||||
telegramChatId: string
|
||||
telegramThreadId: string | null
|
||||
period: string
|
||||
dedupeKey: string
|
||||
outcome: 'dry-run' | 'claimed' | 'duplicate' | 'failed'
|
||||
messageText: string
|
||||
@@ -98,6 +132,11 @@ export function createReminderJobsHandler(options: {
|
||||
}> = []
|
||||
|
||||
for (const target of targets) {
|
||||
if (!requestedPeriod && !isReminderDueToday(target, reminderType, currentInstant)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const period = requestedPeriod ?? targetPeriod(target, currentInstant)
|
||||
const result = await options.reminderService.handleJob({
|
||||
householdId: target.householdId,
|
||||
period,
|
||||
@@ -148,6 +187,7 @@ export function createReminderJobsHandler(options: {
|
||||
householdName: target.householdName,
|
||||
telegramChatId: target.telegramChatId,
|
||||
telegramThreadId: target.telegramThreadId,
|
||||
period,
|
||||
dedupeKey: result.dedupeKey,
|
||||
outcome,
|
||||
messageText: text,
|
||||
@@ -174,7 +214,7 @@ export function createReminderJobsHandler(options: {
|
||||
ok: true,
|
||||
jobId: body.jobId ?? schedulerJobName ?? null,
|
||||
reminderType,
|
||||
period,
|
||||
period: defaultPeriod,
|
||||
dryRun,
|
||||
totals,
|
||||
dispatches
|
||||
|
||||
@@ -26,6 +26,7 @@ const GROUP_ADMIN_COMMAND_NAMES = [
|
||||
'setup',
|
||||
'bind_purchase_topic',
|
||||
'bind_feedback_topic',
|
||||
'bind_reminders_topic',
|
||||
'pending_members',
|
||||
'approve_member'
|
||||
] as const satisfies readonly TelegramCommandName[]
|
||||
|
||||
Reference in New Issue
Block a user