170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
/**
|
||
* Telegram Notification Service
|
||
*
|
||
* Sends alerts to admin via Telegram Bot when:
|
||
* - New TRC20 purchase is confirmed
|
||
* - Purchase is marked as distributed
|
||
*
|
||
* Configuration (via environment variables or admin settings table):
|
||
* TELEGRAM_BOT_TOKEN — Bot token from @BotFather (e.g. 123456:ABC-DEF...)
|
||
* TELEGRAM_CHAT_ID — Chat ID to send messages to (personal or group)
|
||
*
|
||
* How to set up:
|
||
* 1. Open Telegram, search @BotFather, send /newbot
|
||
* 2. Follow prompts to get your Bot Token
|
||
* 3. Start a chat with your bot (or add it to a group)
|
||
* 4. Get Chat ID: https://api.telegram.org/bot<TOKEN>/getUpdates
|
||
* 5. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in environment variables
|
||
*/
|
||
|
||
const TELEGRAM_API = "https://api.telegram.org";
|
||
|
||
export interface TelegramConfig {
|
||
botToken: string;
|
||
chatId: string;
|
||
}
|
||
|
||
/**
|
||
* Get Telegram config from environment variables or DB settings.
|
||
* Returns null if not configured.
|
||
*/
|
||
export async function getTelegramConfig(): Promise<TelegramConfig | null> {
|
||
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||
|
||
if (!botToken || !chatId) {
|
||
return null;
|
||
}
|
||
|
||
return { botToken, chatId };
|
||
}
|
||
|
||
/**
|
||
* Send a message via Telegram Bot API.
|
||
* Uses HTML parse mode for formatting.
|
||
*/
|
||
export async function sendTelegramMessage(
|
||
config: TelegramConfig,
|
||
message: string
|
||
): Promise<boolean> {
|
||
try {
|
||
const url = `${TELEGRAM_API}/bot${config.botToken}/sendMessage`;
|
||
const resp = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
chat_id: config.chatId,
|
||
text: message,
|
||
parse_mode: "HTML",
|
||
disable_web_page_preview: true,
|
||
}),
|
||
signal: AbortSignal.timeout(10_000),
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.text();
|
||
console.warn(`[Telegram] Send failed (${resp.status}): ${err}`);
|
||
return false;
|
||
}
|
||
|
||
console.log("[Telegram] Message sent successfully");
|
||
return true;
|
||
} catch (e) {
|
||
console.warn("[Telegram] Send error:", e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Notify admin about a new TRC20 purchase.
|
||
*/
|
||
export async function notifyNewTRC20Purchase(purchase: {
|
||
txHash: string;
|
||
fromAddress: string;
|
||
usdtAmount: number;
|
||
xicAmount: number;
|
||
evmAddress: string | null;
|
||
}): Promise<void> {
|
||
const config = await getTelegramConfig();
|
||
if (!config) {
|
||
console.log("[Telegram] Not configured — skipping notification (set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID)");
|
||
return;
|
||
}
|
||
|
||
const evmLine = purchase.evmAddress
|
||
? `\n🔑 <b>EVM Address:</b> <code>${purchase.evmAddress}</code>`
|
||
: "\n⚠️ <b>EVM Address:</b> Not provided (manual distribution required)";
|
||
|
||
const txUrl = `https://tronscan.org/#/transaction/${purchase.txHash}`;
|
||
|
||
const message = [
|
||
"🟢 <b>New XIC Presale Purchase!</b>",
|
||
"",
|
||
`💰 <b>Amount:</b> $${purchase.usdtAmount.toFixed(2)} USDT`,
|
||
`🪙 <b>XIC Tokens:</b> ${purchase.xicAmount.toLocaleString()} XIC`,
|
||
`📍 <b>From (TRON):</b> <code>${purchase.fromAddress}</code>`,
|
||
evmLine,
|
||
`🔗 <b>TX:</b> <a href="${txUrl}">${purchase.txHash.slice(0, 16)}...</a>`,
|
||
"",
|
||
`⏰ ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} (CST)`,
|
||
].join("\n");
|
||
|
||
await sendTelegramMessage(config, message);
|
||
}
|
||
|
||
/**
|
||
* Notify admin when a purchase is marked as distributed.
|
||
*/
|
||
export async function notifyDistributed(purchase: {
|
||
txHash: string;
|
||
evmAddress: string;
|
||
xicAmount: number;
|
||
distributeTxHash?: string | null;
|
||
}): Promise<void> {
|
||
const config = await getTelegramConfig();
|
||
if (!config) return;
|
||
|
||
const distTxLine = purchase.distributeTxHash
|
||
? `\n🔗 <b>Dist TX:</b> <code>${purchase.distributeTxHash}</code>`
|
||
: "";
|
||
|
||
const message = [
|
||
"✅ <b>XIC Tokens Distributed!</b>",
|
||
"",
|
||
`🪙 <b>XIC Tokens:</b> ${purchase.xicAmount.toLocaleString()} XIC`,
|
||
`📬 <b>To EVM:</b> <code>${purchase.evmAddress}</code>`,
|
||
distTxLine,
|
||
`🔗 <b>Original TX:</b> <code>${purchase.txHash.slice(0, 16)}...</code>`,
|
||
"",
|
||
`⏰ ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} (CST)`,
|
||
].join("\n");
|
||
|
||
await sendTelegramMessage(config, message);
|
||
}
|
||
|
||
/**
|
||
* Test the Telegram connection with a test message.
|
||
* Returns { success, error } for admin UI feedback.
|
||
*/
|
||
export async function testTelegramConnection(
|
||
botToken: string,
|
||
chatId: string
|
||
): Promise<{ success: boolean; error?: string }> {
|
||
try {
|
||
const config: TelegramConfig = { botToken, chatId };
|
||
const message = [
|
||
"🔔 <b>NAC Presale — Telegram Notification Test</b>",
|
||
"",
|
||
"✅ Connection successful! You will receive purchase alerts here.",
|
||
"",
|
||
`⏰ ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} (CST)`,
|
||
].join("\n");
|
||
|
||
const ok = await sendTelegramMessage(config, message);
|
||
if (!ok) return { success: false, error: "Failed to send message. Check Bot Token and Chat ID." };
|
||
return { success: true };
|
||
} catch (e) {
|
||
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||
}
|
||
}
|