grammy-message-queue 0.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 +115 -0
- package/dist/cjs/index.js +262 -0
- package/dist/esm/index.js +256 -0
- package/dist/types/index.d.ts +68 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# grammy-message-queue
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/grammy-message-queue)
|
|
4
|
+
[](https://www.npmjs.com/package/grammy-message-queue)
|
|
5
|
+
[](https://github.com/snipe-dev/grammy-message-queue)
|
|
6
|
+
|
|
7
|
+
Reliable sequential message delivery queue for Telegram bots built with
|
|
8
|
+
**grammy**.
|
|
9
|
+
|
|
10
|
+
- Preserves message order
|
|
11
|
+
- Automatic Telegram 429 rate limit handling
|
|
12
|
+
- Supports `retry_after`
|
|
13
|
+
- Separate send & edit queues
|
|
14
|
+
- ESM + CommonJS compatible
|
|
15
|
+
- Node.js \>= 16
|
|
16
|
+
|
|
17
|
+
------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
``` bash
|
|
22
|
+
npm install grammy-message-queue
|
|
23
|
+
npm install grammy
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
## Usage (ESM)
|
|
29
|
+
|
|
30
|
+
``` ts
|
|
31
|
+
import { Bot } from "grammy";
|
|
32
|
+
import { TelegramQueue } from "grammy-message-queue";
|
|
33
|
+
|
|
34
|
+
const bot = new Bot(process.env.BOT_TOKEN!);
|
|
35
|
+
const queue = new TelegramQueue(bot.api);
|
|
36
|
+
|
|
37
|
+
await queue.sendMessage(123456789, "<b>Hello from queue</b>");
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
## Usage (CommonJS)
|
|
43
|
+
|
|
44
|
+
``` js
|
|
45
|
+
const { Bot } = require("grammy");
|
|
46
|
+
const { TelegramQueue } = require("grammy-message-queue");
|
|
47
|
+
|
|
48
|
+
const bot = new Bot(process.env.BOT_TOKEN);
|
|
49
|
+
const queue = new TelegramQueue(bot.api);
|
|
50
|
+
|
|
51
|
+
queue.sendMessage(123456789, "<b>Hello from queue</b>");
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### new TelegramQueue(api: Api)
|
|
59
|
+
|
|
60
|
+
Creates a new transport-safe queue instance.
|
|
61
|
+
|
|
62
|
+
### sendMessage(chatId, text, buttons?)
|
|
63
|
+
|
|
64
|
+
Sequential queued send.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
|
|
68
|
+
``` ts
|
|
69
|
+
Promise<number>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### editMessage(chatId, msgId, text, buttons?)
|
|
73
|
+
|
|
74
|
+
Queued edit.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
|
|
78
|
+
``` ts
|
|
79
|
+
Promise<boolean>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### sendSingleMessage(...)
|
|
83
|
+
|
|
84
|
+
Immediate send without queue.
|
|
85
|
+
|
|
86
|
+
### sendSinglePhoto(...)
|
|
87
|
+
|
|
88
|
+
Immediate photo send.
|
|
89
|
+
|
|
90
|
+
### editSingleMessage(...)
|
|
91
|
+
|
|
92
|
+
Immediate edit.
|
|
93
|
+
|
|
94
|
+
------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
## Build
|
|
97
|
+
|
|
98
|
+
``` bash
|
|
99
|
+
npm run build
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
## Publish
|
|
105
|
+
|
|
106
|
+
``` bash
|
|
107
|
+
npm pack
|
|
108
|
+
npm publish --access public
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
## Repository
|
|
114
|
+
|
|
115
|
+
https://github.com/snipe-dev/grammy-message-queue
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TelegramQueue = exports.sleep = void 0;
|
|
4
|
+
exports.applyDefaults = applyDefaults;
|
|
5
|
+
const grammy_1 = require("grammy");
|
|
6
|
+
/**
|
|
7
|
+
* Utility function to delay execution.
|
|
8
|
+
*/
|
|
9
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
exports.sleep = sleep;
|
|
11
|
+
/**
|
|
12
|
+
* Applies default Telegram API options.
|
|
13
|
+
*/
|
|
14
|
+
function applyDefaults(method, payload) {
|
|
15
|
+
if (!payload || typeof payload !== "object") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!payload.parse_mode) {
|
|
19
|
+
payload.parse_mode = "HTML";
|
|
20
|
+
}
|
|
21
|
+
if ((method === "sendMessage" || method === "editMessageText") &&
|
|
22
|
+
!payload.link_preview_options) {
|
|
23
|
+
payload.link_preview_options = { is_disabled: true };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* TelegramQueue
|
|
28
|
+
*
|
|
29
|
+
* Sequential and rate-limit-safe outgoing message queue
|
|
30
|
+
* for Telegram bots built with grammy.
|
|
31
|
+
*/
|
|
32
|
+
class TelegramQueue {
|
|
33
|
+
constructor(api) {
|
|
34
|
+
this.botId = "unnamedbot";
|
|
35
|
+
this.sendQueue = [];
|
|
36
|
+
this.isProcessingSendQueue = false;
|
|
37
|
+
this.pendingSendMessages = new Map();
|
|
38
|
+
this.editQueue = [];
|
|
39
|
+
this.isProcessingEditQueue = false;
|
|
40
|
+
this.pendingEditMessages = new Map();
|
|
41
|
+
this.api = api;
|
|
42
|
+
this.setupApiMiddleware();
|
|
43
|
+
this.api
|
|
44
|
+
.getMe()
|
|
45
|
+
.then((me) => {
|
|
46
|
+
this.botId = me.username?.toLowerCase() || "unnamedbot";
|
|
47
|
+
})
|
|
48
|
+
.catch(() => {
|
|
49
|
+
// Silent failure
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
setupApiMiddleware() {
|
|
53
|
+
this.api.config.use((prev, method, payload) => {
|
|
54
|
+
applyDefaults(method, payload);
|
|
55
|
+
return prev(method, payload);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async sendMessage(chatId, text, buttons) {
|
|
59
|
+
return this.enqueueSendMessage(chatId, text, buttons);
|
|
60
|
+
}
|
|
61
|
+
async enqueueSendMessage(chatId, text, buttons) {
|
|
62
|
+
if (text.length > 4096) {
|
|
63
|
+
throw new Error("Message too long");
|
|
64
|
+
}
|
|
65
|
+
const messageId = `${chatId}_${Date.now()}_${Math.random()}`;
|
|
66
|
+
let resolve;
|
|
67
|
+
let reject;
|
|
68
|
+
const promise = new Promise((res, rej) => {
|
|
69
|
+
resolve = res;
|
|
70
|
+
reject = rej;
|
|
71
|
+
});
|
|
72
|
+
this.sendQueue.push({
|
|
73
|
+
chatId,
|
|
74
|
+
text,
|
|
75
|
+
buttons,
|
|
76
|
+
messageId,
|
|
77
|
+
resolve,
|
|
78
|
+
reject,
|
|
79
|
+
});
|
|
80
|
+
this.pendingSendMessages.set(messageId, promise);
|
|
81
|
+
if (!this.isProcessingSendQueue) {
|
|
82
|
+
this.processSendQueue().catch(() => {
|
|
83
|
+
// Critical failure in queue processor
|
|
84
|
+
console.error("TelegramQueue send processor crashed");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return promise;
|
|
88
|
+
}
|
|
89
|
+
async processSendQueue() {
|
|
90
|
+
if (this.isProcessingSendQueue)
|
|
91
|
+
return;
|
|
92
|
+
this.isProcessingSendQueue = true;
|
|
93
|
+
while (this.sendQueue.length > 0) {
|
|
94
|
+
const item = this.sendQueue[0];
|
|
95
|
+
try {
|
|
96
|
+
const msg = await this.api.sendMessage(item.chatId, item.text, item.buttons ? { reply_markup: item.buttons } : {});
|
|
97
|
+
this.sendQueue.shift();
|
|
98
|
+
this.pendingSendMessages.delete(item.messageId);
|
|
99
|
+
item.resolve(msg.message_id);
|
|
100
|
+
await (0, exports.sleep)(200);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const action = this.handleError(err, item.chatId);
|
|
104
|
+
if (action.retrySending) {
|
|
105
|
+
await (0, exports.sleep)(action.retryAfter * 1000);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (action.removeMessage) {
|
|
109
|
+
this.sendQueue.shift();
|
|
110
|
+
this.pendingSendMessages.delete(item.messageId);
|
|
111
|
+
item.reject(err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.isProcessingSendQueue = false;
|
|
116
|
+
}
|
|
117
|
+
async editMessage(chatId, msgId, text, buttons) {
|
|
118
|
+
return this.enqueueEditMessage(chatId, msgId, text, buttons);
|
|
119
|
+
}
|
|
120
|
+
async enqueueEditMessage(chatId, msgId, text, buttons) {
|
|
121
|
+
if (text.length > 4096) {
|
|
122
|
+
throw new Error("Message too long");
|
|
123
|
+
}
|
|
124
|
+
const editId = `${chatId}_${msgId}_${Date.now()}`;
|
|
125
|
+
let resolve;
|
|
126
|
+
let reject;
|
|
127
|
+
const promise = new Promise((res, rej) => {
|
|
128
|
+
resolve = res;
|
|
129
|
+
reject = rej;
|
|
130
|
+
});
|
|
131
|
+
this.editQueue.push({
|
|
132
|
+
chatId,
|
|
133
|
+
msgId,
|
|
134
|
+
text,
|
|
135
|
+
buttons,
|
|
136
|
+
editId,
|
|
137
|
+
resolve,
|
|
138
|
+
reject,
|
|
139
|
+
});
|
|
140
|
+
this.pendingEditMessages.set(editId, promise);
|
|
141
|
+
if (!this.isProcessingEditQueue) {
|
|
142
|
+
this.processEditQueue().catch(() => {
|
|
143
|
+
console.error("TelegramQueue edit processor crashed");
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return promise;
|
|
147
|
+
}
|
|
148
|
+
async processEditQueue() {
|
|
149
|
+
if (this.isProcessingEditQueue)
|
|
150
|
+
return;
|
|
151
|
+
this.isProcessingEditQueue = true;
|
|
152
|
+
while (this.editQueue.length > 0) {
|
|
153
|
+
const item = this.editQueue[0];
|
|
154
|
+
try {
|
|
155
|
+
await this.api.editMessageText(item.chatId, item.msgId, item.text, item.buttons ? { reply_markup: item.buttons } : {});
|
|
156
|
+
this.editQueue.shift();
|
|
157
|
+
this.pendingEditMessages.delete(item.editId);
|
|
158
|
+
item.resolve(true);
|
|
159
|
+
await (0, exports.sleep)(200);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const action = this.handleError(err, item.chatId);
|
|
163
|
+
if (action.retrySending) {
|
|
164
|
+
await (0, exports.sleep)(action.retryAfter * 1000);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (action.removeMessage) {
|
|
168
|
+
this.editQueue.shift();
|
|
169
|
+
this.pendingEditMessages.delete(item.editId);
|
|
170
|
+
item.reject(err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.isProcessingEditQueue = false;
|
|
175
|
+
}
|
|
176
|
+
async sendSingleMessage(chatId, text, buttons) {
|
|
177
|
+
if (text.length > 4096)
|
|
178
|
+
return null;
|
|
179
|
+
try {
|
|
180
|
+
const msg = await this.api.sendMessage(chatId, text, buttons ? { reply_markup: buttons } : {});
|
|
181
|
+
return msg.message_id;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
this.handleError(err, chatId);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async sendSinglePhoto(chatId, text, photo, buttons) {
|
|
189
|
+
if (text.length > 2048)
|
|
190
|
+
return null;
|
|
191
|
+
try {
|
|
192
|
+
const file = new grammy_1.InputFile(photo, "photo.png");
|
|
193
|
+
const msg = await this.api.sendPhoto(chatId, file, {
|
|
194
|
+
caption: text,
|
|
195
|
+
...(buttons ? { reply_markup: buttons } : {}),
|
|
196
|
+
});
|
|
197
|
+
return msg.message_id;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
this.handleError(err, chatId);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async editSingleMessage(chatId, msgId, text, buttons) {
|
|
205
|
+
if (text.length > 4096)
|
|
206
|
+
return null;
|
|
207
|
+
try {
|
|
208
|
+
await this.api.editMessageText(chatId, msgId, text, buttons ? { reply_markup: buttons } : {});
|
|
209
|
+
return msgId;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
this.handleError(err, chatId);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
handleError(error, chatId) {
|
|
217
|
+
const result = {
|
|
218
|
+
removeMessage: true,
|
|
219
|
+
removeUser: false,
|
|
220
|
+
retrySending: false,
|
|
221
|
+
retryAfter: 1,
|
|
222
|
+
};
|
|
223
|
+
if (!(error instanceof grammy_1.GrammyError)) {
|
|
224
|
+
console.error(`TelegramQueue unknown error for chat ${chatId}:`, error?.message || error);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
const errorCode = error.error_code;
|
|
228
|
+
const description = error.description || "";
|
|
229
|
+
if (errorCode === 429) {
|
|
230
|
+
result.retryAfter = error.parameters?.retry_after || 1;
|
|
231
|
+
result.removeMessage = false;
|
|
232
|
+
result.retrySending = true;
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
if (description.includes("bot was blocked by the user") ||
|
|
236
|
+
description.includes("chat not found") ||
|
|
237
|
+
description.includes("bot was kicked") ||
|
|
238
|
+
description.includes("user is deactivated")) {
|
|
239
|
+
result.removeUser = true;
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
if (description.includes("message text is invalid") ||
|
|
243
|
+
description.includes("message to edit not found")) {
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
console.error(`TelegramQueue unhandled error for chat ${chatId}: ${errorCode} - ${description}`);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
parseReceiver(msg) {
|
|
250
|
+
const chat = msg.chat;
|
|
251
|
+
const name = chat.username
|
|
252
|
+
? "@" + chat.username
|
|
253
|
+
: chat.title || `Chat${chat.id}`;
|
|
254
|
+
return {
|
|
255
|
+
chatId: chat.id,
|
|
256
|
+
botId: this.botId,
|
|
257
|
+
type: chat.type,
|
|
258
|
+
fullname: `${this.botId}|${name}`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
exports.TelegramQueue = TelegramQueue;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { GrammyError, InputFile } from "grammy";
|
|
2
|
+
/**
|
|
3
|
+
* Utility function to delay execution.
|
|
4
|
+
*/
|
|
5
|
+
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
/**
|
|
7
|
+
* Applies default Telegram API options.
|
|
8
|
+
*/
|
|
9
|
+
export function applyDefaults(method, payload) {
|
|
10
|
+
if (!payload || typeof payload !== "object") {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!payload.parse_mode) {
|
|
14
|
+
payload.parse_mode = "HTML";
|
|
15
|
+
}
|
|
16
|
+
if ((method === "sendMessage" || method === "editMessageText") &&
|
|
17
|
+
!payload.link_preview_options) {
|
|
18
|
+
payload.link_preview_options = { is_disabled: true };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* TelegramQueue
|
|
23
|
+
*
|
|
24
|
+
* Sequential and rate-limit-safe outgoing message queue
|
|
25
|
+
* for Telegram bots built with grammy.
|
|
26
|
+
*/
|
|
27
|
+
export class TelegramQueue {
|
|
28
|
+
constructor(api) {
|
|
29
|
+
this.botId = "unnamedbot";
|
|
30
|
+
this.sendQueue = [];
|
|
31
|
+
this.isProcessingSendQueue = false;
|
|
32
|
+
this.pendingSendMessages = new Map();
|
|
33
|
+
this.editQueue = [];
|
|
34
|
+
this.isProcessingEditQueue = false;
|
|
35
|
+
this.pendingEditMessages = new Map();
|
|
36
|
+
this.api = api;
|
|
37
|
+
this.setupApiMiddleware();
|
|
38
|
+
this.api
|
|
39
|
+
.getMe()
|
|
40
|
+
.then((me) => {
|
|
41
|
+
this.botId = me.username?.toLowerCase() || "unnamedbot";
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
// Silent failure
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
setupApiMiddleware() {
|
|
48
|
+
this.api.config.use((prev, method, payload) => {
|
|
49
|
+
applyDefaults(method, payload);
|
|
50
|
+
return prev(method, payload);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async sendMessage(chatId, text, buttons) {
|
|
54
|
+
return this.enqueueSendMessage(chatId, text, buttons);
|
|
55
|
+
}
|
|
56
|
+
async enqueueSendMessage(chatId, text, buttons) {
|
|
57
|
+
if (text.length > 4096) {
|
|
58
|
+
throw new Error("Message too long");
|
|
59
|
+
}
|
|
60
|
+
const messageId = `${chatId}_${Date.now()}_${Math.random()}`;
|
|
61
|
+
let resolve;
|
|
62
|
+
let reject;
|
|
63
|
+
const promise = new Promise((res, rej) => {
|
|
64
|
+
resolve = res;
|
|
65
|
+
reject = rej;
|
|
66
|
+
});
|
|
67
|
+
this.sendQueue.push({
|
|
68
|
+
chatId,
|
|
69
|
+
text,
|
|
70
|
+
buttons,
|
|
71
|
+
messageId,
|
|
72
|
+
resolve,
|
|
73
|
+
reject,
|
|
74
|
+
});
|
|
75
|
+
this.pendingSendMessages.set(messageId, promise);
|
|
76
|
+
if (!this.isProcessingSendQueue) {
|
|
77
|
+
this.processSendQueue().catch(() => {
|
|
78
|
+
// Critical failure in queue processor
|
|
79
|
+
console.error("TelegramQueue send processor crashed");
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return promise;
|
|
83
|
+
}
|
|
84
|
+
async processSendQueue() {
|
|
85
|
+
if (this.isProcessingSendQueue)
|
|
86
|
+
return;
|
|
87
|
+
this.isProcessingSendQueue = true;
|
|
88
|
+
while (this.sendQueue.length > 0) {
|
|
89
|
+
const item = this.sendQueue[0];
|
|
90
|
+
try {
|
|
91
|
+
const msg = await this.api.sendMessage(item.chatId, item.text, item.buttons ? { reply_markup: item.buttons } : {});
|
|
92
|
+
this.sendQueue.shift();
|
|
93
|
+
this.pendingSendMessages.delete(item.messageId);
|
|
94
|
+
item.resolve(msg.message_id);
|
|
95
|
+
await sleep(200);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const action = this.handleError(err, item.chatId);
|
|
99
|
+
if (action.retrySending) {
|
|
100
|
+
await sleep(action.retryAfter * 1000);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (action.removeMessage) {
|
|
104
|
+
this.sendQueue.shift();
|
|
105
|
+
this.pendingSendMessages.delete(item.messageId);
|
|
106
|
+
item.reject(err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.isProcessingSendQueue = false;
|
|
111
|
+
}
|
|
112
|
+
async editMessage(chatId, msgId, text, buttons) {
|
|
113
|
+
return this.enqueueEditMessage(chatId, msgId, text, buttons);
|
|
114
|
+
}
|
|
115
|
+
async enqueueEditMessage(chatId, msgId, text, buttons) {
|
|
116
|
+
if (text.length > 4096) {
|
|
117
|
+
throw new Error("Message too long");
|
|
118
|
+
}
|
|
119
|
+
const editId = `${chatId}_${msgId}_${Date.now()}`;
|
|
120
|
+
let resolve;
|
|
121
|
+
let reject;
|
|
122
|
+
const promise = new Promise((res, rej) => {
|
|
123
|
+
resolve = res;
|
|
124
|
+
reject = rej;
|
|
125
|
+
});
|
|
126
|
+
this.editQueue.push({
|
|
127
|
+
chatId,
|
|
128
|
+
msgId,
|
|
129
|
+
text,
|
|
130
|
+
buttons,
|
|
131
|
+
editId,
|
|
132
|
+
resolve,
|
|
133
|
+
reject,
|
|
134
|
+
});
|
|
135
|
+
this.pendingEditMessages.set(editId, promise);
|
|
136
|
+
if (!this.isProcessingEditQueue) {
|
|
137
|
+
this.processEditQueue().catch(() => {
|
|
138
|
+
console.error("TelegramQueue edit processor crashed");
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return promise;
|
|
142
|
+
}
|
|
143
|
+
async processEditQueue() {
|
|
144
|
+
if (this.isProcessingEditQueue)
|
|
145
|
+
return;
|
|
146
|
+
this.isProcessingEditQueue = true;
|
|
147
|
+
while (this.editQueue.length > 0) {
|
|
148
|
+
const item = this.editQueue[0];
|
|
149
|
+
try {
|
|
150
|
+
await this.api.editMessageText(item.chatId, item.msgId, item.text, item.buttons ? { reply_markup: item.buttons } : {});
|
|
151
|
+
this.editQueue.shift();
|
|
152
|
+
this.pendingEditMessages.delete(item.editId);
|
|
153
|
+
item.resolve(true);
|
|
154
|
+
await sleep(200);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const action = this.handleError(err, item.chatId);
|
|
158
|
+
if (action.retrySending) {
|
|
159
|
+
await sleep(action.retryAfter * 1000);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (action.removeMessage) {
|
|
163
|
+
this.editQueue.shift();
|
|
164
|
+
this.pendingEditMessages.delete(item.editId);
|
|
165
|
+
item.reject(err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.isProcessingEditQueue = false;
|
|
170
|
+
}
|
|
171
|
+
async sendSingleMessage(chatId, text, buttons) {
|
|
172
|
+
if (text.length > 4096)
|
|
173
|
+
return null;
|
|
174
|
+
try {
|
|
175
|
+
const msg = await this.api.sendMessage(chatId, text, buttons ? { reply_markup: buttons } : {});
|
|
176
|
+
return msg.message_id;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
this.handleError(err, chatId);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async sendSinglePhoto(chatId, text, photo, buttons) {
|
|
184
|
+
if (text.length > 2048)
|
|
185
|
+
return null;
|
|
186
|
+
try {
|
|
187
|
+
const file = new InputFile(photo, "photo.png");
|
|
188
|
+
const msg = await this.api.sendPhoto(chatId, file, {
|
|
189
|
+
caption: text,
|
|
190
|
+
...(buttons ? { reply_markup: buttons } : {}),
|
|
191
|
+
});
|
|
192
|
+
return msg.message_id;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
this.handleError(err, chatId);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async editSingleMessage(chatId, msgId, text, buttons) {
|
|
200
|
+
if (text.length > 4096)
|
|
201
|
+
return null;
|
|
202
|
+
try {
|
|
203
|
+
await this.api.editMessageText(chatId, msgId, text, buttons ? { reply_markup: buttons } : {});
|
|
204
|
+
return msgId;
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
this.handleError(err, chatId);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
handleError(error, chatId) {
|
|
212
|
+
const result = {
|
|
213
|
+
removeMessage: true,
|
|
214
|
+
removeUser: false,
|
|
215
|
+
retrySending: false,
|
|
216
|
+
retryAfter: 1,
|
|
217
|
+
};
|
|
218
|
+
if (!(error instanceof GrammyError)) {
|
|
219
|
+
console.error(`TelegramQueue unknown error for chat ${chatId}:`, error?.message || error);
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
const errorCode = error.error_code;
|
|
223
|
+
const description = error.description || "";
|
|
224
|
+
if (errorCode === 429) {
|
|
225
|
+
result.retryAfter = error.parameters?.retry_after || 1;
|
|
226
|
+
result.removeMessage = false;
|
|
227
|
+
result.retrySending = true;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
if (description.includes("bot was blocked by the user") ||
|
|
231
|
+
description.includes("chat not found") ||
|
|
232
|
+
description.includes("bot was kicked") ||
|
|
233
|
+
description.includes("user is deactivated")) {
|
|
234
|
+
result.removeUser = true;
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
if (description.includes("message text is invalid") ||
|
|
238
|
+
description.includes("message to edit not found")) {
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
console.error(`TelegramQueue unhandled error for chat ${chatId}: ${errorCode} - ${description}`);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
parseReceiver(msg) {
|
|
245
|
+
const chat = msg.chat;
|
|
246
|
+
const name = chat.username
|
|
247
|
+
? "@" + chat.username
|
|
248
|
+
: chat.title || `Chat${chat.id}`;
|
|
249
|
+
return {
|
|
250
|
+
chatId: chat.id,
|
|
251
|
+
botId: this.botId,
|
|
252
|
+
type: chat.type,
|
|
253
|
+
fullname: `${this.botId}|${name}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Api } from "grammy";
|
|
2
|
+
import type { InlineKeyboard } from "grammy";
|
|
3
|
+
import type { Message } from "grammy/types";
|
|
4
|
+
/**
|
|
5
|
+
* Internal queue item representing a pending send operation.
|
|
6
|
+
*/
|
|
7
|
+
export type SendQueueItem = {
|
|
8
|
+
chatId: number | string;
|
|
9
|
+
text: string;
|
|
10
|
+
buttons: InlineKeyboard | undefined;
|
|
11
|
+
messageId: string;
|
|
12
|
+
resolve: (value: number) => void;
|
|
13
|
+
reject: (reason?: any) => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Internal queue item representing a pending edit operation.
|
|
17
|
+
*/
|
|
18
|
+
export type EditQueueItem = {
|
|
19
|
+
chatId: number | string;
|
|
20
|
+
msgId: number;
|
|
21
|
+
text: string;
|
|
22
|
+
buttons: InlineKeyboard | undefined;
|
|
23
|
+
editId: string;
|
|
24
|
+
resolve: (value: boolean) => void;
|
|
25
|
+
reject: (reason?: any) => void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Utility function to delay execution.
|
|
29
|
+
*/
|
|
30
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Applies default Telegram API options.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyDefaults(method: string, payload: any): void;
|
|
35
|
+
/**
|
|
36
|
+
* TelegramQueue
|
|
37
|
+
*
|
|
38
|
+
* Sequential and rate-limit-safe outgoing message queue
|
|
39
|
+
* for Telegram bots built with grammy.
|
|
40
|
+
*/
|
|
41
|
+
export declare class TelegramQueue {
|
|
42
|
+
private api;
|
|
43
|
+
private botId;
|
|
44
|
+
private sendQueue;
|
|
45
|
+
private isProcessingSendQueue;
|
|
46
|
+
private pendingSendMessages;
|
|
47
|
+
private editQueue;
|
|
48
|
+
private isProcessingEditQueue;
|
|
49
|
+
private pendingEditMessages;
|
|
50
|
+
constructor(api: Api);
|
|
51
|
+
private setupApiMiddleware;
|
|
52
|
+
sendMessage(chatId: number | string, text: string, buttons?: InlineKeyboard): Promise<number>;
|
|
53
|
+
private enqueueSendMessage;
|
|
54
|
+
private processSendQueue;
|
|
55
|
+
editMessage(chatId: number | string, msgId: number, text: string, buttons?: InlineKeyboard): Promise<boolean>;
|
|
56
|
+
private enqueueEditMessage;
|
|
57
|
+
private processEditQueue;
|
|
58
|
+
sendSingleMessage(chatId: number | string, text: string, buttons?: InlineKeyboard): Promise<number | null>;
|
|
59
|
+
sendSinglePhoto(chatId: number | string, text: string, photo: Buffer, buttons?: InlineKeyboard): Promise<number | null>;
|
|
60
|
+
editSingleMessage(chatId: number | string, msgId: number, text: string, buttons?: InlineKeyboard): Promise<number | null>;
|
|
61
|
+
private handleError;
|
|
62
|
+
parseReceiver(msg: Message): {
|
|
63
|
+
chatId: number;
|
|
64
|
+
botId: string;
|
|
65
|
+
type: "private" | "group" | "supergroup" | "channel";
|
|
66
|
+
fullname: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "grammy-message-queue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reliable sequential message queue for Telegram bots built with grammy. Handles rate limits (429), retry_after and preserves message order.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"telegram",
|
|
7
|
+
"telegram-bot",
|
|
8
|
+
"grammy",
|
|
9
|
+
"message-queue",
|
|
10
|
+
"rate-limit",
|
|
11
|
+
"bot",
|
|
12
|
+
"queue"
|
|
13
|
+
],
|
|
14
|
+
"author": "snipe-dev",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/snipe-dev/grammy-message-queue.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/snipe-dev/grammy-message-queue#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/snipe-dev/grammy-message-queue/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/cjs/index.js",
|
|
26
|
+
"module": "./dist/esm/index.js",
|
|
27
|
+
"types": "./dist/types/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/types/index.d.ts",
|
|
31
|
+
"require": "./dist/cjs/index.js",
|
|
32
|
+
"import": "./dist/esm/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
40
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
41
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=16"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"grammy": "^1.39.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^25.1.0",
|
|
52
|
+
"typescript": "^5.4.0"
|
|
53
|
+
}
|
|
54
|
+
}
|