feat(telegram): integrate Telegram bot for notifications and add related models
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import cron from "node-cron";
|
||||
import { eventBus } from "../../lib/event-bus";
|
||||
import { logger } from "../../lib/logger";
|
||||
import { checkAndNotifyBeloMax } from "../telegram/telegram.service";
|
||||
import { processBeloQuote } from "./belo.service";
|
||||
import { processBlueQuote } from "./blue.service";
|
||||
import { processBnaQuote } from "./bna.service";
|
||||
@@ -12,6 +13,9 @@ async function safeProcess(
|
||||
try {
|
||||
await fn();
|
||||
logger.info(`${name} quote processed successfully`);
|
||||
if (name === "BELO") {
|
||||
await checkAndNotifyBeloMax();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, `Error processing ${name} quote`);
|
||||
}
|
||||
|
||||
20
apps/backend/src/modules/telegram/telegram.handler.ts
Normal file
20
apps/backend/src/modules/telegram/telegram.handler.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Context } from "hono";
|
||||
import { generateCode, getTelegramStatus } from "./telegram.service";
|
||||
|
||||
export async function generateCodeHandler(c: Context) {
|
||||
try {
|
||||
const result = await generateCode();
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
return c.json({ error: "Failed to generate code" }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function telegramStatusHandler(c: Context) {
|
||||
try {
|
||||
const status = await getTelegramStatus();
|
||||
return c.json(status);
|
||||
} catch (error) {
|
||||
return c.json({ connected: false, botUsername: null }, 500);
|
||||
}
|
||||
}
|
||||
9
apps/backend/src/modules/telegram/telegram.router.ts
Normal file
9
apps/backend/src/modules/telegram/telegram.router.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Hono } from "hono";
|
||||
import { generateCodeHandler, telegramStatusHandler } from "./telegram.handler";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.post("/code", generateCodeHandler);
|
||||
app.get("/status", telegramStatusHandler);
|
||||
|
||||
export default app;
|
||||
151
apps/backend/src/modules/telegram/telegram.service.ts
Normal file
151
apps/backend/src/modules/telegram/telegram.service.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import crypto from "crypto";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
import { logger } from "../../lib/logger";
|
||||
import { getBot, getBotUsername } from "../../lib/telegram";
|
||||
|
||||
function generateRandomCode(): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const bytes = crypto.randomBytes(8);
|
||||
let code = "";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
code += chars[bytes[i] % chars.length];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function formatPrice(price: number): string {
|
||||
return price.toLocaleString("es-AR", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateCode(): Promise<{
|
||||
code: string;
|
||||
botUsername: string | null;
|
||||
}> {
|
||||
const code = generateRandomCode();
|
||||
|
||||
await prisma.telegramChat.create({
|
||||
data: { code },
|
||||
});
|
||||
|
||||
return { code, botUsername: getBotUsername() };
|
||||
}
|
||||
|
||||
export async function validateCode(
|
||||
chatId: bigint,
|
||||
code: string,
|
||||
): Promise<boolean> {
|
||||
const record = await prisma.telegramChat.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (!record || record.validatedAt !== null) return false;
|
||||
|
||||
await prisma.telegramChat.deleteMany({
|
||||
where: { chatId, validatedAt: { not: null } },
|
||||
});
|
||||
|
||||
await prisma.telegramChat.update({
|
||||
where: { id: record.id },
|
||||
data: {
|
||||
chatId,
|
||||
validatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getTelegramStatus(): Promise<{
|
||||
botUsername: string | null;
|
||||
connected: boolean;
|
||||
}> {
|
||||
const validated = await prisma.telegramChat.findFirst({
|
||||
where: { validatedAt: { not: null } },
|
||||
});
|
||||
|
||||
return {
|
||||
botUsername: getBotUsername(),
|
||||
connected: validated !== null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkAndNotifyBeloMax(): Promise<void> {
|
||||
const chat = await prisma.telegramChat.findFirst({
|
||||
where: { validatedAt: { not: null } },
|
||||
orderBy: { validatedAt: "desc" },
|
||||
});
|
||||
|
||||
if (!chat?.chatId) return;
|
||||
|
||||
const bot = getBot();
|
||||
if (!bot) return;
|
||||
|
||||
const now = new Date();
|
||||
const artHour = ((now.getUTCHours() - 3) % 24 + 24) % 24;
|
||||
if (artHour < 8 || artHour >= 20) return;
|
||||
|
||||
const currentQuote = await prisma.quote.findFirst({
|
||||
where: { type: "BELO" },
|
||||
orderBy: { timeStamp: "desc" },
|
||||
});
|
||||
if (!currentQuote) return;
|
||||
|
||||
const currentBuy = Number(currentQuote.buy);
|
||||
|
||||
const artOffset = -3;
|
||||
const artMs = now.getTime() + artOffset * 3600000;
|
||||
const artDate = new Date(artMs);
|
||||
const todayStart = new Date(
|
||||
Date.UTC(
|
||||
artDate.getUTCFullYear(),
|
||||
artDate.getUTCMonth(),
|
||||
artDate.getUTCDate(),
|
||||
),
|
||||
);
|
||||
|
||||
const lastNotified = await prisma.telegramDailyMaxNotification.findUnique({
|
||||
where: {
|
||||
date_quoteType: {
|
||||
date: todayStart,
|
||||
quoteType: "BELO",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const lastNotifiedValue = lastNotified ? Number(lastNotified.maxValue) : 0;
|
||||
|
||||
if (currentBuy <= lastNotifiedValue) return;
|
||||
|
||||
const formattedPrice = formatPrice(currentBuy);
|
||||
const message = `BELO alcanzó el máximo del día, la cotización actual es de $${formattedPrice}`;
|
||||
|
||||
try {
|
||||
await bot.api.sendMessage(Number(chat.chatId), message);
|
||||
|
||||
await prisma.telegramDailyMaxNotification.upsert({
|
||||
where: {
|
||||
date_quoteType: {
|
||||
date: todayStart,
|
||||
quoteType: "BELO",
|
||||
},
|
||||
},
|
||||
create: {
|
||||
date: todayStart,
|
||||
quoteType: "BELO",
|
||||
maxValue: currentBuy,
|
||||
},
|
||||
update: {
|
||||
maxValue: currentBuy,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Telegram BELO max notification sent: $${formattedPrice}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to send Telegram BELO max notification");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user