feat(telegram): add /blu and /oficial commands and main menu
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { Bot } from "grammy";
|
||||
import type { Context } from "grammy";
|
||||
import { Bot, InlineKeyboard } from "grammy";
|
||||
import type { QuoteType } from "../generated/prisma/client";
|
||||
import type { QuoteStatus } from "../modules/telegram/telegram.service";
|
||||
import {
|
||||
formatPrice,
|
||||
formatTime,
|
||||
getBeloQuoteStatus,
|
||||
getQuoteStatus,
|
||||
isChatValidated,
|
||||
validateCode,
|
||||
} from "../modules/telegram/telegram.service";
|
||||
@@ -10,6 +13,12 @@ import { logger } from "./logger";
|
||||
|
||||
const token = process.env.BOT_TOKEN;
|
||||
|
||||
const QUOTE_COMMANDS = {
|
||||
BELO: { label: "BELO · USDC/ARS", icon: "💰" },
|
||||
BLUE: { label: "Dólar Blue", icon: "💵" },
|
||||
BNA: { label: "Dólar Oficial", icon: "🏦" },
|
||||
} as const satisfies Record<QuoteType, { label: string; icon: string }>;
|
||||
|
||||
let botInstance: Bot | null = null;
|
||||
let botUsername: string | null = null;
|
||||
|
||||
@@ -21,6 +30,54 @@ export function getBotUsername(): string | null {
|
||||
return botUsername;
|
||||
}
|
||||
|
||||
function buildQuoteMessage(type: QuoteType, status: QuoteStatus): string {
|
||||
const { label, icon } = QUOTE_COMMANDS[type];
|
||||
const progress = Math.round(status.progressPercentage ?? 0);
|
||||
return [
|
||||
`${icon} <b>${label}</b>`,
|
||||
"",
|
||||
`Compra: <b>$${formatPrice(status.buy)}</b>`,
|
||||
`Venta: <b>$${formatPrice(status.sell)}</b>`,
|
||||
"",
|
||||
`Mínimo del día: $${formatPrice(status.minBuy)}`,
|
||||
`Máximo del día: $${formatPrice(status.maxBuy)}`,
|
||||
`Avance del día: <b>${progress}%</b>`,
|
||||
"",
|
||||
`🕐 ${formatTime(status.timeStamp)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function replyQuote(ctx: Context, type: QuoteType): Promise<void> {
|
||||
const chatId = ctx.chat?.id;
|
||||
if (!chatId) return;
|
||||
|
||||
const validated = await isChatValidated(BigInt(chatId));
|
||||
if (!validated) {
|
||||
await ctx.reply(
|
||||
"🔒 Tu cuenta no está conectada. Enviá /menu para ver cómo conectarla.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await getQuoteStatus(type);
|
||||
if (!status) {
|
||||
await ctx.reply(
|
||||
`❌ No hay datos de ${QUOTE_COMMANDS[type].label} disponibles todavía.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.reply(buildQuoteMessage(type, status), { parse_mode: "HTML" });
|
||||
}
|
||||
|
||||
function mainMenu(): InlineKeyboard {
|
||||
return new InlineKeyboard()
|
||||
.text(`${QUOTE_COMMANDS.BELO.icon} BELO`, "quote:BELO")
|
||||
.text(`${QUOTE_COMMANDS.BLUE.icon} Blue`, "quote:BLUE")
|
||||
.row()
|
||||
.text(`${QUOTE_COMMANDS.BNA.icon} Oficial`, "quote:BNA");
|
||||
}
|
||||
|
||||
export async function startBot(): Promise<void> {
|
||||
if (!token) {
|
||||
logger.warn("BOT_TOKEN not set, Telegram bot not started");
|
||||
@@ -31,41 +88,38 @@ export async function startBot(): Promise<void> {
|
||||
const bot = new Bot(token);
|
||||
|
||||
bot.command("start", async (ctx) => {
|
||||
await ctx.reply(
|
||||
"🔗 Enviá el código de verificación que aparece en la Configuración de la app para conectar tu cuenta de Telegram.\n\nUna vez conectado, recibirás notificaciones cuando BELO alcance el máximo del día.",
|
||||
);
|
||||
const validated = await isChatValidated(BigInt(ctx.chat.id));
|
||||
if (validated) {
|
||||
await ctx.reply("👋 ¡Hola! Seleccioná una cotización:", {
|
||||
reply_markup: mainMenu(),
|
||||
});
|
||||
} else {
|
||||
await ctx.reply(
|
||||
"🔗 Enviá el código de verificación que aparece en la Configuración de la app para conectar tu cuenta de Telegram.\n\nUna vez conectado, recibirás notificaciones cuando BELO alcance el máximo del día y podrás consultar las cotizaciones con /menu.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("belo", async (ctx) => {
|
||||
const validated = await isChatValidated(BigInt(ctx.chat.id));
|
||||
if (!validated) {
|
||||
await ctx.reply(
|
||||
"🔒 Tu cuenta no está conectada. Enviá /start para ver cómo conectarla.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
bot.command("menu", async (ctx) => {
|
||||
await ctx.reply("📊 <b>Menú</b>\n\nElegí una cotización:", {
|
||||
parse_mode: "HTML",
|
||||
reply_markup: mainMenu(),
|
||||
});
|
||||
});
|
||||
|
||||
const status = await getBeloQuoteStatus();
|
||||
if (!status) {
|
||||
await ctx.reply("❌ No hay datos de BELO disponibles todavía.");
|
||||
return;
|
||||
}
|
||||
bot.command("belo", (ctx) => replyQuote(ctx, "BELO"));
|
||||
bot.command("blu", (ctx) => replyQuote(ctx, "BLUE"));
|
||||
bot.command("oficial", (ctx) => replyQuote(ctx, "BNA"));
|
||||
|
||||
const progress = Math.round(status.progressPercentage ?? 0);
|
||||
const message = [
|
||||
"💰 <b>BELO · USDC/ARS</b>",
|
||||
"",
|
||||
`Compra: <b>$${formatPrice(status.buy)}</b>`,
|
||||
`Venta: <b>$${formatPrice(status.sell)}</b>`,
|
||||
"",
|
||||
`Mínimo del día: $${formatPrice(status.minBuy)}`,
|
||||
`Máximo del día: $${formatPrice(status.maxBuy)}`,
|
||||
`Avance del día: <b>${progress}%</b>`,
|
||||
"",
|
||||
`🕐 ${formatTime(status.timeStamp)}`,
|
||||
].join("\n");
|
||||
bot.on("callback_query:data", async (ctx) => {
|
||||
const [cmd, rawType] = ctx.callbackQuery.data.split(":");
|
||||
if (cmd !== "quote") return;
|
||||
|
||||
await ctx.reply(message, { parse_mode: "HTML" });
|
||||
const type = rawType as QuoteType;
|
||||
if (!QUOTE_COMMANDS[type]) return;
|
||||
|
||||
await replyQuote(ctx, type);
|
||||
await ctx.answerCallbackQuery();
|
||||
});
|
||||
|
||||
bot.on("message:text", async (ctx) => {
|
||||
@@ -74,7 +128,9 @@ export async function startBot(): Promise<void> {
|
||||
|
||||
const success = await validateCode(BigInt(ctx.chat.id), text);
|
||||
if (success) {
|
||||
await ctx.reply("✅ ¡Conectado! Recibirás notificaciones de BELO aquí.");
|
||||
await ctx.reply(
|
||||
"✅ ¡Conectado! Recibirás notificaciones de BELO aquí.",
|
||||
);
|
||||
} else {
|
||||
await ctx.reply("❌ Código inválido o ya utilizado.");
|
||||
}
|
||||
@@ -83,6 +139,13 @@ export async function startBot(): Promise<void> {
|
||||
const botInfo = await bot.api.getMe();
|
||||
botUsername = botInfo.username;
|
||||
|
||||
await bot.api.setMyCommands([
|
||||
{ command: "menu", description: "Mostrar menú principal" },
|
||||
{ command: "belo", description: "Cotización de BELO (USDC/ARS)" },
|
||||
{ command: "blu", description: "Cotización del dólar blue" },
|
||||
{ command: "oficial", description: "Cotización del dólar oficial" },
|
||||
]);
|
||||
|
||||
bot.start();
|
||||
botInstance = bot;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import crypto from "crypto";
|
||||
import type { QuoteType } from "../../generated/prisma/client";
|
||||
import { logger } from "../../lib/logger";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
import { getBot, getBotUsername } from "../../lib/telegram";
|
||||
@@ -209,7 +210,7 @@ export async function checkAndNotifyBelo(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export interface BeloQuoteStatus {
|
||||
export interface QuoteStatus {
|
||||
buy: number;
|
||||
sell: number;
|
||||
timeStamp: Date;
|
||||
@@ -220,9 +221,11 @@ export interface BeloQuoteStatus {
|
||||
progressPercentage: number | null;
|
||||
}
|
||||
|
||||
export async function getBeloQuoteStatus(): Promise<BeloQuoteStatus | null> {
|
||||
export async function getQuoteStatus(
|
||||
type: QuoteType,
|
||||
): Promise<QuoteStatus | null> {
|
||||
const currentQuote = await prisma.quote.findFirst({
|
||||
where: { type: "BELO" },
|
||||
where: { type },
|
||||
orderBy: { timeStamp: "desc" },
|
||||
});
|
||||
if (!currentQuote) return null;
|
||||
@@ -241,7 +244,7 @@ export async function getBeloQuoteStatus(): Promise<BeloQuoteStatus | null> {
|
||||
|
||||
const agg = await prisma.quoteHistory.aggregate({
|
||||
where: {
|
||||
type: "BELO",
|
||||
type,
|
||||
timeStamp: { gte: todayStart },
|
||||
},
|
||||
_min: { buy: true, sell: true },
|
||||
|
||||
Reference in New Issue
Block a user