UNPKG

telegram-badge

Version:

Generate Telegram group member count badges for GitHub README

255 lines (254 loc) 10.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = handler; const crypto = __importStar(require("crypto")); const badge_generator_1 = require("./badge-generator"); const logger = { info: (message, data = {}) => { console.log(`[INFO] ${message}`, data); }, warn: (message, data = {}) => { console.warn(`[WARN] ${message}`, data); }, error: (message, error = null) => { console.error(`[ERROR] ${message}`, error); }, debug: (message, data = {}) => { if (process.env.DEBUG) { console.log(`[DEBUG] ${message}`, data); } } }; const validateEnvironment = (query) => { const token = process.env.BOT_TOKEN; let channelId = process.env.CHAT_ID; // Check if channelId is provided via URL parameter if (query?.channelId) { channelId = Array.isArray(query.channelId) ? query.channelId[0] : query.channelId; } if (!token) { throw new Error("Missing BOT_TOKEN environment variable"); } if (!channelId) { throw new Error("Missing CHAT_ID environment variable or channelId parameter"); } return { token, channelId }; }; const getMemberCount = async (token, channelId) => { const apiUrl = `https://api.telegram.org/bot${token}/getChatMemberCount?chat_id=${encodeURIComponent(channelId)}`; logger.debug('Fetching member count', { channelId }); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { const response = await fetch(apiUrl, { signal: controller.signal, headers: { 'Accept': 'application/json', 'User-Agent': 'TelegramBadgeGenerator/1.0' } }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); } const data = await response.json(); if (!data.ok) { throw new Error(`Telegram API error: ${data.description}`); } logger.debug('Member count received', { count: data.result }); return data.result; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { logger.error('Request timeout', error); throw new Error('Request timeout: Telegram API took too long to respond'); } logger.error('Error fetching member count', error); throw error; } }; const validateStyleOptions = (options) => { const validStyles = ['flat', 'plastic', 'flat-square', 'for-the-badge', 'social']; let style = options.style || 'flat'; if (!validStyles.includes(style)) { logger.warn(`Invalid style: ${style}, using default 'flat'`); style = 'flat'; } const label = options.label || 'Telegram'; const color = options.color || '2AABEE'; const labelColor = options.labelColor || '555555'; const logo = options.logo !== false; // Logo by default return { style, label, color, labelColor, logo }; }; const createBadge = (members, options) => { const { style, label, color, labelColor, logo } = validateStyleOptions(options); logger.debug('Creating badge', { style, label, color, labelColor, logo }); const normalizedColor = color.replace(/^#/, ''); const normalizedLabelColor = labelColor.replace(/^#/, ''); const format = { label, message: `${members} members`, color: `#${normalizedColor}`, labelColor: `#${normalizedLabelColor}`, style, logo }; return (0, badge_generator_1.generateBadgeSVG)(format); }; const createErrorBadge = (errorMessage) => { const format = { label: 'Error', message: errorMessage, color: '#e05d44', labelColor: '#555555', style: 'flat', logo: false }; return (0, badge_generator_1.generateBadgeSVG)(format); }; const setCacheHeaders = (res, svg) => { res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "max-age=300, s-maxage=600, stale-while-revalidate=86400"); const etag = crypto .createHash('md5') .update(svg) .digest('hex'); res.setHeader("ETag", `"${etag}"`); const expiresDate = new Date(); expiresDate.setSeconds(expiresDate.getSeconds() + 300); res.setHeader("Expires", expiresDate.toUTCString()); logger.debug('Cache headers set'); }; async function handler(req, res) { // Global error handler process.on('uncaughtException', (error) => { console.error('[UNCAUGHT EXCEPTION]', error); if (!res.headersSent) { const errorBadge = createErrorBadge('Uncaught Error'); res.status(500).send(errorBadge); } }); process.on('unhandledRejection', (reason) => { console.error('[UNHANDLED REJECTION]', reason); if (!res.headersSent) { const errorBadge = createErrorBadge('Unhandled Rejection'); res.status(500).send(errorBadge); } }); try { logger.info('Function started', { query: req.query, userAgent: req.headers['user-agent'], referer: req.headers['referer'] || 'unknown', env: { hasToken: !!process.env.BOT_TOKEN, hasChatId: !!process.env.CHAT_ID } }); // Early check for environment variables const chatIdFromQuery = req.query?.channelId; if (!process.env.BOT_TOKEN || (!process.env.CHAT_ID && !chatIdFromQuery)) { logger.error('Missing environment variables', { BOT_TOKEN: !!process.env.BOT_TOKEN, CHAT_ID: !!process.env.CHAT_ID, chatIdFromQuery: !!chatIdFromQuery }); const errorBadge = createErrorBadge('Missing Config'); res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); res.status(500).send(errorBadge); return; } const { token, channelId } = validateEnvironment(req.query); logger.debug('Environment validated', { channelId }); const ifNoneMatch = req.headers['if-none-match']; const requestEtag = `"${crypto .createHash('md5') .update(JSON.stringify({ token, channelId, query: req.query, time: Math.floor(Date.now() / 300000) })) .digest('hex')}"`; if (ifNoneMatch && ifNoneMatch === requestEtag) { logger.info('Returning 304 Not Modified'); res.status(304).end(); return; } const members = await getMemberCount(token, channelId); logger.info('Member count fetched', { members }); const badgeOptions = { style: req.query.style, label: Array.isArray(req.query.label) ? req.query.label[0] : req.query.label, color: Array.isArray(req.query.color) ? req.query.color[0] : req.query.color, labelColor: Array.isArray(req.query.labelColor) ? req.query.labelColor[0] : req.query.labelColor, logo: req.query.logo !== 'false' // Logo enabled by default, only disabled with logo=false }; const svg = createBadge(members, badgeOptions); logger.debug('Badge created'); setCacheHeaders(res, svg); res.status(200).send(svg); logger.info('Badge sent successfully'); } catch (err) { logger.error('Error processing request', err); let errorBadge; let statusCode = 500; if (err instanceof Error) { if (err.message.includes("Missing BOT_TOKEN") || err.message.includes("Missing CHAT_ID")) { errorBadge = createErrorBadge('Configuration Error'); logger.error(`Configuration error: ${err.message}`); } else if (err.message.includes("Telegram API error")) { errorBadge = createErrorBadge('API Error'); logger.error(`Telegram API error: ${err.message}`); } else if (err.message.includes("Request timeout")) { errorBadge = createErrorBadge('Timeout'); statusCode = 503; logger.error(`Timeout error: ${err.message}`); } else { errorBadge = createErrorBadge('Server Error'); logger.error(`Server error: ${err.message}`); } } else { errorBadge = createErrorBadge('Server Error'); logger.error('Unknown error occurred'); } res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); res.status(statusCode).send(errorBadge); logger.info(`Error badge sent with status ${statusCode}`); } }