- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback - /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate) - /api/pledges: GET handler with filtering, search, pagination, sort by dueDate - Dashboard overview: stats, collection progress bar, needs attention, upcoming payments - Dashboard pledges: proper table with status tabs, search, actions, pagination - New shadcn components: Table, Tabs, DropdownMenu, Progress - Setup wizard: 4-step onboarding (org → bank → event → QR code) - Settings API: PUT handler for org create/update - Org resolver: single-tenant fallback to first org - Cron jobs installed: reminders every 15min, overdue check at 6am - Auto-generates installment dates when not provided - HOSTNAME=0.0.0.0 in compose for multi-network binding
143 lines
3.6 KiB
TypeScript
143 lines
3.6 KiB
TypeScript
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
|
|
const DEFAULT_CHAT_ID = process.env.TELEGRAM_CHAT_ID!;
|
|
const ALLOWED_IDS = new Set(
|
|
(process.env.TELEGRAM_ALLOWED_CHAT_IDS || DEFAULT_CHAT_ID)
|
|
.split(",")
|
|
.map((id) => id.trim())
|
|
);
|
|
|
|
const api = async (method: string, body?: Record<string, unknown>) => {
|
|
const res = await fetch(
|
|
`https://api.telegram.org/bot${BOT_TOKEN}/${method}`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
}
|
|
);
|
|
return res.json();
|
|
};
|
|
|
|
export function isAllowed(chatId: number | string): boolean {
|
|
return ALLOWED_IDS.has(String(chatId));
|
|
}
|
|
|
|
export async function send(
|
|
text: string,
|
|
chatId: string | number = DEFAULT_CHAT_ID,
|
|
opts?: { reply_to?: number; keyboard?: InlineKeyboard }
|
|
): Promise<any> {
|
|
if (!isAllowed(chatId)) return null;
|
|
|
|
// Telegram limits messages to 4096 chars
|
|
const truncated =
|
|
text.length > 4000 ? text.slice(0, 4000) + "\n\n... (truncated)" : text;
|
|
|
|
const body: Record<string, unknown> = {
|
|
chat_id: chatId,
|
|
text: truncated,
|
|
parse_mode: "Markdown",
|
|
};
|
|
if (opts?.reply_to) body.reply_to_message_id = opts.reply_to;
|
|
if (opts?.keyboard) {
|
|
body.reply_markup = { inline_keyboard: opts.keyboard };
|
|
}
|
|
return api("sendMessage", body);
|
|
}
|
|
|
|
export async function editMessage(
|
|
chatId: string | number,
|
|
messageId: number,
|
|
text: string,
|
|
keyboard?: InlineKeyboard
|
|
): Promise<any> {
|
|
const truncated =
|
|
text.length > 4000 ? text.slice(0, 4000) + "\n\n... (truncated)" : text;
|
|
const body: Record<string, unknown> = {
|
|
chat_id: chatId,
|
|
message_id: messageId,
|
|
text: truncated,
|
|
parse_mode: "Markdown",
|
|
};
|
|
if (keyboard) body.reply_markup = { inline_keyboard: keyboard };
|
|
return api("editMessageText", body);
|
|
}
|
|
|
|
export async function answerCallback(
|
|
callbackId: string,
|
|
text?: string
|
|
): Promise<any> {
|
|
return api("answerCallbackQuery", {
|
|
callback_query_id: callbackId,
|
|
text,
|
|
});
|
|
}
|
|
|
|
export type InlineKeyboard = Array<
|
|
Array<{ text: string; callback_data: string }>
|
|
>;
|
|
|
|
export function agentKeyboard(agentId: number): InlineKeyboard {
|
|
return [
|
|
[
|
|
{ text: "📋 Logs", callback_data: `logs_${agentId}` },
|
|
{ text: "💬 Talk", callback_data: `talk_${agentId}` },
|
|
{ text: "🛑 Kill", callback_data: `kill_${agentId}` },
|
|
],
|
|
];
|
|
}
|
|
|
|
let offset = 0;
|
|
|
|
export interface TelegramUpdate {
|
|
update_id: number;
|
|
message?: {
|
|
message_id: number;
|
|
from?: { id: number; first_name: string };
|
|
chat: { id: number; type: string };
|
|
text?: string;
|
|
reply_to_message?: { message_id: number };
|
|
};
|
|
callback_query?: {
|
|
id: string;
|
|
from: { id: number };
|
|
message?: { message_id: number; chat: { id: number } };
|
|
data?: string;
|
|
};
|
|
}
|
|
|
|
export async function poll(): Promise<TelegramUpdate[]> {
|
|
try {
|
|
const data = await api("getUpdates", {
|
|
offset,
|
|
timeout: 30,
|
|
allowed_updates: ["message", "callback_query"],
|
|
});
|
|
const updates: TelegramUpdate[] = data.result || [];
|
|
if (updates.length > 0) {
|
|
offset = updates[updates.length - 1].update_id + 1;
|
|
}
|
|
return updates;
|
|
} catch (e) {
|
|
console.error("[telegram] poll error:", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function deleteMessage(
|
|
chatId: string | number,
|
|
messageId: number
|
|
): Promise<boolean> {
|
|
try {
|
|
const res = await api("deleteMessage", {
|
|
chat_id: chatId,
|
|
message_id: messageId,
|
|
});
|
|
return !!res?.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export default { send, editMessage, deleteMessage, poll, isAllowed, agentKeyboard, answerCallback };
|