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 {
|
import {
|
||||||
formatPrice,
|
formatPrice,
|
||||||
formatTime,
|
formatTime,
|
||||||
getBeloQuoteStatus,
|
getQuoteStatus,
|
||||||
isChatValidated,
|
isChatValidated,
|
||||||
validateCode,
|
validateCode,
|
||||||
} from "../modules/telegram/telegram.service";
|
} from "../modules/telegram/telegram.service";
|
||||||
@@ -10,6 +13,12 @@ import { logger } from "./logger";
|
|||||||
|
|
||||||
const token = process.env.BOT_TOKEN;
|
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 botInstance: Bot | null = null;
|
||||||
let botUsername: string | null = null;
|
let botUsername: string | null = null;
|
||||||
|
|
||||||
@@ -21,39 +30,11 @@ export function getBotUsername(): string | null {
|
|||||||
return botUsername;
|
return botUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startBot(): Promise<void> {
|
function buildQuoteMessage(type: QuoteType, status: QuoteStatus): string {
|
||||||
if (!token) {
|
const { label, icon } = QUOTE_COMMANDS[type];
|
||||||
logger.warn("BOT_TOKEN not set, Telegram bot not started");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await getBeloQuoteStatus();
|
|
||||||
if (!status) {
|
|
||||||
await ctx.reply("❌ No hay datos de BELO disponibles todavía.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = Math.round(status.progressPercentage ?? 0);
|
const progress = Math.round(status.progressPercentage ?? 0);
|
||||||
const message = [
|
return [
|
||||||
"💰 <b>BELO · USDC/ARS</b>",
|
`${icon} <b>${label}</b>`,
|
||||||
"",
|
"",
|
||||||
`Compra: <b>$${formatPrice(status.buy)}</b>`,
|
`Compra: <b>$${formatPrice(status.buy)}</b>`,
|
||||||
`Venta: <b>$${formatPrice(status.sell)}</b>`,
|
`Venta: <b>$${formatPrice(status.sell)}</b>`,
|
||||||
@@ -64,8 +45,81 @@ export async function startBot(): Promise<void> {
|
|||||||
"",
|
"",
|
||||||
`🕐 ${formatTime(status.timeStamp)}`,
|
`🕐 ${formatTime(status.timeStamp)}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.reply(message, { parse_mode: "HTML" });
|
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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bot = new Bot(token);
|
||||||
|
|
||||||
|
bot.command("start", async (ctx) => {
|
||||||
|
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("menu", async (ctx) => {
|
||||||
|
await ctx.reply("📊 <b>Menú</b>\n\nElegí una cotización:", {
|
||||||
|
parse_mode: "HTML",
|
||||||
|
reply_markup: mainMenu(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.command("belo", (ctx) => replyQuote(ctx, "BELO"));
|
||||||
|
bot.command("blu", (ctx) => replyQuote(ctx, "BLUE"));
|
||||||
|
bot.command("oficial", (ctx) => replyQuote(ctx, "BNA"));
|
||||||
|
|
||||||
|
bot.on("callback_query:data", async (ctx) => {
|
||||||
|
const [cmd, rawType] = ctx.callbackQuery.data.split(":");
|
||||||
|
if (cmd !== "quote") return;
|
||||||
|
|
||||||
|
const type = rawType as QuoteType;
|
||||||
|
if (!QUOTE_COMMANDS[type]) return;
|
||||||
|
|
||||||
|
await replyQuote(ctx, type);
|
||||||
|
await ctx.answerCallbackQuery();
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.on("message:text", async (ctx) => {
|
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);
|
const success = await validateCode(BigInt(ctx.chat.id), text);
|
||||||
if (success) {
|
if (success) {
|
||||||
await ctx.reply("✅ ¡Conectado! Recibirás notificaciones de BELO aquí.");
|
await ctx.reply(
|
||||||
|
"✅ ¡Conectado! Recibirás notificaciones de BELO aquí.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await ctx.reply("❌ Código inválido o ya utilizado.");
|
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();
|
const botInfo = await bot.api.getMe();
|
||||||
botUsername = botInfo.username;
|
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();
|
bot.start();
|
||||||
botInstance = bot;
|
botInstance = bot;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import type { QuoteType } from "../../generated/prisma/client";
|
||||||
import { logger } from "../../lib/logger";
|
import { logger } from "../../lib/logger";
|
||||||
import { prisma } from "../../lib/prisma";
|
import { prisma } from "../../lib/prisma";
|
||||||
import { getBot, getBotUsername } from "../../lib/telegram";
|
import { getBot, getBotUsername } from "../../lib/telegram";
|
||||||
@@ -209,7 +210,7 @@ export async function checkAndNotifyBelo(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BeloQuoteStatus {
|
export interface QuoteStatus {
|
||||||
buy: number;
|
buy: number;
|
||||||
sell: number;
|
sell: number;
|
||||||
timeStamp: Date;
|
timeStamp: Date;
|
||||||
@@ -220,9 +221,11 @@ export interface BeloQuoteStatus {
|
|||||||
progressPercentage: number | null;
|
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({
|
const currentQuote = await prisma.quote.findFirst({
|
||||||
where: { type: "BELO" },
|
where: { type },
|
||||||
orderBy: { timeStamp: "desc" },
|
orderBy: { timeStamp: "desc" },
|
||||||
});
|
});
|
||||||
if (!currentQuote) return null;
|
if (!currentQuote) return null;
|
||||||
@@ -241,7 +244,7 @@ export async function getBeloQuoteStatus(): Promise<BeloQuoteStatus | null> {
|
|||||||
|
|
||||||
const agg = await prisma.quoteHistory.aggregate({
|
const agg = await prisma.quoteHistory.aggregate({
|
||||||
where: {
|
where: {
|
||||||
type: "BELO",
|
type,
|
||||||
timeStamp: { gte: todayStart },
|
timeStamp: { gte: todayStart },
|
||||||
},
|
},
|
||||||
_min: { buy: true, sell: true },
|
_min: { buy: true, sell: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user