production: reminder cron, dashboard overhaul, shadcn components, setup wizard
- /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
This commit is contained in:
142
lib/telegram.ts
Normal file
142
lib/telegram.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user