feat: add Telegram album purchase bot

This commit is contained in:
2026-08-21 18:07:50 +03:30
parent 9478aa319f
commit 1e7b752532
21 changed files with 1399 additions and 459 deletions
+20 -12
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '@/lib/db';
import { purchaseDb } from '@/lib/db';
import { notifyCustomerOfDecision } from '@/lib/telegram';
export async function PATCH(
request: NextRequest,
@@ -16,25 +17,32 @@ export async function PATCH(
);
}
const db = getDatabase();
// Update purchase status to approved
const result = db.prepare(`
UPDATE purchases
SET approvalStatus = 'approved'
WHERE id = ?
`).run(purchaseId);
if (result.changes === 0) {
const existing = purchaseDb.getById(purchaseId);
if (!existing) {
return NextResponse.json(
{ error: 'Purchase not found' },
{ status: 404 }
);
}
if (existing.approvalStatus !== 'pending') {
return NextResponse.json(
{ error: `Purchase is already ${existing.approvalStatus}` },
{ status: 409 }
);
}
const purchase = purchaseDb.setStatus(purchaseId, 'approved');
if (!purchase) {
return NextResponse.json({ error: 'Purchase was reviewed concurrently' }, { status: 409 });
}
void notifyCustomerOfDecision(purchase).catch((error) => {
console.error('Failed to notify Telegram customer:', error);
});
return NextResponse.json({
success: true,
message: 'Purchase approved successfully'
message: 'Purchase approved successfully',
purchase,
});
} catch (error: any) {
console.error('Error approving purchase:', error);
+20 -12
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '@/lib/db';
import { purchaseDb } from '@/lib/db';
import { notifyCustomerOfDecision } from '@/lib/telegram';
export async function PATCH(
request: NextRequest,
@@ -16,25 +17,32 @@ export async function PATCH(
);
}
const db = getDatabase();
// Update purchase status to rejected
const result = db.prepare(`
UPDATE purchases
SET approvalStatus = 'rejected'
WHERE id = ?
`).run(purchaseId);
if (result.changes === 0) {
const existing = purchaseDb.getById(purchaseId);
if (!existing) {
return NextResponse.json(
{ error: 'Purchase not found' },
{ status: 404 }
);
}
if (existing.approvalStatus !== 'pending') {
return NextResponse.json(
{ error: `Purchase is already ${existing.approvalStatus}` },
{ status: 409 }
);
}
const purchase = purchaseDb.setStatus(purchaseId, 'rejected');
if (!purchase) {
return NextResponse.json({ error: 'Purchase was reviewed concurrently' }, { status: 409 });
}
void notifyCustomerOfDecision(purchase).catch((error) => {
console.error('Failed to notify Telegram customer:', error);
});
return NextResponse.json({
success: true,
message: 'Purchase rejected successfully'
message: 'Purchase rejected successfully',
purchase,
});
} catch (error: any) {
console.error('Error rejecting purchase:', error);
+5
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { purchaseDb } from '@/lib/db';
import { Purchase } from '@/lib/types';
import { notifyPurchaseApprovers } from '@/lib/telegram';
// GET - Get all purchases
export async function GET(request: NextRequest) {
@@ -54,6 +55,10 @@ export async function POST(request: NextRequest) {
};
const created = purchaseDb.create(purchase);
// Telegram delivery must not make a successfully recorded purchase fail.
void notifyPurchaseApprovers(created).catch((error) => {
console.error('Failed to notify Telegram approvers:', error);
});
return NextResponse.json(created, { status: 201 });
} catch (error) {
console.error('Error creating purchase:', error);
+99
View File
@@ -0,0 +1,99 @@
import { NextRequest, NextResponse } from "next/server";
import { telegramApproverDb, telegramSettingsDb } from "@/lib/db";
import { isTelegramConfigured, registerTelegramWebhook } from "@/lib/telegram";
import { TelegramApprover } from "@/lib/types";
export const dynamic = "force-dynamic";
export async function GET() {
const settings = telegramSettingsDb.get();
return NextResponse.json({
approvers: telegramApproverDb.getAll(),
baseUrl: settings.baseUrl,
greetingText: settings.greetingText,
cardNumber: settings.cardNumber,
cardHolderName: settings.cardHolderName,
botConfigured: isTelegramConfigured(),
webhookSecretConfigured: Boolean(process.env.TELEGRAM_WEBHOOK_SECRET),
});
}
export async function PUT(request: NextRequest) {
try {
const body = (await request.json()) as {
approvers?: TelegramApprover[];
baseUrl?: string;
botToken?: string;
greetingText?: string;
cardNumber?: string;
cardHolderName?: string;
};
if (!Array.isArray(body.approvers)) {
return NextResponse.json({ error: "approvers must be an array" }, { status: 400 });
}
const seen = new Set<string>();
const approvers: TelegramApprover[] = [];
for (const item of body.approvers) {
const userId = String(item.userId || "").trim();
if (!/^\d+$/.test(userId)) {
return NextResponse.json({ error: `Invalid Telegram user ID: ${userId}` }, { status: 400 });
}
if (!seen.has(userId)) {
seen.add(userId);
approvers.push({ userId, label: String(item.label || "").trim().slice(0, 100) });
}
}
const baseUrl = String(body.baseUrl || "https://api.telegram.org").trim().replace(/\/$/, "");
let parsedUrl: URL;
try {
parsedUrl = new URL(baseUrl);
} catch {
return NextResponse.json({ error: "Invalid Telegram API base URL" }, { status: 400 });
}
if (!["http:", "https:"].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password) {
return NextResponse.json({ error: "Telegram API base URL must use HTTP or HTTPS without credentials" }, { status: 400 });
}
const botToken = body.botToken?.trim();
const greetingText = String(body.greetingText || "").trim().slice(0, 1000);
const cardNumber = String(body.cardNumber || "").trim().slice(0, 100);
const cardHolderName = String(body.cardHolderName || "").trim().slice(0, 200);
if (!greetingText) {
return NextResponse.json({ error: "Greeting message is required" }, { status: 400 });
}
telegramApproverDb.replaceAll(approvers);
telegramSettingsDb.update({
baseUrl,
greetingText,
cardNumber,
cardHolderName,
...(botToken ? { botToken } : {}),
});
return NextResponse.json({
approvers: telegramApproverDb.getAll(),
baseUrl: telegramSettingsDb.get().baseUrl,
greetingText,
cardNumber,
cardHolderName,
botConfigured: isTelegramConfigured(),
webhookSecretConfigured: Boolean(process.env.TELEGRAM_WEBHOOK_SECRET),
});
} catch (error) {
console.error("Telegram settings update failed:", error);
return NextResponse.json({ error: "Failed to update Telegram settings" }, { status: 500 });
}
}
export async function POST() {
try {
await registerTelegramWebhook();
return NextResponse.json({ message: "Telegram webhook registered successfully" });
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to register Telegram webhook";
console.error("Telegram webhook registration failed:", error);
return NextResponse.json({ error: message }, { status: 502 });
}
}
+32
View File
@@ -0,0 +1,32 @@
import { timingSafeEqual } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { handleTelegramUpdate, TelegramUpdate } from "@/lib/telegram";
export const dynamic = "force-dynamic";
function secretMatches(provided: string, expected: string): boolean {
const left = Buffer.from(provided);
const right = Buffer.from(expected);
return left.length === right.length && timingSafeEqual(left, right);
}
export async function POST(request: NextRequest) {
const expectedSecret = process.env.TELEGRAM_WEBHOOK_SECRET;
if (!expectedSecret) {
return NextResponse.json({ error: "Telegram webhook is not configured" }, { status: 503 });
}
const providedSecret = request.headers.get("x-telegram-bot-api-secret-token") || "";
if (!secretMatches(providedSecret, expectedSecret)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const update = (await request.json()) as TelegramUpdate;
await handleTelegramUpdate(update);
return NextResponse.json({ ok: true });
} catch (error) {
console.error("Telegram webhook error:", error);
return NextResponse.json({ error: "Failed to process Telegram update" }, { status: 500 });
}
}