telegram-badge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alexander Kireyev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # πŸ›‘οΈ Telegram Group Badge Generator
2
+
3
+ [![Build Status](https://github.com/chatman-media/telegram-badge/workflows/CI/badge.svg)](https://github.com/chatman-media/telegram-badge/actions)
4
+ [![npm version](https://badge.fury.io/js/telegram-badge.svg)](https://badge.fury.io/js/telegram-badge)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.5-blue.svg)](https://www.typescriptlang.org/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Π­Ρ‚ΠΎΡ‚ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ SVG-Π±Π΅ΠΉΠ΄ΠΆ с Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΌ количСством участников вашСй Telegram-Π³Ρ€ΡƒΠΏΠΏΡ‹. ИдСально ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ΠΈΡ‚ для отобраТСния активности сообщСства Π² README Π½Π° GitHub ΠΈΠ»ΠΈ Π½Π° сайтС.
9
+
10
+ ## πŸš€ Π”Π΅ΠΌΠΎ
11
+
12
+ ![Telegram Group Members](https://telegram-badge.vercel.app/api/telegram-badge)
13
+
14
+ ---
15
+
16
+ ## πŸ“¦ Π‘Ρ‚Π΅ΠΊ
17
+
18
+ - Node.js / Bun
19
+ - Telegram Bot API
20
+ - Vercel (Serverless API)
21
+
22
+ ---
23
+
24
+ ## πŸ›  Установка
25
+
26
+ 1. ΠšΠ»ΠΎΠ½ΠΈΡ€ΡƒΠΉΡ‚Π΅ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ:
27
+
28
+ ```bash
29
+ git clone https://github.com/chatman-media/telegram-badge.git
30
+ cd telegram-badge
31
+ ```
32
+
33
+ 2. УстановитС зависимости:
34
+
35
+ ```bash
36
+ npm install
37
+ # ΠΈΠ»ΠΈ
38
+ bun install
39
+ ```
40
+
41
+ 3. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ .env Ρ„Π°ΠΉΠ» ΠΈ Π΄ΠΎΠ±Π°Π²ΡŒΡ‚Π΅:
42
+
43
+ ```bash
44
+ BOT_TOKEN=your_telegram_bot_token
45
+ CHAT_ID=@your_group_username_or_chat_id
46
+ ```
47
+
48
+ Π£Π±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ Π±ΠΎΡ‚ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ Π² Π³Ρ€ΡƒΠΏΠΏΡƒ ΠΊΠ°ΠΊ Π°Π΄ΠΌΠΈΠ½.
49
+
50
+ ## πŸ§ͺ Π›ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ запуск
51
+
52
+ ```bash
53
+ npm run dev
54
+ # ΠΈΠ»ΠΈ
55
+ bun dev
56
+ ```
57
+
58
+ ΠžΡ‚ΠΊΡ€ΠΎΠΉ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅: http://localhost:3000/api/telegram-badge
59
+
60
+ ## ☁️ Π”Π΅ΠΏΠ»ΠΎΠΉ Π½Π° Vercel
61
+ 1. Π—Π°Π΄Π΅ΠΏΠ»ΠΎΠΉΡ‚Π΅ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ Π½Π° vercel.com
62
+ 2. Π’ настройках ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° Π΄ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния:
63
+ β€’ BOT_TOKEN
64
+ β€’ CHAT_ID
65
+
66
+ ## 🧩 ИспользованиС в GitHub README
67
+
68
+ Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ строку Π² ваш README.md:
69
+
70
+ ```markdown
71
+ ![Telegram Group Badge](https://telegram-badge.vercel.app/api/telegram-badge)
72
+ ```
73
+
74
+ ### 🎨 ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ стилизации
75
+
76
+ Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π½Π°ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ внСшний Π²ΠΈΠ΄ Π±Π΅ΠΉΠ΄ΠΆΠ° с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΡ… ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ²:
77
+
78
+ | ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ | ОписаниС | Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ |
79
+ |----------|----------|------------------------|
80
+ | `style` | Π‘Ρ‚ΠΈΠ»ΡŒ Π±Π΅ΠΉΠ΄ΠΆΠ° | `flat` |
81
+ | `label` | ВСкст ΠΌΠ΅Ρ‚ΠΊΠΈ | `Telegram` |
82
+ | `color` | Π¦Π²Π΅Ρ‚ основной части Π±Π΅ΠΉΠ΄ΠΆΠ° | `2AABEE` (Ρ†Π²Π΅Ρ‚ Telegram) |
83
+ | `labelColor` | Π¦Π²Π΅Ρ‚ ΠΌΠ΅Ρ‚ΠΊΠΈ Π±Π΅ΠΉΠ΄ΠΆΠ° | `555555` |
84
+
85
+ #### ДоступныС стили:
86
+
87
+ - `flat` - плоский ΡΡ‚ΠΈΠ»ΡŒ (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ)
88
+ - `plastic` - ΠΎΠ±ΡŠΠ΅ΠΌΠ½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ
89
+ - `flat-square` - плоский ΠΊΠ²Π°Π΄Ρ€Π°Ρ‚Π½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ
90
+ - `for-the-badge` - ΡˆΠΈΡ€ΠΎΠΊΠΈΠΉ ΡΡ‚ΠΈΠ»ΡŒ с Π·Π°Π³Π»Π°Π²Π½Ρ‹ΠΌΠΈ Π±ΡƒΠΊΠ²Π°ΠΌΠΈ
91
+ - `social` - ΡΠΎΡ†ΠΈΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ
92
+
93
+ #### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹:
94
+
95
+ Π‘Ρ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½Ρ‹ΠΉ Π±Π΅ΠΉΠ΄ΠΆ:
96
+ ```
97
+ https://telegram-badge.vercel.app/api/telegram-badge
98
+ ```
99
+
100
+ Π‘Π΅ΠΉΠ΄ΠΆ с кастомной ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ:
101
+ ```
102
+ https://telegram-badge.vercel.app/api/telegram-badge?label=Our%20Group
103
+ ```
104
+
105
+ Π‘Π΅ΠΉΠ΄ΠΆ с кастомным Ρ†Π²Π΅Ρ‚ΠΎΠΌ:
106
+ ```
107
+ https://telegram-badge.vercel.app/api/telegram-badge?color=FF0000
108
+ ```
109
+
110
+ Π‘Π΅ΠΉΠ΄ΠΆ с кастомным стилСм:
111
+ ```
112
+ https://telegram-badge.vercel.app/api/telegram-badge?style=for-the-badge
113
+ ```
114
+
115
+ ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ кастомизированный Π±Π΅ΠΉΠ΄ΠΆ:
116
+ ```
117
+ https://telegram-badge.vercel.app/api/telegram-badge?style=social&label=Join%20Us&color=FF5733&labelColor=333333
118
+ ```
119
+
120
+ ## 🧠 ВозмоТности
121
+
122
+ - πŸ‘₯ ΠžΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ количСства участников Π² Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ
123
+ - 🎨 Полная кастомизация внСшнСго Π²ΠΈΠ΄Π° Π±Π΅ΠΉΠ΄ΠΆΠ°
124
+ - πŸ”’ ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° .env ΠΈ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… Vercel для бСзопасного хранСния Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²
125
+ - ⚑ ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠ΅ ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ для быстрой Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ
126
+ - πŸ›‘οΈ ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок с ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΠ²Π½Ρ‹ΠΌΠΈ сообщСниями
127
+ - πŸ†“ БСсплатно Π½Π° Vercel ΠΏΡ€ΠΈ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎΠΉ Π½Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅
128
+ - πŸ“‘ МоТно Ρ€Π°ΡΡˆΠΈΡ€ΠΈΡ‚ΡŒ Π΄ΠΎ отобраТСния активности / количСства сообщСний
129
+
130
+ βΈ»
131
+
132
+ πŸ“œ ЛицСнзия
133
+
134
+ MIT
@@ -0,0 +1,2 @@
1
+ import { Request, Response } from '../types';
2
+ export default function handler(req: Request, res: Response): Promise<void>;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = handler;
37
+ const badge_maker_1 = require("badge-maker");
38
+ const crypto = __importStar(require("crypto"));
39
+ const logger = {
40
+ info: (message, data = {}) => {
41
+ console.log(`[INFO] ${message}`, data);
42
+ },
43
+ warn: (message, data = {}) => {
44
+ console.warn(`[WARN] ${message}`, data);
45
+ },
46
+ error: (message, error = null) => {
47
+ console.error(`[ERROR] ${message}`, error);
48
+ },
49
+ debug: (message, data = {}) => {
50
+ if (process.env.DEBUG) {
51
+ console.log(`[DEBUG] ${message}`, data);
52
+ }
53
+ }
54
+ };
55
+ const validateEnvironment = () => {
56
+ const token = process.env.BOT_TOKEN;
57
+ const chatId = process.env.CHAT_ID;
58
+ if (!token) {
59
+ throw new Error("Missing BOT_TOKEN environment variable");
60
+ }
61
+ if (!chatId) {
62
+ throw new Error("Missing CHAT_ID environment variable");
63
+ }
64
+ return { token, chatId };
65
+ };
66
+ const getMemberCount = async (token, chatId) => {
67
+ const apiUrl = `https://api.telegram.org/bot${token}/getChatMemberCount?chat_id=${encodeURIComponent(chatId)}`;
68
+ logger.debug('Fetching member count', { chatId });
69
+ const controller = new AbortController();
70
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
71
+ try {
72
+ const response = await fetch(apiUrl, {
73
+ signal: controller.signal,
74
+ headers: {
75
+ 'Accept': 'application/json',
76
+ 'User-Agent': 'TelegramBadgeGenerator/1.0'
77
+ }
78
+ });
79
+ clearTimeout(timeoutId);
80
+ if (!response.ok) {
81
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
82
+ }
83
+ const data = await response.json();
84
+ if (!data.ok) {
85
+ throw new Error(`Telegram API error: ${data.description}`);
86
+ }
87
+ logger.debug('Member count received', { count: data.result });
88
+ return data.result;
89
+ }
90
+ catch (error) {
91
+ if (error instanceof Error && error.name === 'AbortError') {
92
+ logger.error('Request timeout', error);
93
+ throw new Error('Request timeout: Telegram API took too long to respond');
94
+ }
95
+ logger.error('Error fetching member count', error);
96
+ throw error;
97
+ }
98
+ };
99
+ const validateStyleOptions = (options) => {
100
+ const validStyles = ['flat', 'plastic', 'flat-square', 'for-the-badge', 'social'];
101
+ let style = options.style || 'flat';
102
+ if (!validStyles.includes(style)) {
103
+ logger.warn(`Invalid style: ${style}, using default 'flat'`);
104
+ style = 'flat';
105
+ }
106
+ const label = options.label || 'Telegram';
107
+ const color = options.color || '2AABEE';
108
+ const labelColor = options.labelColor || '555555';
109
+ return { style, label, color, labelColor };
110
+ };
111
+ const createBadge = (members, options) => {
112
+ const { style, label, color, labelColor } = validateStyleOptions(options);
113
+ logger.debug('Creating badge', { style, label, color, labelColor });
114
+ const normalizedColor = color.replace(/^#/, '');
115
+ const normalizedLabelColor = labelColor.replace(/^#/, '');
116
+ const format = {
117
+ label,
118
+ message: `${members} members`,
119
+ color: `#${normalizedColor}`,
120
+ labelColor: `#${normalizedLabelColor}`,
121
+ style
122
+ };
123
+ return (0, badge_maker_1.makeBadge)(format);
124
+ };
125
+ const createErrorBadge = (errorMessage) => {
126
+ const format = {
127
+ label: 'Error',
128
+ message: errorMessage,
129
+ color: '#e05d44',
130
+ labelColor: '#555555',
131
+ style: 'flat'
132
+ };
133
+ return (0, badge_maker_1.makeBadge)(format);
134
+ };
135
+ const setCacheHeaders = (res, svg) => {
136
+ res.setHeader("Content-Type", "image/svg+xml");
137
+ res.setHeader("Cache-Control", "max-age=300, s-maxage=600, stale-while-revalidate=86400");
138
+ const etag = crypto
139
+ .createHash('md5')
140
+ .update(svg)
141
+ .digest('hex');
142
+ res.setHeader("ETag", `"${etag}"`);
143
+ const expiresDate = new Date();
144
+ expiresDate.setSeconds(expiresDate.getSeconds() + 300);
145
+ res.setHeader("Expires", expiresDate.toUTCString());
146
+ logger.debug('Cache headers set');
147
+ };
148
+ async function handler(req, res) {
149
+ logger.info('Received badge request', {
150
+ query: req.query,
151
+ userAgent: req.headers['user-agent'],
152
+ referer: req.headers['referer'] || 'unknown'
153
+ });
154
+ try {
155
+ const { token, chatId } = validateEnvironment();
156
+ logger.debug('Environment validated', { chatId });
157
+ const ifNoneMatch = req.headers['if-none-match'];
158
+ const requestEtag = `"${crypto
159
+ .createHash('md5')
160
+ .update(JSON.stringify({ token, chatId, query: req.query, time: Math.floor(Date.now() / 300000) }))
161
+ .digest('hex')}"`;
162
+ if (ifNoneMatch && ifNoneMatch === requestEtag) {
163
+ logger.info('Returning 304 Not Modified');
164
+ res.status(304).end();
165
+ return;
166
+ }
167
+ const members = await getMemberCount(token, chatId);
168
+ logger.info('Member count fetched', { members });
169
+ const badgeOptions = {
170
+ style: req.query.style,
171
+ label: req.query.label,
172
+ color: req.query.color,
173
+ labelColor: req.query.labelColor
174
+ };
175
+ const svg = createBadge(members, badgeOptions);
176
+ logger.debug('Badge created');
177
+ setCacheHeaders(res, svg);
178
+ res.status(200).send(svg);
179
+ logger.info('Badge sent successfully');
180
+ }
181
+ catch (err) {
182
+ logger.error('Error processing request', err);
183
+ let errorBadge;
184
+ let statusCode = 500;
185
+ if (err instanceof Error) {
186
+ if (err.message.includes("Missing BOT_TOKEN") || err.message.includes("Missing CHAT_ID")) {
187
+ errorBadge = createErrorBadge('Configuration Error');
188
+ logger.error(`Configuration error: ${err.message}`);
189
+ }
190
+ else if (err.message.includes("Telegram API error")) {
191
+ errorBadge = createErrorBadge('API Error');
192
+ logger.error(`Telegram API error: ${err.message}`);
193
+ }
194
+ else if (err.message.includes("Request timeout")) {
195
+ errorBadge = createErrorBadge('Timeout');
196
+ statusCode = 503;
197
+ logger.error(`Timeout error: ${err.message}`);
198
+ }
199
+ else {
200
+ errorBadge = createErrorBadge('Server Error');
201
+ logger.error(`Server error: ${err.message}`);
202
+ }
203
+ }
204
+ else {
205
+ errorBadge = createErrorBadge('Server Error');
206
+ logger.error('Unknown error occurred');
207
+ }
208
+ res.setHeader("Content-Type", "image/svg+xml");
209
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
210
+ res.status(statusCode).send(errorBadge);
211
+ logger.info(`Error badge sent with status ${statusCode}`);
212
+ }
213
+ }
File without changes
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ // Setup file for Jest tests
3
+ afterEach(() => {
4
+ jest.clearAllTimers();
5
+ jest.restoreAllMocks();
6
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const telegram_badge_1 = __importDefault(require("../api/telegram-badge"));
40
+ const crypto = __importStar(require("crypto"));
41
+ // Mock fetch globally
42
+ global.fetch = jest.fn();
43
+ // Mock console methods for clean test output
44
+ console.log = jest.fn();
45
+ console.error = jest.fn();
46
+ console.warn = jest.fn();
47
+ describe('Telegram Badge API', () => {
48
+ let req;
49
+ let res;
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ // Mock environment variables
53
+ process.env.BOT_TOKEN = 'test_token';
54
+ process.env.CHAT_ID = '@test_chat';
55
+ // Mock request and response objects
56
+ req = {
57
+ query: {},
58
+ headers: {}
59
+ };
60
+ res = {
61
+ status: jest.fn().mockReturnThis(),
62
+ send: jest.fn(),
63
+ setHeader: jest.fn(),
64
+ end: jest.fn()
65
+ };
66
+ // Mock successful Telegram API response
67
+ fetch.mockResolvedValue({
68
+ ok: true,
69
+ json: jest.fn().mockResolvedValue({
70
+ ok: true,
71
+ result: 100
72
+ })
73
+ });
74
+ });
75
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒ SVG Π±Π΅ΠΉΠ΄ΠΆ с количСством участников', async () => {
76
+ await (0, telegram_badge_1.default)(req, res);
77
+ // Check that fetch was called with correct parameters
78
+ expect(fetch).toHaveBeenCalledWith(expect.stringContaining('https://api.telegram.org/bottest_token/getChatMemberCount'), expect.any(Object));
79
+ // Check that headers were set
80
+ expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'image/svg+xml');
81
+ expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', expect.any(String));
82
+ // Check that response was sent with status 200
83
+ expect(res.status).toHaveBeenCalledWith(200);
84
+ expect(res.send).toHaveBeenCalled();
85
+ });
86
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ отсутствиС ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния', async () => {
87
+ // Remove environment variables
88
+ delete process.env.BOT_TOKEN;
89
+ await (0, telegram_badge_1.default)(req, res);
90
+ // Check that error response was sent
91
+ expect(res.status).toHaveBeenCalledWith(500);
92
+ expect(res.send).toHaveBeenCalledWith(expect.any(String));
93
+ });
94
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ ошибки Telegram API', async () => {
95
+ // Mock Telegram API error
96
+ fetch.mockResolvedValue({
97
+ ok: true,
98
+ json: jest.fn().mockResolvedValue({
99
+ ok: false,
100
+ description: 'Test error'
101
+ })
102
+ });
103
+ await (0, telegram_badge_1.default)(req, res);
104
+ // Check that error response was sent
105
+ expect(res.status).toHaveBeenCalledWith(500);
106
+ expect(res.send).toHaveBeenCalledWith(expect.any(String));
107
+ });
108
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ сСтСвыС ошибки', async () => {
109
+ // Mock network error
110
+ fetch.mockRejectedValue(new Error('Network error'));
111
+ await (0, telegram_badge_1.default)(req, res);
112
+ // Check that error response was sent
113
+ expect(res.status).toHaveBeenCalledWith(500);
114
+ expect(res.send).toHaveBeenCalledWith(expect.any(String));
115
+ });
116
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡ€ΠΈΠΌΠ΅Π½ΡΡ‚ΡŒ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ стилизации', async () => {
117
+ // Set styling parameters
118
+ req.query = {
119
+ style: 'flat-square',
120
+ label: 'Custom Label',
121
+ color: 'FF0000',
122
+ labelColor: '000000'
123
+ };
124
+ await (0, telegram_badge_1.default)(req, res);
125
+ // Check that response was sent with status 200
126
+ expect(res.status).toHaveBeenCalledWith(200);
127
+ expect(res.send).toHaveBeenCalled();
128
+ });
129
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒ 304 Not Modified ΠΏΡ€ΠΈ совпадСнии ETag', async () => {
130
+ // Set If-None-Match header
131
+ const testEtag = '\"test-etag\"';
132
+ req.headers['if-none-match'] = testEtag;
133
+ // Mock the specific environment to create a predictable ETag
134
+ const originalEnv = process.env;
135
+ process.env = {
136
+ ...originalEnv,
137
+ BOT_TOKEN: 'test_token_for_etag',
138
+ CHAT_ID: '@test_chat_for_etag'
139
+ };
140
+ // We need to create the exact same hash that would be generated
141
+ const fixedTime = 1234567890000; // Fixed timestamp
142
+ jest.spyOn(Date, 'now').mockReturnValue(fixedTime);
143
+ // Create expected ETag based on our known values
144
+ const expectedHash = crypto
145
+ .createHash('md5')
146
+ .update(JSON.stringify({
147
+ token: 'test_token_for_etag',
148
+ chatId: '@test_chat_for_etag',
149
+ query: {},
150
+ time: Math.floor(fixedTime / 300000)
151
+ }))
152
+ .digest('hex');
153
+ req.headers['if-none-match'] = `"${expectedHash}"`;
154
+ await (0, telegram_badge_1.default)(req, res);
155
+ // Restore environment
156
+ process.env = originalEnv;
157
+ // Check that response was sent with status 304
158
+ expect(res.status).toHaveBeenCalledWith(304);
159
+ expect(res.end).toHaveBeenCalled();
160
+ });
161
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚Ρ‹ запросов', async () => {
162
+ // Mock timeout error
163
+ const abortError = new Error('Request timeout');
164
+ abortError.name = 'AbortError';
165
+ fetch.mockRejectedValue(abortError);
166
+ await (0, telegram_badge_1.default)(req, res);
167
+ // Check that error response was sent with service unavailable status
168
+ expect(res.status).toHaveBeenCalledWith(503);
169
+ expect(res.send).toHaveBeenCalledWith(expect.any(String));
170
+ });
171
+ test('Π΄ΠΎΠ»ΠΆΠ΅Π½ Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ стили Π±Π΅ΠΉΠ΄ΠΆΠ°', async () => {
172
+ // Set invalid style
173
+ req.query = {
174
+ style: 'invalid-style'
175
+ };
176
+ await (0, telegram_badge_1.default)(req, res);
177
+ // Should still work with default style
178
+ expect(res.status).toHaveBeenCalledWith(200);
179
+ expect(res.send).toHaveBeenCalled();
180
+ });
181
+ });
@@ -0,0 +1,49 @@
1
+ export interface BadgeOptions {
2
+ style?: 'flat' | 'plastic' | 'flat-square' | 'for-the-badge' | 'social';
3
+ label?: string;
4
+ color?: string;
5
+ labelColor?: string;
6
+ }
7
+ export interface BadgeFormat {
8
+ label: string;
9
+ message: string;
10
+ color: string;
11
+ labelColor: string;
12
+ style: 'flat' | 'plastic' | 'flat-square' | 'for-the-badge' | 'social';
13
+ }
14
+ export interface TelegramApiResponse {
15
+ ok: boolean;
16
+ result?: number;
17
+ description?: string;
18
+ }
19
+ export interface Logger {
20
+ info: (message: string, data?: Record<string, any>) => void;
21
+ warn: (message: string, data?: Record<string, any>) => void;
22
+ error: (message: string, error?: any) => void;
23
+ debug: (message: string, data?: Record<string, any>) => void;
24
+ }
25
+ export interface RequestQuery {
26
+ style?: string;
27
+ label?: string;
28
+ color?: string;
29
+ labelColor?: string;
30
+ }
31
+ export interface RequestHeaders {
32
+ 'if-none-match'?: string;
33
+ 'user-agent'?: string;
34
+ referer?: string;
35
+ }
36
+ export interface Request {
37
+ query: RequestQuery;
38
+ headers: RequestHeaders;
39
+ }
40
+ export interface Response {
41
+ status: (code: number) => Response;
42
+ send: (data: string) => void;
43
+ end: () => void;
44
+ setHeader: (name: string, value: string) => void;
45
+ }
46
+ export interface Environment {
47
+ token: string;
48
+ chatId: string;
49
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "telegram-badge",
3
+ "version": "1.0.0",
4
+ "description": "Generate Telegram group member count badges for GitHub README",
5
+ "keywords": ["telegram", "badge", "svg", "group", "members", "api"],
6
+ "author": "Chatman Media",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/chatman-media/telegram-badge.git"
11
+ },
12
+ "homepage": "https://github.com/chatman-media/telegram-badge",
13
+ "bugs": {
14
+ "url": "https://github.com/chatman-media/telegram-badge/issues"
15
+ },
16
+ "type": "commonjs",
17
+ "main": "dist/api/telegram-badge.js",
18
+ "types": "dist/api/telegram-badge.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "types",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "dev": "vercel dev",
27
+ "start": "vercel dev",
28
+ "test": "jest",
29
+ "build": "tsc",
30
+ "type-check": "tsc --noEmit",
31
+ "prepublishOnly": "npm run build && npm test",
32
+ "prepack": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "badge-maker": "^5.0.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/jest": "^29.5.12",
39
+ "@types/node": "^24.0.15",
40
+ "dotenv": "^17.2.0",
41
+ "jest": "^30.0.4",
42
+ "node-fetch": "^2.7.0",
43
+ "typescript": "^5.5.4",
44
+ "ts-jest": "^29.2.5",
45
+ "ts-node": "^10.9.2"
46
+ }
47
+ }
package/types/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ export interface BadgeOptions {
2
+ style?: 'flat' | 'plastic' | 'flat-square' | 'for-the-badge' | 'social';
3
+ label?: string;
4
+ color?: string;
5
+ labelColor?: string;
6
+ }
7
+
8
+ export interface BadgeFormat {
9
+ label: string;
10
+ message: string;
11
+ color: string;
12
+ labelColor: string;
13
+ style: 'flat' | 'plastic' | 'flat-square' | 'for-the-badge' | 'social';
14
+ }
15
+
16
+ export interface TelegramApiResponse {
17
+ ok: boolean;
18
+ result?: number;
19
+ description?: string;
20
+ }
21
+
22
+ export interface Logger {
23
+ info: (message: string, data?: Record<string, any>) => void;
24
+ warn: (message: string, data?: Record<string, any>) => void;
25
+ error: (message: string, error?: any) => void;
26
+ debug: (message: string, data?: Record<string, any>) => void;
27
+ }
28
+
29
+ export interface RequestQuery {
30
+ style?: string;
31
+ label?: string;
32
+ color?: string;
33
+ labelColor?: string;
34
+ }
35
+
36
+ export interface RequestHeaders {
37
+ 'if-none-match'?: string;
38
+ 'user-agent'?: string;
39
+ referer?: string;
40
+ }
41
+
42
+ export interface Request {
43
+ query: RequestQuery;
44
+ headers: RequestHeaders;
45
+ }
46
+
47
+ export interface Response {
48
+ status: (code: number) => Response;
49
+ send: (data: string) => void;
50
+ end: () => void;
51
+ setHeader: (name: string, value: string) => void;
52
+ }
53
+
54
+ export interface Environment {
55
+ token: string;
56
+ chatId: string;
57
+ }