feat(bot): add guided private prompts

This commit is contained in:
2026-03-09 05:15:29 +04:00
parent fac2dc0e9d
commit 4e200b506a
11 changed files with 785 additions and 66 deletions

View File

@@ -2,3 +2,4 @@ export { createDbAnonymousFeedbackRepository } from './anonymous-feedback-reposi
export { createDbFinanceRepository } from './finance-repository'
export { createDbHouseholdConfigurationRepository } from './household-config-repository'
export { createDbReminderDispatchRepository } from './reminder-dispatch-repository'
export { createDbTelegramPendingActionRepository } from './telegram-pending-action-repository'

View File

@@ -0,0 +1,144 @@
import { and, eq } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import type {
TelegramPendingActionRecord,
TelegramPendingActionRepository,
TelegramPendingActionType
} from '@household/ports'
function parsePendingActionType(raw: string): TelegramPendingActionType {
if (raw === 'anonymous_feedback') {
return raw
}
throw new Error(`Unexpected telegram pending action type: ${raw}`)
}
function mapPendingAction(row: {
telegramUserId: string
telegramChatId: string
action: string
payload: unknown
expiresAt: Date | null
}): TelegramPendingActionRecord {
return {
telegramUserId: row.telegramUserId,
telegramChatId: row.telegramChatId,
action: parsePendingActionType(row.action),
payload:
row.payload && typeof row.payload === 'object' && !Array.isArray(row.payload)
? (row.payload as Record<string, unknown>)
: {},
expiresAt: row.expiresAt
}
}
export function createDbTelegramPendingActionRepository(databaseUrl: string): {
repository: TelegramPendingActionRepository
close: () => Promise<void>
} {
const { db, queryClient } = createDbClient(databaseUrl, {
max: 5,
prepare: false
})
const repository: TelegramPendingActionRepository = {
async upsertPendingAction(input) {
const rows = await db
.insert(schema.telegramPendingActions)
.values({
telegramUserId: input.telegramUserId,
telegramChatId: input.telegramChatId,
action: input.action,
payload: input.payload,
expiresAt: input.expiresAt,
updatedAt: new Date()
})
.onConflictDoUpdate({
target: [
schema.telegramPendingActions.telegramChatId,
schema.telegramPendingActions.telegramUserId
],
set: {
action: input.action,
payload: input.payload,
expiresAt: input.expiresAt,
updatedAt: new Date()
}
})
.returning({
telegramUserId: schema.telegramPendingActions.telegramUserId,
telegramChatId: schema.telegramPendingActions.telegramChatId,
action: schema.telegramPendingActions.action,
payload: schema.telegramPendingActions.payload,
expiresAt: schema.telegramPendingActions.expiresAt
})
const row = rows[0]
if (!row) {
throw new Error('Pending action upsert did not return a row')
}
return mapPendingAction(row)
},
async getPendingAction(telegramChatId, telegramUserId) {
const now = new Date()
const rows = await db
.select({
telegramUserId: schema.telegramPendingActions.telegramUserId,
telegramChatId: schema.telegramPendingActions.telegramChatId,
action: schema.telegramPendingActions.action,
payload: schema.telegramPendingActions.payload,
expiresAt: schema.telegramPendingActions.expiresAt
})
.from(schema.telegramPendingActions)
.where(
and(
eq(schema.telegramPendingActions.telegramChatId, telegramChatId),
eq(schema.telegramPendingActions.telegramUserId, telegramUserId)
)
)
.limit(1)
const row = rows[0]
if (!row) {
return null
}
if (row.expiresAt && row.expiresAt.getTime() <= now.getTime()) {
await db
.delete(schema.telegramPendingActions)
.where(
and(
eq(schema.telegramPendingActions.telegramChatId, telegramChatId),
eq(schema.telegramPendingActions.telegramUserId, telegramUserId)
)
)
return null
}
return mapPendingAction(row)
},
async clearPendingAction(telegramChatId, telegramUserId) {
await db
.delete(schema.telegramPendingActions)
.where(
and(
eq(schema.telegramPendingActions.telegramChatId, telegramChatId),
eq(schema.telegramPendingActions.telegramUserId, telegramUserId)
)
)
}
}
return {
repository,
close: async () => {
await queryClient.end({ timeout: 5 })
}
}
}

View File

@@ -0,0 +1,13 @@
CREATE TABLE "telegram_pending_actions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"telegram_user_id" text NOT NULL,
"telegram_chat_id" text NOT NULL,
"action" text NOT NULL,
"payload" jsonb DEFAULT '{}'::jsonb NOT NULL,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "telegram_pending_actions_chat_user_unique" ON "telegram_pending_actions" USING btree ("telegram_chat_id","telegram_user_id");--> statement-breakpoint
CREATE INDEX "telegram_pending_actions_user_action_idx" ON "telegram_pending_actions" USING btree ("telegram_user_id","action");

View File

@@ -50,6 +50,13 @@
"when": 1773015092441,
"tag": "0006_marvelous_nehzno",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1773051000000,
"tag": "0007_sudden_murmur",
"breakpoints": true
}
]
}

View File

@@ -107,6 +107,32 @@ export const householdPendingMembers = pgTable(
})
)
export const telegramPendingActions = pgTable(
'telegram_pending_actions',
{
id: uuid('id').defaultRandom().primaryKey(),
telegramUserId: text('telegram_user_id').notNull(),
telegramChatId: text('telegram_chat_id').notNull(),
action: text('action').notNull(),
payload: jsonb('payload')
.default(sql`'{}'::jsonb`)
.notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
chatUserUnique: uniqueIndex('telegram_pending_actions_chat_user_unique').on(
table.telegramChatId,
table.telegramUserId
),
userActionIdx: index('telegram_pending_actions_user_action_idx').on(
table.telegramUserId,
table.action
)
})
)
export const members = pgTable(
'members',
{

View File

@@ -35,3 +35,9 @@ export type {
SettlementSnapshotLineRecord,
SettlementSnapshotRecord
} from './finance'
export {
TELEGRAM_PENDING_ACTION_TYPES,
type TelegramPendingActionRecord,
type TelegramPendingActionRepository,
type TelegramPendingActionType
} from './telegram-pending-actions'

View File

@@ -0,0 +1,20 @@
export const TELEGRAM_PENDING_ACTION_TYPES = ['anonymous_feedback'] as const
export type TelegramPendingActionType = (typeof TELEGRAM_PENDING_ACTION_TYPES)[number]
export interface TelegramPendingActionRecord {
telegramUserId: string
telegramChatId: string
action: TelegramPendingActionType
payload: Record<string, unknown>
expiresAt: Date | null
}
export interface TelegramPendingActionRepository {
upsertPendingAction(input: TelegramPendingActionRecord): Promise<TelegramPendingActionRecord>
getPendingAction(
telegramChatId: string,
telegramUserId: string
): Promise<TelegramPendingActionRecord | null>
clearPendingAction(telegramChatId: string, telegramUserId: string): Promise<void>
}