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.
- package/README.md +587 -0
- package/index.js +18 -0
- package/lib/Context.js +195 -0
- package/lib/Downloader.js +234 -0
- package/lib/Router.js +152 -0
- package/lib/Session.js +77 -0
- package/lib/Zerogram.js +325 -0
- package/package.json +25 -0
- package/shared/downloadState.js +9 -0
- package/utils/ffmpeg.js +152 -0
- package/utils/loader.js +71 -0
- package/utils/markdown.js +55 -0
package/lib/Zerogram.js
ADDED
|
@@ -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
|
+
}
|
package/utils/ffmpeg.js
ADDED
|
@@ -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 };
|
package/utils/loader.js
ADDED
|
@@ -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
|
+
}
|