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/Context.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const { Api } = require('telegram/tl');
|
|
2
|
+
const { markdownToHtml, markdownToMarkdownV2 } = require('../utils/markdown');
|
|
3
|
+
const Downloader = require('./Downloader');
|
|
4
|
+
const debug = require('debug')('zerogram:context');
|
|
5
|
+
|
|
6
|
+
class ContextFactory {
|
|
7
|
+
constructor(bot) {
|
|
8
|
+
this.bot = bot;
|
|
9
|
+
this.downloader = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
create(update) {
|
|
13
|
+
const rawMessage = update.message ?? {};
|
|
14
|
+
// GramJS usa message.message para el texto, mapearlo a message.text para consistencia
|
|
15
|
+
const message = {
|
|
16
|
+
...rawMessage,
|
|
17
|
+
text: rawMessage.message || rawMessage.text || ''
|
|
18
|
+
};
|
|
19
|
+
const peer = this._extractPeer(message, update);
|
|
20
|
+
const isCallback = update?.className === 'UpdateBotCallbackQuery' || update?.className === 'UpdateInlineBotCallbackQuery';
|
|
21
|
+
const callbackData = Buffer.isBuffer(update?.data)
|
|
22
|
+
? update.data.toString('utf8')
|
|
23
|
+
: (update?.data ?? null);
|
|
24
|
+
const payload = callbackData ? callbackData.split(':')[0] : null;
|
|
25
|
+
const downloader = this._getDownloader();
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
bot: this.bot,
|
|
29
|
+
client: this.bot.client,
|
|
30
|
+
update,
|
|
31
|
+
message,
|
|
32
|
+
isCallback,
|
|
33
|
+
callbackData,
|
|
34
|
+
|
|
35
|
+
reply: this._createReplyFn(peer, message.id),
|
|
36
|
+
send: this._createSendFn(peer),
|
|
37
|
+
sendFile: this._createSendFileFn(peer),
|
|
38
|
+
answerCallback: this._createAnswerCallbackFn(update),
|
|
39
|
+
|
|
40
|
+
getReplyMessage: this._createGetReplyMessageFn(peer, message),
|
|
41
|
+
getMessages: this._createGetMessagesFn(peer),
|
|
42
|
+
thumbFromVideo: (buf, opts) => this.bot.ffutil.thumbnail(buf, opts),
|
|
43
|
+
|
|
44
|
+
downloadMedia: (msg, opts) => downloader.downloadMedia(msg, opts),
|
|
45
|
+
downloadMediaStream: (msg, opts) => downloader.downloadMediaStream(msg, opts),
|
|
46
|
+
downloadMediaBatch: (msgs, opts) => downloader.downloadMediaBatch(msgs, opts),
|
|
47
|
+
downloadControllers: downloader.controllers
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_getDownloader() {
|
|
52
|
+
if (!this.bot.client) {
|
|
53
|
+
throw new Error('Telegram client no está inicializado todavía');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!this.downloader || this.downloader.client !== this.bot.client) {
|
|
57
|
+
this.downloader = new Downloader(this.bot.client, this.bot.ffutil);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.downloader;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_extractPeer(message, update) {
|
|
64
|
+
// Para mensajes normales
|
|
65
|
+
const msgPeer = message.peerId ?? message.peer ?? null;
|
|
66
|
+
if (msgPeer) return msgPeer;
|
|
67
|
+
|
|
68
|
+
// Para callbacks: el peer está en update.peer (el chat) o update.userId (el usuario)
|
|
69
|
+
const callbackPeer = update?.peer ?? update?.chat ?? update?.chatInstance ?? null;
|
|
70
|
+
if (callbackPeer) return callbackPeer;
|
|
71
|
+
|
|
72
|
+
// Si no hay peer, intentar obtenerlo del bot
|
|
73
|
+
return update?.userId ?? update?.fromId ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_createAnswerCallbackFn(update) {
|
|
77
|
+
return async (text = '', opts = {}) => {
|
|
78
|
+
// GramJS usa snake_case en los updates
|
|
79
|
+
const queryId = update?.queryId ?? update?.query_id;
|
|
80
|
+
|
|
81
|
+
if (!queryId) {
|
|
82
|
+
throw new Error('answerCallback() solo está disponible en callbacks con queryId');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Convertir a BigInt de forma segura
|
|
86
|
+
let queryIdBigInt;
|
|
87
|
+
try {
|
|
88
|
+
if (typeof queryId === 'bigint') {
|
|
89
|
+
queryIdBigInt = queryId;
|
|
90
|
+
} else if (typeof queryId === 'string') {
|
|
91
|
+
queryIdBigInt = BigInt(queryId);
|
|
92
|
+
} else if (typeof queryId === 'number') {
|
|
93
|
+
queryIdBigInt = BigInt(queryId);
|
|
94
|
+
} else {
|
|
95
|
+
// Puede ser un objeto con toString
|
|
96
|
+
queryIdBigInt = BigInt(queryId.toString());
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
throw new Error(`Invalid queryId format: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.bot.client.invoke(new Api.messages.SetBotCallbackAnswer({
|
|
103
|
+
queryId: queryIdBigInt,
|
|
104
|
+
message: text || '',
|
|
105
|
+
alert: !!opts.alert,
|
|
106
|
+
url: opts.url,
|
|
107
|
+
cacheTime: Number.isInteger(opts.cacheTime) ? opts.cacheTime : 0
|
|
108
|
+
}));
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_createReplyFn(peer, messageId) {
|
|
113
|
+
return (text, opts = {}) => {
|
|
114
|
+
const htmlText = markdownToHtml(text);
|
|
115
|
+
|
|
116
|
+
return (async () => {
|
|
117
|
+
let msg = await this.bot.client.sendMessage(peer, {
|
|
118
|
+
message: htmlText,
|
|
119
|
+
parseMode: 'html',
|
|
120
|
+
replyTo: messageId,
|
|
121
|
+
...opts
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(msg)) msg = msg[0];
|
|
125
|
+
|
|
126
|
+
msg.safeEdit = (newText, editOpts = {}) => {
|
|
127
|
+
const htmlNewText = markdownToHtml(newText);
|
|
128
|
+
return this.bot.client.editMessage(peer, {
|
|
129
|
+
message: msg.id,
|
|
130
|
+
text: htmlNewText,
|
|
131
|
+
parseMode: 'html',
|
|
132
|
+
...editOpts
|
|
133
|
+
}).catch((err) => {
|
|
134
|
+
debug('safeEdit error:', err);
|
|
135
|
+
return null;
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return msg;
|
|
140
|
+
})();
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_createSendFn(peer) {
|
|
145
|
+
return (text, opts = {}) => {
|
|
146
|
+
const mode = opts.parseMode || 'html';
|
|
147
|
+
const formattedText = mode === 'markdownv2'
|
|
148
|
+
? markdownToMarkdownV2(text)
|
|
149
|
+
: markdownToHtml(text);
|
|
150
|
+
|
|
151
|
+
return this.bot.client.sendMessage(peer, {
|
|
152
|
+
message: formattedText,
|
|
153
|
+
parseMode: mode,
|
|
154
|
+
...opts
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_createSendFileFn(peer) {
|
|
160
|
+
return (file, opts = {}) => {
|
|
161
|
+
return this.bot.client.sendFile(peer, { file, ...opts });
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_createGetReplyMessageFn(peer, message) {
|
|
166
|
+
return async () => {
|
|
167
|
+
if (!message.replyTo) return null;
|
|
168
|
+
|
|
169
|
+
const id = message.replyTo.replyToMsgId ?? message.replyTo.msgId ?? null;
|
|
170
|
+
if (!id) return null;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const [msg] = await this.bot.client.getMessages(peer, { ids: [id] });
|
|
174
|
+
return msg || null;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
debug('Error getting reply message:', error);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_createGetMessagesFn(peer) {
|
|
183
|
+
return async (ids) => {
|
|
184
|
+
try {
|
|
185
|
+
if (!Array.isArray(ids)) ids = [ids];
|
|
186
|
+
return await this.bot.client.getMessages(peer, { ids });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
debug('Error getting messages:', error);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = ContextFactory;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { PassThrough } = require('stream');
|
|
3
|
+
const debug = require('debug')('zerogram:downloader');
|
|
4
|
+
const { downloadControllers } = require('../shared/downloadState');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maneja descargas de medios con streaming real
|
|
8
|
+
*/
|
|
9
|
+
class Downloader {
|
|
10
|
+
constructor(client, ffutil) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.ffutil = ffutil;
|
|
13
|
+
this.controllers = downloadControllers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Descarga un medio con progreso
|
|
18
|
+
*/
|
|
19
|
+
async downloadMedia(msg, opts = {}) {
|
|
20
|
+
const { filePath, onProgress, controller } = opts;
|
|
21
|
+
|
|
22
|
+
if (!msg) {
|
|
23
|
+
throw new Error('Message is required for downloadMedia');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (controller?.signal?.aborted) {
|
|
27
|
+
throw new Error('DOWNLOAD_CANCELLED');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
let lastBytes = 0;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const buffer = await this.client.downloadMedia(msg, {
|
|
35
|
+
outputFile: filePath,
|
|
36
|
+
progressCallback: (downloadedBytes, totalBytes) => {
|
|
37
|
+
if (controller?.signal?.aborted) {
|
|
38
|
+
throw new Error('DOWNLOAD_CANCELLED');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
42
|
+
const speed = elapsed > 0 ? downloadedBytes / elapsed : 0;
|
|
43
|
+
const delta = downloadedBytes - lastBytes;
|
|
44
|
+
|
|
45
|
+
if (typeof onProgress === 'function') {
|
|
46
|
+
onProgress({
|
|
47
|
+
downloadedBytes,
|
|
48
|
+
totalBytes,
|
|
49
|
+
speed,
|
|
50
|
+
percent: totalBytes ? (downloadedBytes / totalBytes) * 100 : 0,
|
|
51
|
+
delta,
|
|
52
|
+
elapsed
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lastBytes = downloadedBytes;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return buffer;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.message === 'DOWNLOAD_CANCELLED') {
|
|
63
|
+
debug('Download cancelled by user');
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Descarga con streaming real - ¡NUEVO!
|
|
71
|
+
* Retorna un stream que empieza a emitir datos inmediatamente
|
|
72
|
+
*/
|
|
73
|
+
downloadMediaStream(msg, opts = {}) {
|
|
74
|
+
const { onProgress, controller } = opts;
|
|
75
|
+
|
|
76
|
+
if (!msg) {
|
|
77
|
+
throw new Error('Message is required for downloadMediaStream');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stream = new PassThrough();
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
let downloadedBytes = 0;
|
|
83
|
+
let totalBytes = 0;
|
|
84
|
+
|
|
85
|
+
// Obtener el tamaño total del archivo
|
|
86
|
+
if (msg.media?.document) {
|
|
87
|
+
totalBytes = msg.media.document.size || 0;
|
|
88
|
+
} else if (msg.media?.photo) {
|
|
89
|
+
const sizes = msg.media.photo.sizes || [];
|
|
90
|
+
const largest = sizes[sizes.length - 1];
|
|
91
|
+
totalBytes = largest?.size || 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Iniciar descarga asíncrona
|
|
95
|
+
(async () => {
|
|
96
|
+
try {
|
|
97
|
+
// Verificar cancelación inicial
|
|
98
|
+
if (controller?.signal?.aborted) {
|
|
99
|
+
stream.destroy(new Error('DOWNLOAD_CANCELLED'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Usar iterDownload para streaming real
|
|
104
|
+
for await (const chunk of this.client.iterDownload({
|
|
105
|
+
file: msg.media,
|
|
106
|
+
requestSize: 512 * 1024, // 512KB chunks
|
|
107
|
+
})) {
|
|
108
|
+
// Verificar cancelación en cada chunk
|
|
109
|
+
if (controller?.signal?.aborted) {
|
|
110
|
+
stream.destroy(new Error('DOWNLOAD_CANCELLED'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
downloadedBytes += chunk.length;
|
|
115
|
+
|
|
116
|
+
// Escribir chunk al stream inmediatamente
|
|
117
|
+
if (!stream.write(chunk)) {
|
|
118
|
+
// Esperar si el buffer está lleno
|
|
119
|
+
await new Promise(resolve => stream.once('drain', resolve));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Callback de progreso
|
|
123
|
+
if (typeof onProgress === 'function') {
|
|
124
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
125
|
+
const speed = elapsed > 0 ? downloadedBytes / elapsed : 0;
|
|
126
|
+
|
|
127
|
+
onProgress({
|
|
128
|
+
downloadedBytes,
|
|
129
|
+
totalBytes,
|
|
130
|
+
speed,
|
|
131
|
+
percent: totalBytes ? (downloadedBytes / totalBytes) * 100 : 0,
|
|
132
|
+
elapsed
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Terminar el stream
|
|
138
|
+
stream.end();
|
|
139
|
+
debug('Stream completed successfully');
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
debug('Stream error:', error);
|
|
143
|
+
stream.destroy(error);
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
|
|
147
|
+
return stream;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Descarga múltiples medios en lote
|
|
152
|
+
*/
|
|
153
|
+
async downloadMediaBatch(messages, opts = {}) {
|
|
154
|
+
const { dir = '.', onProgress, controller } = opts;
|
|
155
|
+
|
|
156
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
157
|
+
throw new Error('messages[] is required and must not be empty');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (controller?.signal?.aborted) {
|
|
161
|
+
throw new Error('DOWNLOAD_CANCELLED');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const totalBytes = messages.reduce((acc, msg) => {
|
|
165
|
+
return acc + (msg.media?.document?.size || 0);
|
|
166
|
+
}, 0);
|
|
167
|
+
|
|
168
|
+
let overallDownloaded = 0;
|
|
169
|
+
const results = [];
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < messages.length; i++) {
|
|
172
|
+
const msg = messages[i];
|
|
173
|
+
|
|
174
|
+
if (controller?.signal?.aborted) {
|
|
175
|
+
throw new Error('DOWNLOAD_CANCELLED');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fileName = this._extractFileName(msg, i);
|
|
179
|
+
const filePath = path.join(dir, fileName);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const buffer = await this.downloadMedia(msg, {
|
|
183
|
+
filePath,
|
|
184
|
+
controller,
|
|
185
|
+
onProgress: (progress) => {
|
|
186
|
+
overallDownloaded += progress.delta;
|
|
187
|
+
|
|
188
|
+
if (typeof onProgress === 'function') {
|
|
189
|
+
onProgress({
|
|
190
|
+
index: i + 1,
|
|
191
|
+
total: messages.length,
|
|
192
|
+
fileName,
|
|
193
|
+
...progress,
|
|
194
|
+
overallDownloaded,
|
|
195
|
+
overallTotal: totalBytes,
|
|
196
|
+
overallPercent: totalBytes > 0
|
|
197
|
+
? (overallDownloaded / totalBytes) * 100
|
|
198
|
+
: 0
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
results.push({ filePath, buffer, message: msg });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error.message === 'DOWNLOAD_CANCELLED') {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
debug(`Error downloading file ${i + 1}:`, error);
|
|
211
|
+
results.push({ filePath, error, message: msg });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extrae el nombre de archivo de un mensaje
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
_extractFileName(msg, index) {
|
|
223
|
+
const attrs = msg.media?.document?.attributes;
|
|
224
|
+
if (attrs) {
|
|
225
|
+
const fileAttr = attrs.find(a => a.fileName);
|
|
226
|
+
if (fileAttr?.fileName) {
|
|
227
|
+
return fileAttr.fileName;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return `file_${msg.id || index}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = Downloader;
|
package/lib/Router.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const debug = require('debug')('zerogram:router');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maneja el enrutamiento de comandos y callbacks
|
|
5
|
+
*/
|
|
6
|
+
class Router {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.commands = new Map();
|
|
9
|
+
this.callbacks = new Map();
|
|
10
|
+
this.middlewares = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Registra un comando
|
|
15
|
+
*/
|
|
16
|
+
addCommand(name, handler) {
|
|
17
|
+
this.commands.set(name, handler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registra un callback
|
|
22
|
+
*/
|
|
23
|
+
addCallback(action, handler) {
|
|
24
|
+
this.callbacks.set(action, handler);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Registra un middleware
|
|
29
|
+
*/
|
|
30
|
+
addMiddleware(middleware) {
|
|
31
|
+
this.middlewares.push(middleware);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Procesa una actualización
|
|
36
|
+
*/
|
|
37
|
+
async route(ctx, update) {
|
|
38
|
+
// Ejecutar middlewares
|
|
39
|
+
await this._runMiddlewares(ctx);
|
|
40
|
+
|
|
41
|
+
// Detectar tipo de update
|
|
42
|
+
if (this._isCommand(ctx)) {
|
|
43
|
+
return this._handleCommand(ctx);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this._isCallback(update)) {
|
|
47
|
+
return this._handleCallback(ctx, update);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Evento no manejado
|
|
51
|
+
ctx.bot.emit('unhandled', ctx);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Verifica si es un comando
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
_isCommand(ctx) {
|
|
59
|
+
const text = ctx.message.text || ctx.message.message || '';
|
|
60
|
+
return text.startsWith('/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verifica si es un callback
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
_isCallback(update) {
|
|
68
|
+
const name = update?.className;
|
|
69
|
+
return name === 'UpdateBotCallbackQuery' || name === 'UpdateInlineBotCallbackQuery';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Maneja comandos
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
async _handleCommand(ctx) {
|
|
77
|
+
const text = ctx.message.text || ctx.message.message || '';
|
|
78
|
+
const [cmd, ...args] = text.slice(1).split(/\s+/);
|
|
79
|
+
|
|
80
|
+
const handler = this.commands.get(cmd);
|
|
81
|
+
if (handler) {
|
|
82
|
+
debug(`Executing command: /${cmd}`);
|
|
83
|
+
return handler(ctx, args);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Maneja callbacks
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
async _handleCallback(ctx, update) {
|
|
92
|
+
const data = update.data.toString('utf8');
|
|
93
|
+
let action, payload;
|
|
94
|
+
|
|
95
|
+
if (data.includes(':')) {
|
|
96
|
+
// Formato action:payload (con dos puntos)
|
|
97
|
+
const [actionPart, ...payloadParts] = data.split(':');
|
|
98
|
+
action = actionPart;
|
|
99
|
+
payload = payloadParts.join(':');
|
|
100
|
+
} else if (/_\d+$/.test(data)) {
|
|
101
|
+
// Formato action_payload (con underscore y número al final)
|
|
102
|
+
const lastUnderscore = data.lastIndexOf('_');
|
|
103
|
+
action = data.substring(0, lastUnderscore + 1); // incluye el _
|
|
104
|
+
payload = data.substring(lastUnderscore + 1);
|
|
105
|
+
} else {
|
|
106
|
+
// Formato simple sin separador
|
|
107
|
+
action = data;
|
|
108
|
+
payload = '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
debug(`Callback received - data: ${data}, action: ${action}, payload: ${payload}`);
|
|
112
|
+
|
|
113
|
+
const handler = this.callbacks.get(action);
|
|
114
|
+
if (handler) {
|
|
115
|
+
debug(`Executing callback handler: ${action}`);
|
|
116
|
+
return handler({
|
|
117
|
+
...ctx,
|
|
118
|
+
action,
|
|
119
|
+
payload,
|
|
120
|
+
queryId: update.queryId,
|
|
121
|
+
userId: update.userId?.toString()
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
debug(`No handler found for callback: ${action}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ejecuta middlewares en cadena
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
async _runMiddlewares(ctx) {
|
|
133
|
+
let index = -1;
|
|
134
|
+
|
|
135
|
+
const dispatch = async (i) => {
|
|
136
|
+
if (i <= index) {
|
|
137
|
+
throw new Error('next() called multiple times');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
index = i;
|
|
141
|
+
const middleware = this.middlewares[i];
|
|
142
|
+
|
|
143
|
+
if (!middleware) return;
|
|
144
|
+
|
|
145
|
+
await middleware(ctx, () => dispatch(i + 1));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await dispatch(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = Router;
|
package/lib/Session.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const fsSync = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const debug = require('debug')('zerogram:session');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maneja la persistencia de sesiones
|
|
8
|
+
*/
|
|
9
|
+
class SessionManager {
|
|
10
|
+
constructor(botToken, sessionsDir) {
|
|
11
|
+
this.sessionsDir = path.resolve(sessionsDir);
|
|
12
|
+
this.sessionFile = path.join(
|
|
13
|
+
this.sessionsDir,
|
|
14
|
+
`${botToken.split(':')[0]}.session`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Carga una sesión existente
|
|
20
|
+
* @returns {Promise<string>} Session data o string vacío
|
|
21
|
+
*/
|
|
22
|
+
async load() {
|
|
23
|
+
try {
|
|
24
|
+
// Crear directorio si no existe
|
|
25
|
+
if (!fsSync.existsSync(this.sessionsDir)) {
|
|
26
|
+
await fs.mkdir(this.sessionsDir, { recursive: true });
|
|
27
|
+
debug('Sessions directory created');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Cargar archivo de sesión
|
|
31
|
+
if (fsSync.existsSync(this.sessionFile)) {
|
|
32
|
+
const data = await fs.readFile(this.sessionFile, 'utf8');
|
|
33
|
+
debug('Session loaded from file');
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
debug('No existing session found');
|
|
38
|
+
return '';
|
|
39
|
+
} catch (error) {
|
|
40
|
+
debug('Error loading session:', error);
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Guarda la sesión actual
|
|
47
|
+
* @param {string} sessionData - Datos de sesión a guardar
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
*/
|
|
50
|
+
async save(sessionData) {
|
|
51
|
+
try {
|
|
52
|
+
await fs.writeFile(this.sessionFile, sessionData, 'utf8');
|
|
53
|
+
debug('Session saved successfully');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
debug('Error saving session:', error);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Elimina la sesión
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
async delete() {
|
|
65
|
+
try {
|
|
66
|
+
if (fsSync.existsSync(this.sessionFile)) {
|
|
67
|
+
await fs.unlink(this.sessionFile);
|
|
68
|
+
debug('Session deleted');
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
debug('Error deleting session:', error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = SessionManager;
|