mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
feat(infra): add aws lambda pulumi deployment target
This commit is contained in:
@@ -6,6 +6,7 @@ WORKDIR /app
|
||||
COPY bun.lock package.json tsconfig.base.json ./
|
||||
COPY apps/bot/package.json apps/bot/package.json
|
||||
COPY apps/miniapp/package.json apps/miniapp/package.json
|
||||
COPY infra/pulumi/aws/package.json infra/pulumi/aws/package.json
|
||||
COPY packages/adapters-db/package.json packages/adapters-db/package.json
|
||||
COPY packages/application/package.json packages/application/package.json
|
||||
COPY packages/config/package.json packages/config/package.json
|
||||
|
||||
37
apps/bot/Dockerfile.lambda
Normal file
37
apps/bot/Dockerfile.lambda
Normal file
@@ -0,0 +1,37 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM oven/bun:1.3.10 AS deps
|
||||
WORKDIR /app
|
||||
|
||||
COPY bun.lock package.json tsconfig.base.json ./
|
||||
COPY apps/bot/package.json apps/bot/package.json
|
||||
COPY apps/miniapp/package.json apps/miniapp/package.json
|
||||
COPY infra/pulumi/aws/package.json infra/pulumi/aws/package.json
|
||||
COPY packages/adapters-db/package.json packages/adapters-db/package.json
|
||||
COPY packages/application/package.json packages/application/package.json
|
||||
COPY packages/config/package.json packages/config/package.json
|
||||
COPY packages/contracts/package.json packages/contracts/package.json
|
||||
COPY packages/db/package.json packages/db/package.json
|
||||
COPY packages/domain/package.json packages/domain/package.json
|
||||
COPY packages/observability/package.json packages/observability/package.json
|
||||
COPY packages/ports/package.json packages/ports/package.json
|
||||
COPY scripts/package.json scripts/package.json
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
FROM deps AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY apps ./apps
|
||||
COPY packages ./packages
|
||||
|
||||
RUN bun run --filter @household/bot build
|
||||
|
||||
FROM oven/bun:1.3.10 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=build /app/apps/bot/dist ./apps/bot/dist
|
||||
|
||||
CMD ["bun", "apps/bot/dist/lambda.js"]
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||
"build": "bun build src/index.ts src/lambda.ts --outdir dist --target bun",
|
||||
"typecheck": "tsgo --project tsconfig.json --noEmit",
|
||||
"test": "bun test --pass-with-no-tests",
|
||||
"lint": "oxlint \"src\""
|
||||
|
||||
843
apps/bot/src/app.ts
Normal file
843
apps/bot/src/app.ts
Normal file
@@ -0,0 +1,843 @@
|
||||
import { webhookCallback } from 'grammy'
|
||||
import type { InlineKeyboardMarkup } from 'grammy/types'
|
||||
|
||||
import {
|
||||
createAnonymousFeedbackService,
|
||||
createFinanceCommandService,
|
||||
createHouseholdAdminService,
|
||||
createHouseholdOnboardingService,
|
||||
createHouseholdSetupService,
|
||||
createLocalePreferenceService,
|
||||
createMiniAppAdminService,
|
||||
createPaymentConfirmationService,
|
||||
createReminderJobService
|
||||
} from '@household/application'
|
||||
import {
|
||||
createDbAnonymousFeedbackRepository,
|
||||
createDbFinanceRepository,
|
||||
createDbHouseholdConfigurationRepository,
|
||||
createDbProcessedBotMessageRepository,
|
||||
createDbReminderDispatchRepository,
|
||||
createDbTelegramPendingActionRepository,
|
||||
createDbTopicMessageHistoryRepository
|
||||
} from '@household/adapters-db'
|
||||
import { configureLogger, getLogger } from '@household/observability'
|
||||
|
||||
import { registerAnonymousFeedback } from './anonymous-feedback'
|
||||
import {
|
||||
createInMemoryAssistantConversationMemoryStore,
|
||||
createInMemoryAssistantRateLimiter,
|
||||
createInMemoryAssistantUsageTracker,
|
||||
registerDmAssistant
|
||||
} from './dm-assistant'
|
||||
import { createFinanceCommandsService } from './finance-commands'
|
||||
import { createTelegramBot } from './bot'
|
||||
import { getBotRuntimeConfig, type BotRuntimeConfig } from './config'
|
||||
import { registerHouseholdSetupCommands } from './household-setup'
|
||||
import { HouseholdContextCache } from './household-context-cache'
|
||||
import { createMiniAppAuthHandler, createMiniAppJoinHandler } from './miniapp-auth'
|
||||
import {
|
||||
createMiniAppApproveMemberHandler,
|
||||
createMiniAppPendingMembersHandler,
|
||||
createMiniAppPromoteMemberHandler,
|
||||
createMiniAppRejectMemberHandler,
|
||||
createMiniAppSettingsHandler,
|
||||
createMiniAppUpdateMemberAbsencePolicyHandler,
|
||||
createMiniAppUpdateMemberDisplayNameHandler,
|
||||
createMiniAppUpdateMemberRentWeightHandler,
|
||||
createMiniAppUpdateMemberStatusHandler,
|
||||
createMiniAppUpdateOwnDisplayNameHandler,
|
||||
createMiniAppUpdateSettingsHandler,
|
||||
createMiniAppUpsertUtilityCategoryHandler
|
||||
} from './miniapp-admin'
|
||||
import { createMiniAppDashboardHandler } from './miniapp-dashboard'
|
||||
import {
|
||||
createMiniAppAddPaymentHandler,
|
||||
createMiniAppAddPurchaseHandler,
|
||||
createMiniAppAddUtilityBillHandler,
|
||||
createMiniAppBillingCycleHandler,
|
||||
createMiniAppCloseCycleHandler,
|
||||
createMiniAppDeletePaymentHandler,
|
||||
createMiniAppDeletePurchaseHandler,
|
||||
createMiniAppDeleteUtilityBillHandler,
|
||||
createMiniAppOpenCycleHandler,
|
||||
createMiniAppRentUpdateHandler,
|
||||
createMiniAppSubmitUtilityBillHandler,
|
||||
createMiniAppUpdatePaymentHandler,
|
||||
createMiniAppUpdatePurchaseHandler,
|
||||
createMiniAppUpdateUtilityBillHandler
|
||||
} from './miniapp-billing'
|
||||
import { createMiniAppLocalePreferenceHandler } from './miniapp-locale'
|
||||
import { createNbgExchangeRateProvider } from './nbg-exchange-rates'
|
||||
import { createOpenAiChatAssistant } from './openai-chat-assistant'
|
||||
import { createOpenAiPurchaseInterpreter } from './openai-purchase-interpreter'
|
||||
import {
|
||||
createPurchaseMessageRepository,
|
||||
registerConfiguredPurchaseTopicIngestion
|
||||
} from './purchase-topic-ingestion'
|
||||
import { registerConfiguredPaymentTopicIngestion } from './payment-topic-ingestion'
|
||||
import { createReminderJobsHandler } from './reminder-jobs'
|
||||
import { registerReminderTopicUtilities } from './reminder-topic-utilities'
|
||||
import { createSchedulerRequestAuthorizer } from './scheduler-auth'
|
||||
import { createBotWebhookServer } from './server'
|
||||
import { createTopicProcessor } from './topic-processor'
|
||||
|
||||
export interface BotRuntimeApp {
|
||||
readonly fetch: (request: Request) => Promise<Response>
|
||||
readonly shutdown: () => Promise<void>
|
||||
readonly runtime: BotRuntimeConfig
|
||||
}
|
||||
|
||||
export async function createBotRuntimeApp(): Promise<BotRuntimeApp> {
|
||||
const runtime = getBotRuntimeConfig()
|
||||
|
||||
configureLogger({
|
||||
level: runtime.logLevel,
|
||||
service: '@household/bot'
|
||||
})
|
||||
|
||||
const logger = getLogger('runtime')
|
||||
const shutdownTasks: Array<() => Promise<void>> = []
|
||||
const householdConfigurationRepositoryClient = runtime.databaseUrl
|
||||
? createDbHouseholdConfigurationRepository(runtime.databaseUrl)
|
||||
: null
|
||||
const bot = createTelegramBot(
|
||||
runtime.telegramBotToken,
|
||||
getLogger('telegram'),
|
||||
householdConfigurationRepositoryClient?.repository
|
||||
)
|
||||
bot.botInfo = await bot.api.getMe()
|
||||
const webhookHandler = webhookCallback(bot, 'std/http', {
|
||||
onTimeout: 'return'
|
||||
})
|
||||
const financeRepositoryClients = new Map<string, ReturnType<typeof createDbFinanceRepository>>()
|
||||
const financeServices = new Map<string, ReturnType<typeof createFinanceCommandService>>()
|
||||
const paymentConfirmationServices = new Map<
|
||||
string,
|
||||
ReturnType<typeof createPaymentConfirmationService>
|
||||
>()
|
||||
const exchangeRateProvider = createNbgExchangeRateProvider({
|
||||
logger: getLogger('fx')
|
||||
})
|
||||
const householdOnboardingService = householdConfigurationRepositoryClient
|
||||
? createHouseholdOnboardingService({
|
||||
repository: householdConfigurationRepositoryClient.repository
|
||||
})
|
||||
: null
|
||||
const miniAppAdminService = householdConfigurationRepositoryClient
|
||||
? createMiniAppAdminService(householdConfigurationRepositoryClient.repository)
|
||||
: null
|
||||
const localePreferenceService = householdConfigurationRepositoryClient
|
||||
? createLocalePreferenceService(householdConfigurationRepositoryClient.repository)
|
||||
: null
|
||||
const telegramPendingActionRepositoryClient = runtime.databaseUrl
|
||||
? createDbTelegramPendingActionRepository(runtime.databaseUrl)
|
||||
: null
|
||||
const processedBotMessageRepositoryClient =
|
||||
runtime.databaseUrl && runtime.assistantEnabled
|
||||
? createDbProcessedBotMessageRepository(runtime.databaseUrl)
|
||||
: null
|
||||
const purchaseRepositoryClient = runtime.databaseUrl
|
||||
? createPurchaseMessageRepository(runtime.databaseUrl)
|
||||
: null
|
||||
const topicMessageHistoryRepositoryClient = runtime.databaseUrl
|
||||
? createDbTopicMessageHistoryRepository(runtime.databaseUrl)
|
||||
: null
|
||||
const purchaseInterpreter = createOpenAiPurchaseInterpreter(
|
||||
runtime.openaiApiKey,
|
||||
runtime.purchaseParserModel
|
||||
)
|
||||
const assistantMemoryStore = createInMemoryAssistantConversationMemoryStore(
|
||||
runtime.assistantMemoryMaxTurns
|
||||
)
|
||||
const assistantRateLimiter = createInMemoryAssistantRateLimiter({
|
||||
burstLimit: runtime.assistantRateLimitBurst,
|
||||
burstWindowMs: runtime.assistantRateLimitBurstWindowMs,
|
||||
rollingLimit: runtime.assistantRateLimitRolling,
|
||||
rollingWindowMs: runtime.assistantRateLimitRollingWindowMs
|
||||
})
|
||||
const assistantUsageTracker = createInMemoryAssistantUsageTracker()
|
||||
const conversationalAssistant = createOpenAiChatAssistant(
|
||||
runtime.openaiApiKey,
|
||||
runtime.assistantModel,
|
||||
runtime.assistantTimeoutMs
|
||||
)
|
||||
const topicProcessor = createTopicProcessor(
|
||||
runtime.openaiApiKey,
|
||||
runtime.topicProcessorModel,
|
||||
runtime.topicProcessorTimeoutMs,
|
||||
getLogger('topic-processor')
|
||||
)
|
||||
const householdContextCache = new HouseholdContextCache()
|
||||
const anonymousFeedbackRepositoryClients = new Map<
|
||||
string,
|
||||
ReturnType<typeof createDbAnonymousFeedbackRepository>
|
||||
>()
|
||||
const anonymousFeedbackServices = new Map<
|
||||
string,
|
||||
ReturnType<typeof createAnonymousFeedbackService>
|
||||
>()
|
||||
|
||||
function financeServiceForHousehold(householdId: string) {
|
||||
const existing = financeServices.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const repositoryClient = financeRepositoryForHousehold(householdId)
|
||||
const service = createFinanceCommandService({
|
||||
householdId,
|
||||
repository: repositoryClient.repository,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
exchangeRateProvider
|
||||
})
|
||||
financeServices.set(householdId, service)
|
||||
return service
|
||||
}
|
||||
|
||||
function financeRepositoryForHousehold(householdId: string) {
|
||||
const existing = financeRepositoryClients.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const repositoryClient = createDbFinanceRepository(runtime.databaseUrl!, householdId)
|
||||
financeRepositoryClients.set(householdId, repositoryClient)
|
||||
shutdownTasks.push(repositoryClient.close)
|
||||
return repositoryClient
|
||||
}
|
||||
|
||||
function paymentConfirmationServiceForHousehold(householdId: string) {
|
||||
const existing = paymentConfirmationServices.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const service = createPaymentConfirmationService({
|
||||
householdId,
|
||||
financeService: financeServiceForHousehold(householdId),
|
||||
repository: financeRepositoryForHousehold(householdId).repository,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
exchangeRateProvider
|
||||
})
|
||||
paymentConfirmationServices.set(householdId, service)
|
||||
return service
|
||||
}
|
||||
|
||||
function anonymousFeedbackServiceForHousehold(householdId: string) {
|
||||
const existing = anonymousFeedbackServices.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const repositoryClient = createDbAnonymousFeedbackRepository(runtime.databaseUrl!, householdId)
|
||||
anonymousFeedbackRepositoryClients.set(householdId, repositoryClient)
|
||||
shutdownTasks.push(repositoryClient.close)
|
||||
|
||||
const service = createAnonymousFeedbackService(repositoryClient.repository)
|
||||
anonymousFeedbackServices.set(householdId, service)
|
||||
return service
|
||||
}
|
||||
|
||||
if (householdConfigurationRepositoryClient) {
|
||||
shutdownTasks.push(householdConfigurationRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (telegramPendingActionRepositoryClient) {
|
||||
shutdownTasks.push(telegramPendingActionRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (processedBotMessageRepositoryClient) {
|
||||
shutdownTasks.push(processedBotMessageRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (purchaseRepositoryClient) {
|
||||
shutdownTasks.push(purchaseRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (topicMessageHistoryRepositoryClient) {
|
||||
shutdownTasks.push(topicMessageHistoryRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
|
||||
registerConfiguredPurchaseTopicIngestion(
|
||||
bot,
|
||||
householdConfigurationRepositoryClient.repository,
|
||||
purchaseRepositoryClient.repository,
|
||||
{
|
||||
...(topicProcessor
|
||||
? {
|
||||
topicProcessor,
|
||||
contextCache: householdContextCache,
|
||||
memoryStore: assistantMemoryStore,
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
historyRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {})
|
||||
}
|
||||
: {}),
|
||||
...(purchaseInterpreter
|
||||
? {
|
||||
interpreter: purchaseInterpreter
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('purchase-ingestion')
|
||||
}
|
||||
)
|
||||
|
||||
registerConfiguredPaymentTopicIngestion(
|
||||
bot,
|
||||
householdConfigurationRepositoryClient.repository,
|
||||
telegramPendingActionRepositoryClient!.repository,
|
||||
financeServiceForHousehold,
|
||||
paymentConfirmationServiceForHousehold,
|
||||
{
|
||||
...(topicProcessor
|
||||
? {
|
||||
topicProcessor,
|
||||
contextCache: householdContextCache,
|
||||
memoryStore: assistantMemoryStore,
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
historyRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {})
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('payment-ingestion')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'purchase-topic-ingestion'
|
||||
},
|
||||
'Purchase topic ingestion is disabled. Set DATABASE_URL to enable Telegram topic lookups.'
|
||||
)
|
||||
}
|
||||
|
||||
if (runtime.financeCommandsEnabled) {
|
||||
const financeCommands = createFinanceCommandsService({
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
financeServiceForHousehold,
|
||||
...(runtime.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: runtime.miniAppUrl,
|
||||
botUsername: bot.botInfo?.username
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
financeCommands.register(bot)
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'finance-commands'
|
||||
},
|
||||
'Finance commands are disabled. Set DATABASE_URL to enable household lookups.'
|
||||
)
|
||||
}
|
||||
|
||||
if (householdConfigurationRepositoryClient) {
|
||||
registerHouseholdSetupCommands({
|
||||
bot,
|
||||
householdSetupService: createHouseholdSetupService(
|
||||
householdConfigurationRepositoryClient.repository
|
||||
),
|
||||
householdAdminService: createHouseholdAdminService(
|
||||
householdConfigurationRepositoryClient.repository
|
||||
),
|
||||
householdOnboardingService: householdOnboardingService!,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
...(telegramPendingActionRepositoryClient
|
||||
? {
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(runtime.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: runtime.miniAppUrl
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('household-setup')
|
||||
})
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'household-setup'
|
||||
},
|
||||
'Household setup commands are disabled. Set DATABASE_URL to enable.'
|
||||
)
|
||||
}
|
||||
|
||||
const reminderJobs = runtime.reminderJobsEnabled
|
||||
? (() => {
|
||||
const reminderRepositoryClient = createDbReminderDispatchRepository(runtime.databaseUrl!)
|
||||
const reminderService = createReminderJobService(reminderRepositoryClient.repository)
|
||||
|
||||
shutdownTasks.push(reminderRepositoryClient.close)
|
||||
|
||||
return createReminderJobsHandler({
|
||||
listReminderTargets: () =>
|
||||
householdConfigurationRepositoryClient!.repository.listReminderTargets(),
|
||||
ensureBillingCycle: async ({ householdId, at }) => {
|
||||
await financeServiceForHousehold(householdId).ensureExpectedCycle(at)
|
||||
},
|
||||
releaseReminderDispatch: (input) =>
|
||||
reminderRepositoryClient.repository.releaseReminderDispatch(input),
|
||||
sendReminderMessage: async (target, content) => {
|
||||
const threadId =
|
||||
target.telegramThreadId !== null ? Number(target.telegramThreadId) : undefined
|
||||
|
||||
if (target.telegramThreadId !== null && (!threadId || !Number.isInteger(threadId))) {
|
||||
throw new Error(
|
||||
`Invalid reminder thread id for household ${target.householdId}: ${target.telegramThreadId}`
|
||||
)
|
||||
}
|
||||
|
||||
await bot.api.sendMessage(target.telegramChatId, content.text, {
|
||||
...(threadId
|
||||
? {
|
||||
message_thread_id: threadId
|
||||
}
|
||||
: {}),
|
||||
...(content.replyMarkup
|
||||
? {
|
||||
reply_markup: content.replyMarkup as InlineKeyboardMarkup
|
||||
}
|
||||
: {})
|
||||
})
|
||||
},
|
||||
reminderService,
|
||||
...(runtime.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: runtime.miniAppUrl
|
||||
}
|
||||
: {}),
|
||||
...(bot.botInfo?.username
|
||||
? {
|
||||
botUsername: bot.botInfo.username
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('scheduler')
|
||||
})
|
||||
})()
|
||||
: null
|
||||
|
||||
if (!runtime.reminderJobsEnabled) {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'reminder-jobs'
|
||||
},
|
||||
'Reminder jobs are disabled. Set DATABASE_URL and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
runtime.anonymousFeedbackEnabled &&
|
||||
householdConfigurationRepositoryClient &&
|
||||
telegramPendingActionRepositoryClient
|
||||
) {
|
||||
registerAnonymousFeedback({
|
||||
bot,
|
||||
anonymousFeedbackServiceForHousehold,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
logger: getLogger('anonymous-feedback')
|
||||
})
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'anonymous-feedback'
|
||||
},
|
||||
'Anonymous feedback is disabled. Set DATABASE_URL to enable household and topic lookups.'
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
runtime.assistantEnabled &&
|
||||
householdConfigurationRepositoryClient &&
|
||||
telegramPendingActionRepositoryClient
|
||||
) {
|
||||
if (processedBotMessageRepositoryClient) {
|
||||
registerDmAssistant({
|
||||
bot,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
messageProcessingRepository: processedBotMessageRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
financeServiceForHousehold,
|
||||
memoryStore: assistantMemoryStore,
|
||||
rateLimiter: assistantRateLimiter,
|
||||
usageTracker: assistantUsageTracker,
|
||||
...(purchaseRepositoryClient
|
||||
? {
|
||||
purchaseRepository: purchaseRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
topicMessageHistoryRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(purchaseInterpreter
|
||||
? {
|
||||
purchaseInterpreter
|
||||
}
|
||||
: {}),
|
||||
...(conversationalAssistant
|
||||
? {
|
||||
assistant: conversationalAssistant
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('dm-assistant')
|
||||
})
|
||||
} else {
|
||||
registerDmAssistant({
|
||||
bot,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
financeServiceForHousehold,
|
||||
memoryStore: assistantMemoryStore,
|
||||
rateLimiter: assistantRateLimiter,
|
||||
usageTracker: assistantUsageTracker,
|
||||
...(purchaseRepositoryClient
|
||||
? {
|
||||
purchaseRepository: purchaseRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
topicMessageHistoryRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(purchaseInterpreter
|
||||
? {
|
||||
purchaseInterpreter
|
||||
}
|
||||
: {}),
|
||||
...(conversationalAssistant
|
||||
? {
|
||||
assistant: conversationalAssistant
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('dm-assistant')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (householdConfigurationRepositoryClient && telegramPendingActionRepositoryClient) {
|
||||
registerReminderTopicUtilities({
|
||||
bot,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('reminder-utilities')
|
||||
})
|
||||
}
|
||||
|
||||
const server = createBotWebhookServer({
|
||||
webhookPath: runtime.telegramWebhookPath,
|
||||
webhookSecret: runtime.telegramWebhookSecret,
|
||||
webhookHandler,
|
||||
miniAppAuth: householdOnboardingService
|
||||
? createMiniAppAuthHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
logger: getLogger('miniapp-auth')
|
||||
})
|
||||
: undefined,
|
||||
miniAppJoin: householdOnboardingService
|
||||
? createMiniAppJoinHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
logger: getLogger('miniapp-auth')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDashboard: householdOnboardingService
|
||||
? createMiniAppDashboardHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
financeServiceForHousehold,
|
||||
onboardingService: householdOnboardingService,
|
||||
logger: getLogger('miniapp-dashboard')
|
||||
})
|
||||
: undefined,
|
||||
miniAppPendingMembers: householdOnboardingService
|
||||
? createMiniAppPendingMembersHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppApproveMember: householdOnboardingService
|
||||
? createMiniAppApproveMemberHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppRejectMember: householdOnboardingService
|
||||
? createMiniAppRejectMemberHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppSettings: householdOnboardingService
|
||||
? createMiniAppSettingsHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
assistantUsageTracker,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateSettings: householdOnboardingService
|
||||
? createMiniAppUpdateSettingsHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpsertUtilityCategory: householdOnboardingService
|
||||
? createMiniAppUpsertUtilityCategoryHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppPromoteMember: householdOnboardingService
|
||||
? createMiniAppPromoteMemberHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateOwnDisplayName: householdOnboardingService
|
||||
? createMiniAppUpdateOwnDisplayNameHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberDisplayName: householdOnboardingService
|
||||
? createMiniAppUpdateMemberDisplayNameHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberRentWeight: householdOnboardingService
|
||||
? createMiniAppUpdateMemberRentWeightHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberStatus: householdOnboardingService
|
||||
? createMiniAppUpdateMemberStatusHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberAbsencePolicy: householdOnboardingService
|
||||
? createMiniAppUpdateMemberAbsencePolicyHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppBillingCycle: householdOnboardingService
|
||||
? createMiniAppBillingCycleHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppOpenCycle: householdOnboardingService
|
||||
? createMiniAppOpenCycleHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppCloseCycle: householdOnboardingService
|
||||
? createMiniAppCloseCycleHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppRentUpdate: householdOnboardingService
|
||||
? createMiniAppRentUpdateHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppAddUtilityBill: householdOnboardingService
|
||||
? createMiniAppAddUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppSubmitUtilityBill: householdOnboardingService
|
||||
? createMiniAppSubmitUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateUtilityBill: householdOnboardingService
|
||||
? createMiniAppUpdateUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDeleteUtilityBill: householdOnboardingService
|
||||
? createMiniAppDeleteUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppAddPurchase: householdOnboardingService
|
||||
? createMiniAppAddPurchaseHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdatePurchase: householdOnboardingService
|
||||
? createMiniAppUpdatePurchaseHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDeletePurchase: householdOnboardingService
|
||||
? createMiniAppDeletePurchaseHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppAddPayment: householdOnboardingService
|
||||
? createMiniAppAddPaymentHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdatePayment: householdOnboardingService
|
||||
? createMiniAppUpdatePaymentHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDeletePayment: householdOnboardingService
|
||||
? createMiniAppDeletePaymentHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppLocalePreference: householdOnboardingService
|
||||
? createMiniAppLocalePreferenceHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
localePreferenceService: localePreferenceService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
scheduler:
|
||||
reminderJobs && runtime.schedulerSharedSecret
|
||||
? {
|
||||
authorize: createSchedulerRequestAuthorizer({
|
||||
sharedSecret: runtime.schedulerSharedSecret,
|
||||
oidcAllowedEmails: runtime.schedulerOidcAllowedEmails
|
||||
}).authorize,
|
||||
handler: reminderJobs.handle
|
||||
}
|
||||
: reminderJobs
|
||||
? {
|
||||
authorize: createSchedulerRequestAuthorizer({
|
||||
oidcAllowedEmails: runtime.schedulerOidcAllowedEmails
|
||||
}).authorize,
|
||||
handler: reminderJobs.handle
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
|
||||
return {
|
||||
fetch: server.fetch,
|
||||
runtime,
|
||||
shutdown: async () => {
|
||||
await Promise.allSettled(shutdownTasks.map((close) => close()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,841 +1,22 @@
|
||||
import { webhookCallback } from 'grammy'
|
||||
import type { InlineKeyboardMarkup } from 'grammy/types'
|
||||
import { getLogger } from '@household/observability'
|
||||
|
||||
import {
|
||||
createAnonymousFeedbackService,
|
||||
createHouseholdAdminService,
|
||||
createFinanceCommandService,
|
||||
createHouseholdOnboardingService,
|
||||
createLocalePreferenceService,
|
||||
createMiniAppAdminService,
|
||||
createHouseholdSetupService,
|
||||
createReminderJobService,
|
||||
createPaymentConfirmationService
|
||||
} from '@household/application'
|
||||
import {
|
||||
createDbAnonymousFeedbackRepository,
|
||||
createDbFinanceRepository,
|
||||
createDbHouseholdConfigurationRepository,
|
||||
createDbProcessedBotMessageRepository,
|
||||
createDbReminderDispatchRepository,
|
||||
createDbTelegramPendingActionRepository,
|
||||
createDbTopicMessageHistoryRepository
|
||||
} from '@household/adapters-db'
|
||||
import { configureLogger, getLogger } from '@household/observability'
|
||||
|
||||
import { registerAnonymousFeedback } from './anonymous-feedback'
|
||||
import {
|
||||
createInMemoryAssistantConversationMemoryStore,
|
||||
createInMemoryAssistantRateLimiter,
|
||||
createInMemoryAssistantUsageTracker,
|
||||
registerDmAssistant
|
||||
} from './dm-assistant'
|
||||
import { createFinanceCommandsService } from './finance-commands'
|
||||
import { createTelegramBot } from './bot'
|
||||
import { getBotRuntimeConfig } from './config'
|
||||
import { registerHouseholdSetupCommands } from './household-setup'
|
||||
import { createOpenAiChatAssistant } from './openai-chat-assistant'
|
||||
import { createOpenAiPurchaseInterpreter } from './openai-purchase-interpreter'
|
||||
import { createTopicProcessor } from './topic-processor'
|
||||
import { HouseholdContextCache } from './household-context-cache'
|
||||
import {
|
||||
createPurchaseMessageRepository,
|
||||
registerConfiguredPurchaseTopicIngestion
|
||||
} from './purchase-topic-ingestion'
|
||||
import { registerConfiguredPaymentTopicIngestion } from './payment-topic-ingestion'
|
||||
import { createReminderJobsHandler } from './reminder-jobs'
|
||||
import { registerReminderTopicUtilities } from './reminder-topic-utilities'
|
||||
import { createSchedulerRequestAuthorizer } from './scheduler-auth'
|
||||
import { createBotWebhookServer } from './server'
|
||||
import { createMiniAppAuthHandler, createMiniAppJoinHandler } from './miniapp-auth'
|
||||
import { createMiniAppDashboardHandler } from './miniapp-dashboard'
|
||||
import {
|
||||
createMiniAppApproveMemberHandler,
|
||||
createMiniAppRejectMemberHandler,
|
||||
createMiniAppPendingMembersHandler,
|
||||
createMiniAppPromoteMemberHandler,
|
||||
createMiniAppSettingsHandler,
|
||||
createMiniAppUpdateMemberDisplayNameHandler,
|
||||
createMiniAppUpdateMemberAbsencePolicyHandler,
|
||||
createMiniAppUpdateOwnDisplayNameHandler,
|
||||
createMiniAppUpdateMemberStatusHandler,
|
||||
createMiniAppUpdateMemberRentWeightHandler,
|
||||
createMiniAppUpdateSettingsHandler,
|
||||
createMiniAppUpsertUtilityCategoryHandler
|
||||
} from './miniapp-admin'
|
||||
import {
|
||||
createMiniAppAddPaymentHandler,
|
||||
createMiniAppAddPurchaseHandler,
|
||||
createMiniAppAddUtilityBillHandler,
|
||||
createMiniAppBillingCycleHandler,
|
||||
createMiniAppCloseCycleHandler,
|
||||
createMiniAppDeletePaymentHandler,
|
||||
createMiniAppDeletePurchaseHandler,
|
||||
createMiniAppDeleteUtilityBillHandler,
|
||||
createMiniAppOpenCycleHandler,
|
||||
createMiniAppRentUpdateHandler,
|
||||
createMiniAppSubmitUtilityBillHandler,
|
||||
createMiniAppUpdatePaymentHandler,
|
||||
createMiniAppUpdatePurchaseHandler,
|
||||
createMiniAppUpdateUtilityBillHandler
|
||||
} from './miniapp-billing'
|
||||
import { createMiniAppLocalePreferenceHandler } from './miniapp-locale'
|
||||
import { createNbgExchangeRateProvider } from './nbg-exchange-rates'
|
||||
|
||||
const runtime = getBotRuntimeConfig()
|
||||
configureLogger({
|
||||
level: runtime.logLevel,
|
||||
service: '@household/bot'
|
||||
})
|
||||
|
||||
const logger = getLogger('runtime')
|
||||
const shutdownTasks: Array<() => Promise<void>> = []
|
||||
const householdConfigurationRepositoryClient = runtime.databaseUrl
|
||||
? createDbHouseholdConfigurationRepository(runtime.databaseUrl)
|
||||
: null
|
||||
const bot = createTelegramBot(
|
||||
runtime.telegramBotToken,
|
||||
getLogger('telegram'),
|
||||
householdConfigurationRepositoryClient?.repository
|
||||
)
|
||||
bot.botInfo = await bot.api.getMe()
|
||||
const webhookHandler = webhookCallback(bot, 'std/http', {
|
||||
onTimeout: 'return'
|
||||
})
|
||||
const financeRepositoryClients = new Map<string, ReturnType<typeof createDbFinanceRepository>>()
|
||||
const financeServices = new Map<string, ReturnType<typeof createFinanceCommandService>>()
|
||||
const paymentConfirmationServices = new Map<
|
||||
string,
|
||||
ReturnType<typeof createPaymentConfirmationService>
|
||||
>()
|
||||
const exchangeRateProvider = createNbgExchangeRateProvider({
|
||||
logger: getLogger('fx')
|
||||
})
|
||||
const householdOnboardingService = householdConfigurationRepositoryClient
|
||||
? createHouseholdOnboardingService({
|
||||
repository: householdConfigurationRepositoryClient.repository
|
||||
})
|
||||
: null
|
||||
const miniAppAdminService = householdConfigurationRepositoryClient
|
||||
? createMiniAppAdminService(householdConfigurationRepositoryClient.repository)
|
||||
: null
|
||||
const localePreferenceService = householdConfigurationRepositoryClient
|
||||
? createLocalePreferenceService(householdConfigurationRepositoryClient.repository)
|
||||
: null
|
||||
const telegramPendingActionRepositoryClient = runtime.databaseUrl
|
||||
? createDbTelegramPendingActionRepository(runtime.databaseUrl!)
|
||||
: null
|
||||
const processedBotMessageRepositoryClient =
|
||||
runtime.databaseUrl && runtime.assistantEnabled
|
||||
? createDbProcessedBotMessageRepository(runtime.databaseUrl!)
|
||||
: null
|
||||
const purchaseRepositoryClient = runtime.databaseUrl
|
||||
? createPurchaseMessageRepository(runtime.databaseUrl!)
|
||||
: null
|
||||
const topicMessageHistoryRepositoryClient = runtime.databaseUrl
|
||||
? createDbTopicMessageHistoryRepository(runtime.databaseUrl!)
|
||||
: null
|
||||
const purchaseInterpreter = createOpenAiPurchaseInterpreter(
|
||||
runtime.openaiApiKey,
|
||||
runtime.purchaseParserModel
|
||||
)
|
||||
const assistantMemoryStore = createInMemoryAssistantConversationMemoryStore(
|
||||
runtime.assistantMemoryMaxTurns
|
||||
)
|
||||
const assistantRateLimiter = createInMemoryAssistantRateLimiter({
|
||||
burstLimit: runtime.assistantRateLimitBurst,
|
||||
burstWindowMs: runtime.assistantRateLimitBurstWindowMs,
|
||||
rollingLimit: runtime.assistantRateLimitRolling,
|
||||
rollingWindowMs: runtime.assistantRateLimitRollingWindowMs
|
||||
})
|
||||
const assistantUsageTracker = createInMemoryAssistantUsageTracker()
|
||||
const conversationalAssistant = createOpenAiChatAssistant(
|
||||
runtime.openaiApiKey,
|
||||
runtime.assistantModel,
|
||||
runtime.assistantTimeoutMs
|
||||
)
|
||||
const topicProcessor = createTopicProcessor(
|
||||
runtime.openaiApiKey,
|
||||
runtime.topicProcessorModel,
|
||||
runtime.topicProcessorTimeoutMs,
|
||||
getLogger('topic-processor')
|
||||
)
|
||||
const householdContextCache = new HouseholdContextCache()
|
||||
const anonymousFeedbackRepositoryClients = new Map<
|
||||
string,
|
||||
ReturnType<typeof createDbAnonymousFeedbackRepository>
|
||||
>()
|
||||
const anonymousFeedbackServices = new Map<
|
||||
string,
|
||||
ReturnType<typeof createAnonymousFeedbackService>
|
||||
>()
|
||||
|
||||
function financeServiceForHousehold(householdId: string) {
|
||||
const existing = financeServices.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const repositoryClient = financeRepositoryForHousehold(householdId)
|
||||
const service = createFinanceCommandService({
|
||||
householdId,
|
||||
repository: repositoryClient.repository,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
exchangeRateProvider
|
||||
})
|
||||
financeServices.set(householdId, service)
|
||||
return service
|
||||
}
|
||||
|
||||
function financeRepositoryForHousehold(householdId: string) {
|
||||
const existing = financeRepositoryClients.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const repositoryClient = createDbFinanceRepository(runtime.databaseUrl!, householdId)
|
||||
financeRepositoryClients.set(householdId, repositoryClient)
|
||||
shutdownTasks.push(repositoryClient.close)
|
||||
return repositoryClient
|
||||
}
|
||||
|
||||
function paymentConfirmationServiceForHousehold(householdId: string) {
|
||||
const existing = paymentConfirmationServices.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const service = createPaymentConfirmationService({
|
||||
householdId,
|
||||
financeService: financeServiceForHousehold(householdId),
|
||||
repository: financeRepositoryForHousehold(householdId).repository,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
exchangeRateProvider
|
||||
})
|
||||
paymentConfirmationServices.set(householdId, service)
|
||||
return service
|
||||
}
|
||||
|
||||
function anonymousFeedbackServiceForHousehold(householdId: string) {
|
||||
const existing = anonymousFeedbackServices.get(householdId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const repositoryClient = createDbAnonymousFeedbackRepository(runtime.databaseUrl!, householdId)
|
||||
anonymousFeedbackRepositoryClients.set(householdId, repositoryClient)
|
||||
shutdownTasks.push(repositoryClient.close)
|
||||
|
||||
const service = createAnonymousFeedbackService(repositoryClient.repository)
|
||||
anonymousFeedbackServices.set(householdId, service)
|
||||
return service
|
||||
}
|
||||
|
||||
if (householdConfigurationRepositoryClient) {
|
||||
shutdownTasks.push(householdConfigurationRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (telegramPendingActionRepositoryClient) {
|
||||
shutdownTasks.push(telegramPendingActionRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (processedBotMessageRepositoryClient) {
|
||||
shutdownTasks.push(processedBotMessageRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (purchaseRepositoryClient) {
|
||||
shutdownTasks.push(purchaseRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (topicMessageHistoryRepositoryClient) {
|
||||
shutdownTasks.push(topicMessageHistoryRepositoryClient.close)
|
||||
}
|
||||
|
||||
if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
|
||||
registerConfiguredPurchaseTopicIngestion(
|
||||
bot,
|
||||
householdConfigurationRepositoryClient.repository,
|
||||
purchaseRepositoryClient.repository,
|
||||
{
|
||||
...(topicProcessor
|
||||
? {
|
||||
topicProcessor,
|
||||
contextCache: householdContextCache,
|
||||
memoryStore: assistantMemoryStore,
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
historyRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {})
|
||||
}
|
||||
: {}),
|
||||
...(purchaseInterpreter
|
||||
? {
|
||||
interpreter: purchaseInterpreter
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('purchase-ingestion')
|
||||
}
|
||||
)
|
||||
|
||||
registerConfiguredPaymentTopicIngestion(
|
||||
bot,
|
||||
householdConfigurationRepositoryClient.repository,
|
||||
telegramPendingActionRepositoryClient!.repository,
|
||||
financeServiceForHousehold,
|
||||
paymentConfirmationServiceForHousehold,
|
||||
{
|
||||
...(topicProcessor
|
||||
? {
|
||||
topicProcessor,
|
||||
contextCache: householdContextCache,
|
||||
memoryStore: assistantMemoryStore,
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
historyRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {})
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('payment-ingestion')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'purchase-topic-ingestion'
|
||||
},
|
||||
'Purchase topic ingestion is disabled. Set DATABASE_URL to enable Telegram topic lookups.'
|
||||
)
|
||||
}
|
||||
|
||||
if (runtime.financeCommandsEnabled) {
|
||||
const financeCommands = createFinanceCommandsService({
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
financeServiceForHousehold,
|
||||
...(runtime.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: runtime.miniAppUrl,
|
||||
botUsername: bot.botInfo?.username
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
financeCommands.register(bot)
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'finance-commands'
|
||||
},
|
||||
'Finance commands are disabled. Set DATABASE_URL to enable household lookups.'
|
||||
)
|
||||
}
|
||||
|
||||
if (householdConfigurationRepositoryClient) {
|
||||
registerHouseholdSetupCommands({
|
||||
bot,
|
||||
householdSetupService: createHouseholdSetupService(
|
||||
householdConfigurationRepositoryClient.repository
|
||||
),
|
||||
householdAdminService: createHouseholdAdminService(
|
||||
householdConfigurationRepositoryClient.repository
|
||||
),
|
||||
householdOnboardingService: householdOnboardingService!,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
...(telegramPendingActionRepositoryClient
|
||||
? {
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(runtime.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: runtime.miniAppUrl
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('household-setup')
|
||||
})
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'household-setup'
|
||||
},
|
||||
'Household setup commands are disabled. Set DATABASE_URL to enable.'
|
||||
)
|
||||
}
|
||||
|
||||
const reminderJobs = runtime.reminderJobsEnabled
|
||||
? (() => {
|
||||
const reminderRepositoryClient = createDbReminderDispatchRepository(runtime.databaseUrl!)
|
||||
const reminderService = createReminderJobService(reminderRepositoryClient.repository)
|
||||
|
||||
shutdownTasks.push(reminderRepositoryClient.close)
|
||||
|
||||
return createReminderJobsHandler({
|
||||
listReminderTargets: () =>
|
||||
householdConfigurationRepositoryClient!.repository.listReminderTargets(),
|
||||
ensureBillingCycle: async ({ householdId, at }) => {
|
||||
await financeServiceForHousehold(householdId).ensureExpectedCycle(at)
|
||||
},
|
||||
releaseReminderDispatch: (input) =>
|
||||
reminderRepositoryClient.repository.releaseReminderDispatch(input),
|
||||
sendReminderMessage: async (target, content) => {
|
||||
const threadId =
|
||||
target.telegramThreadId !== null ? Number(target.telegramThreadId) : undefined
|
||||
|
||||
if (target.telegramThreadId !== null && (!threadId || !Number.isInteger(threadId))) {
|
||||
throw new Error(
|
||||
`Invalid reminder thread id for household ${target.householdId}: ${target.telegramThreadId}`
|
||||
)
|
||||
}
|
||||
|
||||
await bot.api.sendMessage(target.telegramChatId, content.text, {
|
||||
...(threadId
|
||||
? {
|
||||
message_thread_id: threadId
|
||||
}
|
||||
: {}),
|
||||
...(content.replyMarkup
|
||||
? {
|
||||
reply_markup: content.replyMarkup as InlineKeyboardMarkup
|
||||
}
|
||||
: {})
|
||||
})
|
||||
},
|
||||
reminderService,
|
||||
...(runtime.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: runtime.miniAppUrl
|
||||
}
|
||||
: {}),
|
||||
...(bot.botInfo?.username
|
||||
? {
|
||||
botUsername: bot.botInfo.username
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('scheduler')
|
||||
})
|
||||
})()
|
||||
: null
|
||||
|
||||
if (!runtime.reminderJobsEnabled) {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'reminder-jobs'
|
||||
},
|
||||
'Reminder jobs are disabled. Set DATABASE_URL and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
runtime.anonymousFeedbackEnabled &&
|
||||
householdConfigurationRepositoryClient &&
|
||||
telegramPendingActionRepositoryClient
|
||||
) {
|
||||
registerAnonymousFeedback({
|
||||
bot,
|
||||
anonymousFeedbackServiceForHousehold,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient!.repository,
|
||||
logger: getLogger('anonymous-feedback')
|
||||
})
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
event: 'runtime.feature_disabled',
|
||||
feature: 'anonymous-feedback'
|
||||
},
|
||||
'Anonymous feedback is disabled. Set DATABASE_URL to enable household and topic lookups.'
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
runtime.assistantEnabled &&
|
||||
householdConfigurationRepositoryClient &&
|
||||
telegramPendingActionRepositoryClient
|
||||
) {
|
||||
if (processedBotMessageRepositoryClient) {
|
||||
registerDmAssistant({
|
||||
bot,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
messageProcessingRepository: processedBotMessageRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
financeServiceForHousehold,
|
||||
memoryStore: assistantMemoryStore,
|
||||
rateLimiter: assistantRateLimiter,
|
||||
usageTracker: assistantUsageTracker,
|
||||
...(purchaseRepositoryClient
|
||||
? {
|
||||
purchaseRepository: purchaseRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
topicMessageHistoryRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(purchaseInterpreter
|
||||
? {
|
||||
purchaseInterpreter
|
||||
}
|
||||
: {}),
|
||||
...(conversationalAssistant
|
||||
? {
|
||||
assistant: conversationalAssistant
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('dm-assistant')
|
||||
})
|
||||
} else {
|
||||
registerDmAssistant({
|
||||
bot,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
financeServiceForHousehold,
|
||||
memoryStore: assistantMemoryStore,
|
||||
rateLimiter: assistantRateLimiter,
|
||||
usageTracker: assistantUsageTracker,
|
||||
...(purchaseRepositoryClient
|
||||
? {
|
||||
purchaseRepository: purchaseRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(topicMessageHistoryRepositoryClient
|
||||
? {
|
||||
topicMessageHistoryRepository: topicMessageHistoryRepositoryClient.repository
|
||||
}
|
||||
: {}),
|
||||
...(purchaseInterpreter
|
||||
? {
|
||||
purchaseInterpreter
|
||||
}
|
||||
: {}),
|
||||
...(conversationalAssistant
|
||||
? {
|
||||
assistant: conversationalAssistant
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('dm-assistant')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (householdConfigurationRepositoryClient && telegramPendingActionRepositoryClient) {
|
||||
registerReminderTopicUtilities({
|
||||
bot,
|
||||
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
|
||||
promptRepository: telegramPendingActionRepositoryClient.repository,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('reminder-utilities')
|
||||
})
|
||||
}
|
||||
|
||||
const server = createBotWebhookServer({
|
||||
webhookPath: runtime.telegramWebhookPath,
|
||||
webhookSecret: runtime.telegramWebhookSecret,
|
||||
webhookHandler,
|
||||
miniAppAuth: householdOnboardingService
|
||||
? createMiniAppAuthHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
logger: getLogger('miniapp-auth')
|
||||
})
|
||||
: undefined,
|
||||
miniAppJoin: householdOnboardingService
|
||||
? createMiniAppJoinHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
logger: getLogger('miniapp-auth')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDashboard: householdOnboardingService
|
||||
? createMiniAppDashboardHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
financeServiceForHousehold,
|
||||
onboardingService: householdOnboardingService!,
|
||||
logger: getLogger('miniapp-dashboard')
|
||||
})
|
||||
: undefined,
|
||||
miniAppPendingMembers: householdOnboardingService
|
||||
? createMiniAppPendingMembersHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppApproveMember: householdOnboardingService
|
||||
? createMiniAppApproveMemberHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppRejectMember: householdOnboardingService
|
||||
? createMiniAppRejectMemberHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppSettings: householdOnboardingService
|
||||
? createMiniAppSettingsHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
assistantUsageTracker,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateSettings: householdOnboardingService
|
||||
? createMiniAppUpdateSettingsHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpsertUtilityCategory: householdOnboardingService
|
||||
? createMiniAppUpsertUtilityCategoryHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppPromoteMember: householdOnboardingService
|
||||
? createMiniAppPromoteMemberHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateOwnDisplayName: householdOnboardingService
|
||||
? createMiniAppUpdateOwnDisplayNameHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberDisplayName: householdOnboardingService
|
||||
? createMiniAppUpdateMemberDisplayNameHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberRentWeight: householdOnboardingService
|
||||
? createMiniAppUpdateMemberRentWeightHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberStatus: householdOnboardingService
|
||||
? createMiniAppUpdateMemberStatusHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberAbsencePolicy: householdOnboardingService
|
||||
? createMiniAppUpdateMemberAbsencePolicyHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppBillingCycle: householdOnboardingService
|
||||
? createMiniAppBillingCycleHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppOpenCycle: householdOnboardingService
|
||||
? createMiniAppOpenCycleHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppCloseCycle: householdOnboardingService
|
||||
? createMiniAppCloseCycleHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppRentUpdate: householdOnboardingService
|
||||
? createMiniAppRentUpdateHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppAddUtilityBill: householdOnboardingService
|
||||
? createMiniAppAddUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppSubmitUtilityBill: householdOnboardingService
|
||||
? createMiniAppSubmitUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateUtilityBill: householdOnboardingService
|
||||
? createMiniAppUpdateUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDeleteUtilityBill: householdOnboardingService
|
||||
? createMiniAppDeleteUtilityBillHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppAddPurchase: householdOnboardingService
|
||||
? createMiniAppAddPurchaseHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdatePurchase: householdOnboardingService
|
||||
? createMiniAppUpdatePurchaseHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDeletePurchase: householdOnboardingService
|
||||
? createMiniAppDeletePurchaseHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppAddPayment: householdOnboardingService
|
||||
? createMiniAppAddPaymentHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdatePayment: householdOnboardingService
|
||||
? createMiniAppUpdatePaymentHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppDeletePayment: householdOnboardingService
|
||||
? createMiniAppDeletePaymentHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
financeServiceForHousehold,
|
||||
logger: getLogger('miniapp-billing')
|
||||
})
|
||||
: undefined,
|
||||
miniAppLocalePreference: householdOnboardingService
|
||||
? createMiniAppLocalePreferenceHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
localePreferenceService: localePreferenceService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
scheduler:
|
||||
reminderJobs && runtime.schedulerSharedSecret
|
||||
? {
|
||||
authorize: createSchedulerRequestAuthorizer({
|
||||
sharedSecret: runtime.schedulerSharedSecret,
|
||||
oidcAllowedEmails: runtime.schedulerOidcAllowedEmails
|
||||
}).authorize,
|
||||
handler: reminderJobs.handle
|
||||
}
|
||||
: reminderJobs
|
||||
? {
|
||||
authorize: createSchedulerRequestAuthorizer({
|
||||
oidcAllowedEmails: runtime.schedulerOidcAllowedEmails
|
||||
}).authorize,
|
||||
handler: reminderJobs.handle
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
import { createBotRuntimeApp } from './app'
|
||||
|
||||
if (import.meta.main) {
|
||||
const app = await createBotRuntimeApp()
|
||||
const logger = getLogger('runtime')
|
||||
|
||||
Bun.serve({
|
||||
port: runtime.port,
|
||||
fetch: server.fetch
|
||||
port: app.runtime.port,
|
||||
fetch: app.fetch
|
||||
})
|
||||
|
||||
logger.info(
|
||||
{
|
||||
event: 'runtime.started',
|
||||
port: runtime.port,
|
||||
webhookPath: runtime.telegramWebhookPath
|
||||
mode: 'bun',
|
||||
port: app.runtime.port,
|
||||
webhookPath: app.runtime.telegramWebhookPath
|
||||
},
|
||||
'Bot webhook server started'
|
||||
)
|
||||
@@ -849,10 +30,6 @@ if (import.meta.main) {
|
||||
'Bot shutdown requested'
|
||||
)
|
||||
|
||||
for (const close of shutdownTasks) {
|
||||
void close()
|
||||
}
|
||||
void app.shutdown()
|
||||
})
|
||||
}
|
||||
|
||||
export { server }
|
||||
|
||||
115
apps/bot/src/lambda-adapter.test.ts
Normal file
115
apps/bot/src/lambda-adapter.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
handleLambdaFunctionUrlEvent,
|
||||
requestFromLambdaEvent,
|
||||
responseToLambdaResult,
|
||||
type LambdaFunctionUrlRequest
|
||||
} from './lambda-adapter'
|
||||
|
||||
function baseEvent(overrides: Partial<LambdaFunctionUrlRequest> = {}): LambdaFunctionUrlRequest {
|
||||
return {
|
||||
version: '2.0',
|
||||
rawPath: '/healthz',
|
||||
rawQueryString: '',
|
||||
headers: {
|
||||
host: 'api.example.com',
|
||||
'x-forwarded-proto': 'https'
|
||||
},
|
||||
requestContext: {
|
||||
http: {
|
||||
method: 'GET'
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('lambda adapter', () => {
|
||||
test('translates a function url event into a request', async () => {
|
||||
const request = requestFromLambdaEvent(
|
||||
baseEvent({
|
||||
rawPath: '/webhook/telegram',
|
||||
rawQueryString: 'foo=bar',
|
||||
headers: {
|
||||
host: 'api.example.com',
|
||||
'x-forwarded-proto': 'https',
|
||||
'x-telegram-bot-api-secret-token': 'secret-token'
|
||||
},
|
||||
requestContext: {
|
||||
http: {
|
||||
method: 'POST'
|
||||
}
|
||||
},
|
||||
body: JSON.stringify({ update_id: 1 })
|
||||
})
|
||||
)
|
||||
|
||||
expect(request.method).toBe('POST')
|
||||
expect(request.url).toBe('https://api.example.com/webhook/telegram?foo=bar')
|
||||
expect(request.headers.get('x-telegram-bot-api-secret-token')).toBe('secret-token')
|
||||
expect(await request.json()).toEqual({ update_id: 1 })
|
||||
})
|
||||
|
||||
test('translates a response into a lambda result', async () => {
|
||||
const response = await responseToLambdaResult(
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 201,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'cache-control': 'no-store'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(response).toEqual({
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
'cache-control': 'no-store',
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ok: true })
|
||||
})
|
||||
})
|
||||
|
||||
test('preserves health endpoint behavior through the adapter', async () => {
|
||||
const response = await handleLambdaFunctionUrlEvent(
|
||||
baseEvent(),
|
||||
async () =>
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(response.headers).toEqual({
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
})
|
||||
expect(response.body).toBe(JSON.stringify({ ok: true }))
|
||||
})
|
||||
|
||||
test('decodes base64 request bodies', async () => {
|
||||
const event = baseEvent({
|
||||
rawPath: '/api/miniapp/session',
|
||||
requestContext: {
|
||||
http: {
|
||||
method: 'POST'
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
host: 'api.example.com',
|
||||
'x-forwarded-proto': 'https',
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: btoa(JSON.stringify({ hello: 'world' })),
|
||||
isBase64Encoded: true
|
||||
})
|
||||
|
||||
const request = requestFromLambdaEvent(event)
|
||||
|
||||
expect(await request.json()).toEqual({ hello: 'world' })
|
||||
})
|
||||
})
|
||||
110
apps/bot/src/lambda-adapter.ts
Normal file
110
apps/bot/src/lambda-adapter.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
export interface LambdaFunctionUrlRequest {
|
||||
version: '2.0'
|
||||
rawPath: string
|
||||
rawQueryString?: string
|
||||
headers?: Record<string, string | undefined>
|
||||
cookies?: string[]
|
||||
body?: string
|
||||
isBase64Encoded?: boolean
|
||||
requestContext: {
|
||||
domainName?: string
|
||||
http: {
|
||||
method: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface LambdaFunctionUrlResponse {
|
||||
statusCode: number
|
||||
headers?: Record<string, string>
|
||||
cookies?: string[]
|
||||
body: string
|
||||
isBase64Encoded?: boolean
|
||||
}
|
||||
|
||||
function normalizeHeaders(
|
||||
headers: Record<string, string | undefined> | undefined
|
||||
): Record<string, string> {
|
||||
const normalized: Record<string, string> = {}
|
||||
|
||||
if (!headers) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized[key] = value
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function requestUrl(event: LambdaFunctionUrlRequest): string {
|
||||
const headers = normalizeHeaders(event.headers)
|
||||
const host = headers.host || event.requestContext.domainName || 'lambda-url.local'
|
||||
const protocol = headers['x-forwarded-proto'] || 'https'
|
||||
const query = event.rawQueryString?.length ? `?${event.rawQueryString}` : ''
|
||||
|
||||
return `${protocol}://${host}${event.rawPath}${query}`
|
||||
}
|
||||
|
||||
function requestBody(event: LambdaFunctionUrlRequest): string | Uint8Array | null {
|
||||
if (event.body === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (event.isBase64Encoded) {
|
||||
return Uint8Array.from(atob(event.body), (char) => char.charCodeAt(0))
|
||||
}
|
||||
|
||||
return event.body
|
||||
}
|
||||
|
||||
export function requestFromLambdaEvent(event: LambdaFunctionUrlRequest): Request {
|
||||
const headers = new Headers(normalizeHeaders(event.headers))
|
||||
|
||||
if (event.cookies?.length) {
|
||||
headers.set('cookie', event.cookies.join('; '))
|
||||
}
|
||||
|
||||
return new Request(requestUrl(event), {
|
||||
method: event.requestContext.http.method,
|
||||
headers,
|
||||
body: requestBody(event)
|
||||
})
|
||||
}
|
||||
|
||||
export async function responseToLambdaResult(
|
||||
response: Response
|
||||
): Promise<LambdaFunctionUrlResponse> {
|
||||
const headers: Record<string, string> = {}
|
||||
const cookies: string[] = []
|
||||
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
if (key.toLowerCase() === 'set-cookie') {
|
||||
cookies.push(value)
|
||||
continue
|
||||
}
|
||||
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: response.status,
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||
...(cookies.length > 0 ? { cookies } : {}),
|
||||
body: await response.text()
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLambdaFunctionUrlEvent(
|
||||
event: LambdaFunctionUrlRequest,
|
||||
handler: (request: Request) => Promise<Response>
|
||||
): Promise<LambdaFunctionUrlResponse> {
|
||||
const request = requestFromLambdaEvent(event)
|
||||
const response = await handler(request)
|
||||
return responseToLambdaResult(response)
|
||||
}
|
||||
90
apps/bot/src/lambda.ts
Normal file
90
apps/bot/src/lambda.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getLogger } from '@household/observability'
|
||||
|
||||
import { createBotRuntimeApp } from './app'
|
||||
import {
|
||||
handleLambdaFunctionUrlEvent,
|
||||
type LambdaFunctionUrlRequest,
|
||||
type LambdaFunctionUrlResponse
|
||||
} from './lambda-adapter'
|
||||
|
||||
const appPromise = createBotRuntimeApp()
|
||||
const logger = getLogger('lambda')
|
||||
|
||||
export async function handler(event: LambdaFunctionUrlRequest): Promise<LambdaFunctionUrlResponse> {
|
||||
const app = await appPromise
|
||||
return handleLambdaFunctionUrlEvent(event, app.fetch)
|
||||
}
|
||||
|
||||
async function postRuntimeResponse(
|
||||
requestId: string,
|
||||
response: LambdaFunctionUrlResponse
|
||||
): Promise<void> {
|
||||
const runtimeApi = process.env.AWS_LAMBDA_RUNTIME_API
|
||||
if (!runtimeApi) {
|
||||
throw new Error('AWS_LAMBDA_RUNTIME_API environment variable is required')
|
||||
}
|
||||
|
||||
await fetch(`http://${runtimeApi}/2018-06-01/runtime/invocation/${requestId}/response`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
}
|
||||
|
||||
async function postRuntimeError(requestId: string, error: unknown): Promise<void> {
|
||||
const runtimeApi = process.env.AWS_LAMBDA_RUNTIME_API
|
||||
if (!runtimeApi) {
|
||||
throw new Error('AWS_LAMBDA_RUNTIME_API environment variable is required')
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : 'Unknown Lambda runtime error'
|
||||
|
||||
await fetch(`http://${runtimeApi}/2018-06-01/runtime/invocation/${requestId}/error`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
errorMessage: message,
|
||||
errorType: error instanceof Error ? error.name : 'Error'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function runtimeLoop(): Promise<void> {
|
||||
const runtimeApi = process.env.AWS_LAMBDA_RUNTIME_API
|
||||
if (!runtimeApi) {
|
||||
throw new Error('AWS_LAMBDA_RUNTIME_API environment variable is required')
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
event: 'runtime.started',
|
||||
mode: 'lambda'
|
||||
},
|
||||
'Bot Lambda runtime started'
|
||||
)
|
||||
|
||||
while (true) {
|
||||
const invocation = await fetch(`http://${runtimeApi}/2018-06-01/runtime/invocation/next`)
|
||||
const requestId = invocation.headers.get('lambda-runtime-aws-request-id')
|
||||
|
||||
if (!requestId) {
|
||||
throw new Error('Lambda runtime response did not include a request id')
|
||||
}
|
||||
|
||||
try {
|
||||
const event = (await invocation.json()) as LambdaFunctionUrlRequest
|
||||
const response = await handler(event)
|
||||
await postRuntimeResponse(requestId, response)
|
||||
} catch (error) {
|
||||
await postRuntimeError(requestId, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
void runtimeLoop()
|
||||
}
|
||||
Reference in New Issue
Block a user