zerogramjs 2.1.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.
@@ -0,0 +1,325 @@
1
+ const { EventEmitter } = require('events');
2
+ const { TelegramClient } = require('telegram');
3
+ const { StringSession } = require('telegram/sessions');
4
+ const { Button } = require('telegram/tl/custom/button');
5
+ const debug = require('debug')('zerogram');
6
+
7
+ const SessionManager = require('./Session');
8
+ const Router = require('./Router');
9
+ const ContextFactory = require('./Context');
10
+ const { FFMpegUtil } = require('../utils/ffmpeg');
11
+
12
+ /**
13
+ * Clase principal de Zerogram
14
+ * @extends EventEmitter
15
+ */
16
+ class Zerogram extends EventEmitter {
17
+ /**
18
+ * @param {number} apiId - Telegram API ID
19
+ * @param {string} apiHash - Telegram API Hash
20
+ * @param {string} botToken - Bot token from @BotFather
21
+ * @param {Object} opts - Opciones adicionales
22
+ * @param {boolean} opts.debug - Activar modo debug
23
+ * @param {string} opts.sessionsDir - Directorio para sesiones
24
+ * @param {number} opts.maxRetries - Máximo de reintentos de conexión
25
+ * @param {number} opts.retryDelay - Delay entre reintentos (ms)
26
+ */
27
+ constructor(apiId, apiHash, botToken, opts = {}) {
28
+ super();
29
+
30
+ this._validateConfig(apiId, apiHash, botToken);
31
+
32
+ this.apiId = apiId;
33
+ this.apiHash = apiHash;
34
+ this.botToken = botToken;
35
+
36
+ // Opciones
37
+ this.options = {
38
+ debug: !!opts.debug,
39
+ sessionsDir: opts.sessionsDir || 'sessions',
40
+ maxRetries: opts.maxRetries || 3,
41
+ retryDelay: opts.retryDelay || 2000,
42
+ connectionRetries: opts.connectionRetries || 3,
43
+ useWSS: opts.useWSS !== false,
44
+ maxConcurrentDownloads: opts.maxConcurrentDownloads || 1
45
+ };
46
+
47
+ // Componentes internos
48
+ this.sessionManager = new SessionManager(this.botToken, this.options.sessionsDir);
49
+ this.router = new Router();
50
+ this.contextFactory = new ContextFactory(this);
51
+ this.ffutil = FFMpegUtil.create();
52
+
53
+ // Estado
54
+ this.client = null;
55
+ this._initialized = false;
56
+
57
+ // Manejo de señales del sistema
58
+ this._setupSignalHandlers();
59
+ }
60
+
61
+ /**
62
+ * Valida la configuración inicial
63
+ * @private
64
+ */
65
+ _validateConfig(apiId, apiHash, botToken) {
66
+ if (!apiId || typeof apiId !== 'number') {
67
+ throw new Error('apiId must be a valid number');
68
+ }
69
+ if (!apiHash || typeof apiHash !== 'string') {
70
+ throw new Error('apiHash must be a valid string');
71
+ }
72
+ if (!botToken || typeof botToken !== 'string' || !botToken.includes(':')) {
73
+ throw new Error('botToken must be a valid bot token');
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Configura manejadores de señales del sistema
79
+ * @private
80
+ */
81
+ _setupSignalHandlers() {
82
+ const shutdown = async (signal) => {
83
+ debug(`Received ${signal}, shutting down gracefully...`);
84
+ await this.stop();
85
+ process.exit(0);
86
+ };
87
+
88
+ process.on('SIGINT', () => shutdown('SIGINT'));
89
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
90
+ }
91
+
92
+ /**
93
+ * Inicializa el cliente de Telegram
94
+ * @returns {Promise<void>}
95
+ */
96
+ async init() {
97
+ if (this._initialized) {
98
+ debug('Client already initialized');
99
+ return;
100
+ }
101
+
102
+ try {
103
+ // Cargar sesión existente
104
+ const sessionData = await this.sessionManager.load();
105
+
106
+ // Crear cliente
107
+ this.client = new TelegramClient(
108
+ new StringSession(sessionData),
109
+ this.apiId,
110
+ this.apiHash,
111
+ {
112
+ connectionRetries: this.options.connectionRetries,
113
+ useWSS: this.options.useWSS,
114
+ retryDelay: this.options.retryDelay,
115
+ maxConcurrentDownloads: this.options.maxConcurrentDownloads
116
+ }
117
+ );
118
+
119
+ // Conectar con reintentos
120
+ await this._connectWithRetry();
121
+
122
+ // Guardar sesión
123
+ await this.sessionManager.save(this.client.session.save());
124
+
125
+ // Configurar router
126
+ this.client.addEventHandler((update) =>
127
+ this._handleUpdate(update).catch((err) => {
128
+ debug('Error handling update:', err);
129
+ this.emit('error', err);
130
+ })
131
+ );
132
+
133
+ this._initialized = true;
134
+ this.emit('ready');
135
+ debug('Client initialized successfully');
136
+ } catch (error) {
137
+ debug('Initialization failed:', error);
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Conecta al cliente con manejo de reintentos y FloodWait
144
+ * @private
145
+ */
146
+ async _connectWithRetry() {
147
+ const { maxRetries, retryDelay } = this.options;
148
+
149
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
150
+ try {
151
+ debug(`Connection attempt ${attempt}/${maxRetries}`);
152
+ await this.client.start({ botAuthToken: this.botToken });
153
+ debug('Connection successful');
154
+ return;
155
+ } catch (error) {
156
+ debug(`Connection attempt ${attempt} failed:`, error.message);
157
+
158
+ // Manejo de FloodWait
159
+ if (error.errorMessage === 'FLOOD') {
160
+ const waitTime = error.seconds || 300;
161
+ console.warn(`⏰ FloodWait: waiting ${waitTime}s before retry...`);
162
+
163
+ if (attempt < maxRetries) {
164
+ await this._sleep(waitTime * 1000);
165
+ continue;
166
+ } else {
167
+ throw new Error(`FloodWait: Must wait ${waitTime}s. Try again later.`);
168
+ }
169
+ }
170
+
171
+ // Último intento fallido
172
+ if (attempt === maxRetries) {
173
+ throw error;
174
+ }
175
+
176
+ // Esperar antes del siguiente intento
177
+ await this._sleep(retryDelay * attempt);
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Maneja actualizaciones de Telegram
184
+ * @private
185
+ */
186
+ async _handleUpdate(update) {
187
+ // Ignorar mensajes salientes
188
+ if (update?.message?.out) return;
189
+
190
+ const ctx = this.contextFactory.create(update);
191
+ await this.router.route(ctx, update);
192
+ }
193
+
194
+ /**
195
+ * Detiene el cliente
196
+ * @returns {Promise<void>}
197
+ */
198
+ async stop() {
199
+ if (!this.client || !this._initialized) {
200
+ debug('Client not initialized or already stopped');
201
+ return;
202
+ }
203
+
204
+ try {
205
+ await this.sessionManager.save(this.client.session.save());
206
+ await this.client.disconnect();
207
+ this._initialized = false;
208
+ this.emit('stopped');
209
+ debug('Client stopped successfully');
210
+ } catch (error) {
211
+ debug('Error during stop:', error);
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Registra un comando
218
+ * @param {string} name - Nombre del comando (sin /)
219
+ * @param {Function} handler - Función manejadora (ctx, args) => {}
220
+ */
221
+ command(name, handler) {
222
+ if (typeof name !== 'string' || !name) {
223
+ throw new Error('Command name must be a non-empty string');
224
+ }
225
+ if (typeof handler !== 'function') {
226
+ throw new Error('Command handler must be a function');
227
+ }
228
+ this.router.addCommand(name, handler);
229
+ debug(`Command registered: ${name}`);
230
+ }
231
+
232
+ /**
233
+ * Registra un callback handler
234
+ * @param {string} action - Nombre de la acción
235
+ * @param {Function} handler - Función manejadora
236
+ */
237
+ onCallback(action, handler) {
238
+ if (typeof action !== 'string' || !action) {
239
+ throw new Error('Callback action must be a non-empty string');
240
+ }
241
+ if (typeof handler !== 'function') {
242
+ throw new Error('Callback handler must be a function');
243
+ }
244
+ this.router.addCallback(action, handler);
245
+ debug(`Callback registered: ${action}`);
246
+ }
247
+
248
+ /**
249
+ * Registra un middleware
250
+ * @param {Function} middleware - Función middleware (ctx, next) => {}
251
+ */
252
+ use(middleware) {
253
+ if (typeof middleware !== 'function') {
254
+ throw new Error('Middleware must be a function');
255
+ }
256
+ this.router.addMiddleware(middleware);
257
+ debug('Middleware registered');
258
+ }
259
+
260
+ /**
261
+ * Helper para crear botones inline
262
+ * @static
263
+ */
264
+ static createButton(text, callbackData) {
265
+ const dataBuf = Buffer.isBuffer(callbackData)
266
+ ? callbackData
267
+ : Buffer.from(String(callbackData));
268
+ return Button.inline(text, dataBuf);
269
+ }
270
+
271
+ /**
272
+ * Helper para crear botones URL
273
+ * @static
274
+ */
275
+ static urlButton(text, url) {
276
+ return Button.url(text, url);
277
+ }
278
+
279
+ /**
280
+ * Obtiene el historial de mensajes de un chat
281
+ * @param {string|number} chatId - ID del chat
282
+ * @param {Object} options - Opciones
283
+ * @param {number} options.limit - Número máximo de mensajes (default: 100)
284
+ * @param {number} options.offset - Offset para paginación
285
+ * @param {Date} options.fromDate - Fecha desde la cual obtener mensajes
286
+ * @param {Date} options.toDate - Fecha hasta la cual obtener mensajes
287
+ * @returns {Promise<Array>} Array de mensajes
288
+ */
289
+ async getHistory(chatId, options = {}) {
290
+ if (!this.client || !this._initialized) {
291
+ throw new Error('Client not initialized');
292
+ }
293
+
294
+ const {
295
+ limit = 100,
296
+ offset = 0,
297
+ fromDate,
298
+ toDate
299
+ } = options;
300
+
301
+ try {
302
+ const messages = await this.client.getMessages(chatId, {
303
+ limit,
304
+ offsetId: offset,
305
+ minId: fromDate ? Math.floor(fromDate.getTime() / 1000) : undefined,
306
+ maxId: toDate ? Math.floor(toDate.getTime() / 1000) : undefined
307
+ });
308
+
309
+ return messages;
310
+ } catch (error) {
311
+ debug('Error getting history:', error);
312
+ throw error;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Utilidad para sleep
318
+ * @private
319
+ */
320
+ _sleep(ms) {
321
+ return new Promise(resolve => setTimeout(resolve, ms));
322
+ }
323
+ }
324
+
325
+ module.exports = Zerogram;
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "zerogramjs",
3
+ "version": "2.1.0",
4
+ "description": "Framework moderno y elegante para crear bots de Telegram con Node.js.",
5
+ "main": "index.js",
6
+ "files": ["lib/", "shared/", "utils/", "index.js"],
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": ["telegram", "bot", "telegram-bot-api", "nodejs"],
14
+ "author": "UserZero075",
15
+ "license": "MIT",
16
+ "type": "commonjs",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/UserZero075/ZerogramJS.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/UserZero075/ZerogramJS/issues"
23
+ },
24
+ "homepage": "https://github.com/UserZero075/ZerogramJS#readme"
25
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Estado compartido para controladores de descarga
3
+ * Permite cancelar descargas en curso
4
+ */
5
+ const downloadControllers = new Map();
6
+
7
+ module.exports = {
8
+ downloadControllers
9
+ };
@@ -0,0 +1,152 @@
1
+ const { spawn, spawnSync } = require('child_process');
2
+ const debug = require('debug')('zerogram:ffmpeg');
3
+
4
+ /**
5
+ * Utilidades para trabajar con FFmpeg
6
+ */
7
+ class FFMpegUtil {
8
+ constructor(ffmpegPath, ffprobePath) {
9
+ this.ffmpegPath = ffmpegPath;
10
+ this.ffprobePath = ffprobePath;
11
+ }
12
+
13
+ /**
14
+ * Detecta si un binario existe
15
+ * @private
16
+ */
17
+ static _bin(cmd, env) {
18
+ if (process.env[env]) {
19
+ return process.env[env];
20
+ }
21
+
22
+ try {
23
+ const result = spawnSync(cmd, ['-version'], { stdio: 'ignore' });
24
+ return !result.error && result.status === 0 ? cmd : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Crea una instancia auto-detectando binarios
32
+ * @static
33
+ */
34
+ static create() {
35
+ const ffmpegPath = FFMpegUtil._bin('ffmpeg', 'FFMPEG_PATH');
36
+ const ffprobePath = FFMpegUtil._bin('ffprobe', 'FFPROBE_PATH');
37
+
38
+ if (!ffmpegPath) {
39
+ debug('FFmpeg not found in PATH');
40
+ }
41
+ if (!ffprobePath) {
42
+ debug('FFprobe not found in PATH');
43
+ }
44
+
45
+ return new FFMpegUtil(ffmpegPath, ffprobePath);
46
+ }
47
+
48
+ /**
49
+ * Obtiene la duración de un video
50
+ * @param {Buffer} buffer - Buffer del video
51
+ * @param {number} timeout - Timeout en ms
52
+ * @returns {Promise<number>} Duración en segundos
53
+ */
54
+ duration(buffer, timeout = 8000) {
55
+ if (!this.ffprobePath) {
56
+ debug('ffprobe not available');
57
+ return Promise.resolve(0);
58
+ }
59
+
60
+ return new Promise((resolve) => {
61
+ const process = spawn(this.ffprobePath, [
62
+ '-v', 'error',
63
+ '-show_entries', 'format=duration',
64
+ '-of', 'default=nw=1:nk=1',
65
+ 'pipe:0'
66
+ ]);
67
+
68
+ let output = '';
69
+ const timer = setTimeout(() => {
70
+ process.kill('SIGKILL');
71
+ resolve(0);
72
+ }, timeout);
73
+
74
+ process.stdout.on('data', (chunk) => {
75
+ output += chunk.toString();
76
+ });
77
+
78
+ process.on('close', () => {
79
+ clearTimeout(timer);
80
+ const duration = parseFloat(output.trim());
81
+ resolve(Number.isFinite(duration) ? duration : 0);
82
+ });
83
+
84
+ process.on('error', (err) => {
85
+ debug('ffprobe error:', err);
86
+ clearTimeout(timer);
87
+ resolve(0);
88
+ });
89
+
90
+ process.stdin.write(buffer);
91
+ process.stdin.end();
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Genera un thumbnail de un video
97
+ * @param {Buffer} buffer - Buffer del video
98
+ * @param {Object} opts - Opciones
99
+ * @param {number} opts.seek - Posición en segundos
100
+ * @param {number} opts.width - Ancho del thumbnail
101
+ * @param {number} opts.timeout - Timeout en ms
102
+ * @returns {Promise<Buffer>} Buffer de la imagen
103
+ */
104
+ thumbnail(buffer, opts = {}) {
105
+ const { seek = 0.5, width = 320, timeout = 12000 } = opts;
106
+
107
+ if (!this.ffmpegPath) {
108
+ throw new Error('FFmpeg binary not found');
109
+ }
110
+
111
+ return new Promise((resolve, reject) => {
112
+ const process = spawn(this.ffmpegPath, [
113
+ '-i', 'pipe:0',
114
+ '-ss', `${seek}`,
115
+ '-frames:v', '1',
116
+ '-q:v', '2',
117
+ '-vf', `scale=${width}:-1`,
118
+ '-f', 'image2',
119
+ 'pipe:1'
120
+ ]);
121
+
122
+ const chunks = [];
123
+ const timer = setTimeout(() => {
124
+ process.kill('SIGKILL');
125
+ reject(new Error('FFmpeg timeout'));
126
+ }, timeout);
127
+
128
+ process.stdout.on('data', (chunk) => {
129
+ chunks.push(chunk);
130
+ });
131
+
132
+ process.on('error', (err) => {
133
+ clearTimeout(timer);
134
+ reject(err);
135
+ });
136
+
137
+ process.on('close', (code) => {
138
+ clearTimeout(timer);
139
+ if (code === 0) {
140
+ resolve(Buffer.concat(chunks));
141
+ } else {
142
+ reject(new Error(`FFmpeg exited with code ${code}`));
143
+ }
144
+ });
145
+
146
+ process.stdin.write(buffer);
147
+ process.stdin.end();
148
+ });
149
+ }
150
+ }
151
+
152
+ module.exports = { FFMpegUtil };
@@ -0,0 +1,71 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const debug = require('debug')('zerogram:loader');
4
+
5
+ /**
6
+ * Carga módulos automáticamente desde un directorio
7
+ * @param {string} dir - Directorio a escanear
8
+ * @param {string} mode - 'command' o 'callback'
9
+ * @param {Function} inject - Función para registrar el módulo
10
+ */
11
+ async function loadModules(dir, mode, inject) {
12
+ if (!fs.existsSync(dir)) {
13
+ debug(`Directory not found: ${dir}`);
14
+ return;
15
+ }
16
+
17
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.js'));
18
+ debug(`Loading ${files.length} modules from ${dir}`);
19
+
20
+ for (const file of files) {
21
+ const modulePath = path.join(dir, file);
22
+ const moduleName = file.replace(/\.js$/, '');
23
+
24
+ try {
25
+ const mod = require(modulePath);
26
+
27
+ if (mode === 'command') {
28
+ loadCommand(moduleName, mod, inject);
29
+ } else if (mode === 'callback') {
30
+ loadCallbacks(file, mod, inject);
31
+ }
32
+ } catch (error) {
33
+ console.error(`❌ Error loading ${file}:`, error.message);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Carga un módulo de comando
40
+ * @private
41
+ */
42
+ function loadCommand(name, mod, inject) {
43
+ if (typeof mod === 'function') {
44
+ inject(name, mod);
45
+ debug(`✅ Command loaded: ${name}`);
46
+ } else if (typeof mod.default === 'function') {
47
+ inject(name, mod.default);
48
+ debug(`✅ Command loaded: ${name} (default export)`);
49
+ } else {
50
+ debug(`⚠️ Skipped ${name}: not a function`);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Carga callbacks de un módulo
56
+ * @private
57
+ */
58
+ function loadCallbacks(fileName, mod, inject) {
59
+ debug(`📂 Loading callbacks from ${fileName}:`);
60
+
61
+ Object.entries(mod).forEach(([key, fn]) => {
62
+ if (typeof fn === 'function') {
63
+ inject(key, fn);
64
+ debug(` ✅ Callback registered: ${key}`);
65
+ } else {
66
+ debug(` ⚠️ Skipped ${key}: not a function`);
67
+ }
68
+ });
69
+ }
70
+
71
+ module.exports = { loadModules };
@@ -0,0 +1,55 @@
1
+ function parseMarkdown(text) {
2
+ // Placeholder — usa HTML directamente en telegram
3
+ return { cleanedText: text, entities: [] };
4
+ }
5
+
6
+ /**
7
+ * Convierte markdown a HTML
8
+ */
9
+ function markdownToHtml(text) {
10
+ return text
11
+ // Links: [text](url)
12
+ .replace(/\[(.+?)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
13
+ // Bold: **text** or __text__
14
+ .replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')
15
+ .replace(/__(.*?)__/g, '<i>$1</i>')
16
+ // Italic: *text*
17
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<i>$1</i>')
18
+ // Strikethrough: ~~text~~
19
+ .replace(/~~(.*?)~~/g, '<strike>$1</strike>')
20
+ // Code: `text`
21
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
22
+ // Code block: ```text```
23
+ .replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre>$2</pre>')
24
+ // Lists
25
+ .replace(/^• /gm, '• ')
26
+ }
27
+
28
+ /**
29
+ * Convierte markdown a MarkdownV2
30
+ */
31
+ function markdownToMarkdownV2(text) {
32
+ function escapeMarkdownV2(str) {
33
+ return str.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, '\\$1')
34
+ }
35
+
36
+ return text
37
+ // Links: [text](url)
38
+ .replace(/\[(.+?)\]\((https?:\/\/[^)]+)\)/g, ($0, text, url) => `[${escapeMarkdownV2(text)}](${url})`)
39
+ // Code block: ```text``` (process before inline code)
40
+ .replace(/```(\w+)?\n([\s\S]*?)```/g, ($0, lang, code) => `\`${code.replace(/`/g, '')}\``)
41
+ // Bold: **text**
42
+ .replace(/\*\*(.*?)\*\*/g, '*$1*')
43
+ // Italic: *text*
44
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '_$1_')
45
+ // Code: `text`
46
+ .replace(/`([^`]+)`/g, '`$1`')
47
+ // Escape remaining special chars
48
+ .replace(/([_\*\[\]()~`>#+\-=|{}.!\\])/g, '\\$1')
49
+ }
50
+
51
+ module.exports = {
52
+ parseMarkdown,
53
+ markdownToHtml,
54
+ markdownToMarkdownV2
55
+ }