/opt/canhelp/apps/api/src/lib
NameSizeModeActions
activity.ts13240644editdlrm
apple-client-secret.ts40730644editdlrm
balance.ts35060644editdlrm
daily-limits.ts42820644editdlrm
email.ts527080644editdlrm
fomo.ts113660644editdlrm
google-oauth.ts105260644editdlrm
helper-capability.ts42070644editdlrm
messaging-rules.ts31230644editdlrm
nameUtils.ts7090644editdlrm
notif.ts189290644editdlrm
plan-history.ts10200644editdlrm
push.ts63860644editdlrm
referral.ts61100644editdlrm
telegram.ts62070644editdlrm
turnstile.ts7410644editdlrm
Edit: /opt/canhelp/apps/api/src/lib/telegram.ts (6207B)
import { db } from '../db.js' import { settings as settingsTable } from '@canhelp/db' import { inArray } from 'drizzle-orm' // ─── Config cache (TTL = 60s) ───────────────────────────────────────────── export const LOCALES = ['el', 'en', 'ru', 'uk'] as const export type Locale = typeof LOCALES[number] type TelegramConfig = { botToken: string adminChatId: string channels: Record // per-language channel IDs notifyRegistrations: boolean postNewTasks: boolean } let _cache: TelegramConfig | null = null let _cacheAt = 0 const CACHE_TTL = 60_000 async function loadConfig(): Promise { if (Date.now() - _cacheAt < CACHE_TTL && _cache) return _cache const rows = await db .select() .from(settingsTable) .where( inArray(settingsTable.key, [ 'telegram.botToken', 'telegram.adminChatId', 'telegram.channelId.el', 'telegram.channelId.en', 'telegram.channelId.ru', 'telegram.channelId.uk', 'telegram.notifyRegistrations', 'telegram.postNewTasks', ]), ) .catch(() => []) const kv: Record = {} for (const r of rows) kv[r.key] = r.value _cache = { botToken: kv['telegram.botToken'] ?? '', adminChatId: kv['telegram.adminChatId'] ?? '', channels: { el: kv['telegram.channelId.el'] ?? '', en: kv['telegram.channelId.en'] ?? '', ru: kv['telegram.channelId.ru'] ?? '', uk: kv['telegram.channelId.uk'] ?? '', }, notifyRegistrations: kv['telegram.notifyRegistrations'] === 'true', postNewTasks: kv['telegram.postNewTasks'] === 'true', } _cacheAt = Date.now() return _cache } export function invalidateTelegramCache() { _cacheAt = 0 _cache = null } // ─── Core send ──────────────────────────────────────────────────────────── async function sendMessage(botToken: string, chatId: string, text: string): Promise { if (!botToken || !chatId) return const url = `https://api.telegram.org/bot${botToken}/sendMessage` const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'HTML' }), }) if (!res.ok) { const body = await res.text().catch(() => '') throw new Error(`Telegram API error ${res.status}: ${body}`) } } // ─── Public helpers ──────────────────────────────────────────────────────── /** Send a notification to the admin chat (e.g. new registration) */ export async function notifyAdmin(text: string): Promise { const cfg = await loadConfig() if (!cfg.botToken || !cfg.adminChatId) return await sendMessage(cfg.botToken, cfg.adminChatId, text) } /** Returns true if registration notifications are enabled */ export async function isRegistrationNotifyEnabled(): Promise { const cfg = await loadConfig() return !!(cfg.botToken && cfg.adminChatId && cfg.notifyRegistrations) } /** Returns true if new-task channel posts are enabled (at least one channel configured) */ export async function isNewTaskPostEnabled(): Promise { const cfg = await loadConfig() return !!(cfg.botToken && cfg.postNewTasks && LOCALES.some((l) => cfg.channels[l])) } type TaskPayload = { id: string title: string titleEl?: string | null titleEn?: string | null titleRu?: string | null titleUk?: string | null description: string descriptionEl?: string | null descriptionEn?: string | null descriptionRu?: string | null descriptionUk?: string | null budget?: string | null location?: string | null } /** Post a new task to all configured language channels */ export async function postTaskToChannels(task: TaskPayload, siteUrl: string): Promise { const cfg = await loadConfig() if (!cfg.botToken || !cfg.postNewTasks) return const localeTitle: Record = { el: task.titleEl || task.title, en: task.titleEn || task.title, ru: task.titleRu || task.title, uk: task.titleUk || task.title, } const localeDesc: Record = { el: task.descriptionEl || task.description, en: task.descriptionEn || task.description, ru: task.descriptionRu || task.description, uk: task.descriptionUk || task.description, } const link = `${siteUrl}/tasks/${task.id}` await Promise.allSettled( LOCALES.filter((l) => cfg.channels[l]).map((l) => { const title = localeTitle[l] const desc = localeDesc[l] const budget = task.budget ? `💰 ${task.budget} €` : '' const location = task.location ? `📍 ${task.location}` : '' const lines = [ `📋 ${title}`, desc ? desc.slice(0, 200) + (desc.length > 200 ? '…' : '') : '', [budget, location].filter(Boolean).join(' '), `🔗 ${link}`, ].filter(Boolean) return sendMessage(cfg.botToken, cfg.channels[l], lines.join('\n')) }), ) } /** * Test connection using the provided credentials (not the cached ones). * Sends a test message to the given chatId. */ export async function testTelegramConnection(botToken: string, chatId: string): Promise<{ ok: true; botName: string } | { ok: false; error: string }> { try { const meRes = await fetch(`https://api.telegram.org/bot${botToken}/getMe`) if (!meRes.ok) { const body = await meRes.text().catch(() => '') return { ok: false, error: `Invalid bot token: ${body}` } } const me = (await meRes.json()) as { result?: { username?: string; first_name?: string } } const botName = me.result?.username || me.result?.first_name || 'Bot' await sendMessage(botToken, chatId, `✅ CanHelp connected!\nBot: @${botName}`) return { ok: true, botName } } catch (err) { return { ok: false, error: String(err) } } }