@zero-bot.net/tg-bot-api 1.3.0 → 1.5.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 +100 -81
- package/index.js +5 -11
- package/package.json +19 -37
- package/src/errors.js +33 -3
- package/src/fileTypes.js +188 -0
- package/src/network.js +47 -0
- package/src/telegram.d.ts +33 -3
- package/src/telegram.js +140 -23
- package/src/telegramPolling.js +9 -3
- package/src/telegramWebHook.js +53 -4
- package/.github/ISSUE_TEMPLATE.md +0 -68
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -23
- package/CHANGELOG.md +0 -515
- package/CODE_OF_CONDUCT.md +0 -74
- package/CONTRIBUTING.md +0 -45
- package/doc/api.hbs +0 -19
- package/doc/api.md +0 -3473
- package/doc/experimental.md +0 -28
- package/doc/help.md +0 -151
- package/doc/usage.md +0 -269
- package/lib/errors.js +0 -62
- package/lib/telegram.d.ts +0 -5262
- package/lib/telegram.js +0 -4179
- package/lib/telegramPolling.js +0 -196
- package/lib/telegramWebHook.js +0 -156
- package/lib/utils.js +0 -3
package/README.md
CHANGED
|
@@ -2,130 +2,149 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
Node.js
|
|
5
|
+
A lightweight, dependency-light Node.js library for the [Telegram Bot API](https://core.telegram.org/bots/api).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[](https://core.telegram.org/bots/api)
|
|
7
|
+
[](https://core.telegram.org/bots/api)
|
|
9
8
|
[](https://www.npmjs.org/package/@zero-bot.net/tg-bot-api)
|
|
10
|
-
[](https://travis-ci.org/ZeroBot-net/tg-bot-api)
|
|
11
|
-
[](https://codecov.io/gh/ZeroBot-net/tg-bot-api)
|
|
12
|
-
|
|
13
|
-
[](https://telegram.me/node_telegram_bot_api)
|
|
14
|
-
[](https://t.me/+nc3A9Hs1S81mYzdk)
|
|
15
|
-
[](https://telegram.me/Yago_Perez)
|
|
16
9
|
|
|
17
10
|
</div>
|
|
18
11
|
|
|
12
|
+
## ✨ Features
|
|
13
|
+
|
|
14
|
+
- **202 methods** — full Telegram Bot API 10.3 coverage
|
|
15
|
+
- **No build step** — ships native CommonJS, runs on Node.js 18+
|
|
16
|
+
- **Small dependency tree** — file-type detection and MIME lookup are built in
|
|
17
|
+
- **Built-in TypeScript definitions**
|
|
18
|
+
- **Both update modes** — long polling *and* webhooks (with a built-in HTTP(S) server)
|
|
19
|
+
- Rich Messages, Ephemeral Messages, Guest Mode, Business Accounts
|
|
20
|
+
- Gifts, Stars & Payments, Checklists, Media Polls, Live Photos, Stories
|
|
21
|
+
- Communities, Managed Bots, Suggested Posts, and more
|
|
22
|
+
|
|
19
23
|
## 📦 Install
|
|
20
24
|
|
|
21
25
|
```sh
|
|
22
26
|
npm i @zero-bot.net/tg-bot-api
|
|
23
27
|
```
|
|
24
28
|
|
|
25
|
-
<br/>
|
|
26
|
-
|
|
27
|
-
> ✍️ **Note:** If you use Typescript you can install this package that contains type definitions for this library
|
|
28
|
-
>```sh
|
|
29
|
-
>npm install --save-dev @types/@zero-bot.net/tg-bot-api
|
|
30
|
-
>```
|
|
31
|
-
|
|
32
29
|
## 🚀 Usage
|
|
33
30
|
|
|
34
31
|
```js
|
|
35
32
|
const TelegramBot = require('@zero-bot.net/tg-bot-api');
|
|
36
33
|
|
|
37
|
-
// replace the value below with the Telegram token you receive from @BotFather
|
|
38
34
|
const token = 'YOUR_TELEGRAM_BOT_TOKEN';
|
|
39
35
|
|
|
40
|
-
//
|
|
41
|
-
const bot = new TelegramBot(token, {polling: true});
|
|
36
|
+
// Polling mode
|
|
37
|
+
const bot = new TelegramBot(token, { polling: true });
|
|
42
38
|
|
|
43
|
-
// Matches "/echo [whatever]"
|
|
44
39
|
bot.onText(/\/echo (.+)/, (msg, match) => {
|
|
45
|
-
|
|
46
|
-
// 'match' is the result of executing the regexp above on the text content
|
|
47
|
-
// of the message
|
|
48
|
-
|
|
49
|
-
const chatId = msg.chat.id;
|
|
50
|
-
const resp = match[1]; // the captured "whatever"
|
|
51
|
-
|
|
52
|
-
// send back the matched "whatever" to the chat
|
|
53
|
-
bot.sendMessage(chatId, resp);
|
|
40
|
+
bot.sendMessage(msg.chat.id, match[1]);
|
|
54
41
|
});
|
|
55
42
|
|
|
56
|
-
// Listen for any kind of message. There are different kinds of
|
|
57
|
-
// messages.
|
|
58
43
|
bot.on('message', (msg) => {
|
|
59
|
-
|
|
44
|
+
console.log(msg.chat.id, msg.text);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Sending files
|
|
49
|
+
|
|
50
|
+
`sendPhoto`, `sendDocument`, `sendAudio`, ... accept a **local path**, a
|
|
51
|
+
**stream**, a **Buffer**, a **URL**, or a **`file_id`**:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
await bot.sendPhoto(chatId, './cat.png'); // path
|
|
55
|
+
await bot.sendPhoto(chatId, buffer); // Buffer (type auto-detected)
|
|
56
|
+
await bot.sendPhoto(chatId, 'https://x/y.png'); // URL
|
|
57
|
+
await bot.sendPhoto(chatId, fileId); // previously uploaded file
|
|
58
|
+
```
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
### Webhooks
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const bot = new TelegramBot(token, {
|
|
64
|
+
webHook: { port: 8443, host: '0.0.0.0', healthEndpoint: '/healthz' },
|
|
63
65
|
});
|
|
66
|
+
|
|
67
|
+
bot.on('message', (msg) => bot.sendMessage(msg.chat.id, 'hi'));
|
|
68
|
+
await bot.setWebHook('https://example.com:8443');
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
### Error handling
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* [Help Information][help]
|
|
72
|
-
* API Reference: ([api-release](../master/doc/api.md) / [development][api-dev] / [experimental][api-experimental])
|
|
73
|
-
* [Contributing to the Project][contributing]
|
|
74
|
-
* [Experimental Features][experimental]
|
|
73
|
+
All API errors are `errors.TelegramError` (code `ETELEGRAM`) and carry the raw
|
|
74
|
+
server `response`. Transport failures are `errors.FatalError` (code `EFATAL`)
|
|
75
|
+
with the original error preserved as `.cause`.
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Experimental features reside on the **experimental** branch._
|
|
77
|
+
```js
|
|
78
|
+
const { TelegramError } = require('@zero-bot.net/tg-bot-api');
|
|
79
79
|
|
|
80
|
+
bot.on('polling_error', (err) => {
|
|
81
|
+
if (err instanceof TelegramError) console.error(err.response.body);
|
|
82
|
+
else console.error(err.message, err.cause);
|
|
83
|
+
});
|
|
84
|
+
```
|
|
80
85
|
|
|
81
|
-
##
|
|
86
|
+
## ⚡ Performance & latency
|
|
87
|
+
|
|
88
|
+
Warm (keep-alive) requests take **20–80ms**. Cold connections pay DNS + TCP +
|
|
89
|
+
TLS **plus Node's 250ms IPv6 fallback window** — that is what pushes a request
|
|
90
|
+
to 200–600ms. Ranked fixes:
|
|
91
|
+
|
|
92
|
+
1. **Host near Telegram** (EU/US) — RTT drops from ~200ms to ~10–30ms. No code.
|
|
93
|
+
2. **Self-hosted Bot API server** — biggest win for media; files never leave
|
|
94
|
+
your machine:
|
|
95
|
+
```js
|
|
96
|
+
const bot = new TelegramBot(token, { baseApiUrl: 'http://127.0.0.1:8081' });
|
|
97
|
+
```
|
|
98
|
+
3. **Remove the IPv6 fallback penalty** (up to −250ms per connection):
|
|
99
|
+
```js
|
|
100
|
+
TelegramBot.applyNetworkTuning(); // ipv4-first + 100ms fallback window
|
|
101
|
+
// or: new TelegramBot(token, { ipv4First: true });
|
|
102
|
+
```
|
|
103
|
+
4. **Pre-warm the connection** so the first request is not a cold start:
|
|
104
|
+
```js
|
|
105
|
+
const bot = new TelegramBot(token, { prewarm: true });
|
|
106
|
+
// or: await bot.preheat();
|
|
107
|
+
```
|
|
108
|
+
5. **Tune polling** for instant updates:
|
|
109
|
+
`{ polling: { params: { timeout: 30 }, interval: 50 } }`.
|
|
110
|
+
6. **Fewer round-trips** — batch with `sendMediaGroup` / `forwardMessages` /
|
|
111
|
+
`copyMessages`, and reuse `file_id`s instead of re-uploading.
|
|
112
|
+
|
|
113
|
+
Keep-alive is enabled by default (`forever: true`); tune the pool if needed:
|
|
82
114
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
```js
|
|
116
|
+
new TelegramBot(token, {
|
|
117
|
+
request: {
|
|
118
|
+
agentOptions: { keepAlive: true, keepAliveMsecs: 10000, maxSockets: 64 },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
86
122
|
|
|
87
|
-
|
|
88
|
-
the Project. Head over and subscribe!
|
|
123
|
+
## 📚 Documentation
|
|
89
124
|
|
|
90
|
-
|
|
125
|
+
- [`doc/usage.md`](doc/usage.md) — full usage guide
|
|
126
|
+
- [`doc/api.md`](doc/api.md) — API reference (generated from `src/telegram.js`)
|
|
127
|
+
- [`doc/help.md`](doc/help.md) — FAQs and common pitfalls
|
|
128
|
+
- [`examples/`](examples/) — runnable examples
|
|
91
129
|
|
|
92
|
-
|
|
130
|
+
## 🧪 Development
|
|
93
131
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
* [node-telegram-keyboard-wrapper](https://github.com/alexandercerutti/node-telegram-keyboard-wrapper): A wrapper to improve keyboards structures creation through a more easy-to-see way (supports Inline Keyboards, Reply Keyboard, Remove Keyboard and Force Reply)
|
|
100
|
-
* [beetube-bot](https://github.com/kodjunkie/beetube-bot): A telegram bot for music, videos, movies, EDM tracks, torrent downloads, files and more.
|
|
101
|
-
* [telegram-inline-calendar](https://github.com/VDS13/telegram-inline-calendar): Date and time picker and inline calendar for Node.js telegram bots.
|
|
102
|
-
* [telegram-captcha](https://github.com/VDS13/telegram-captcha): Telegram bot to protect Telegram groups from automatic bots.
|
|
132
|
+
```sh
|
|
133
|
+
npm test # offline mocha test suite
|
|
134
|
+
npm run lint # eslint
|
|
135
|
+
npm run doc # regenerate doc/api.md
|
|
136
|
+
```
|
|
103
137
|
|
|
138
|
+
Requires Node.js >= 18.
|
|
104
139
|
|
|
105
140
|
## 👥 Contributors
|
|
106
141
|
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
</a>
|
|
111
|
-
</p>
|
|
142
|
+
<a href="https://github.com/ZeroBot-net/tg-bot-api/graphs/contributors">
|
|
143
|
+
<img src="https://contrib.rocks/image?repo=ZeroBot-net/tg-bot-api" />
|
|
144
|
+
</a>
|
|
112
145
|
|
|
113
146
|
## License
|
|
114
147
|
|
|
115
148
|
**The MIT License (MIT)**
|
|
116
149
|
|
|
117
|
-
Copyright © 2019 Yago
|
|
118
150
|
Copyright © 2026 Grandpa EJ
|
|
119
|
-
|
|
120
|
-
[usage]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/doc/usage.md
|
|
121
|
-
[examples]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/examples
|
|
122
|
-
[help]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/doc/help.md
|
|
123
|
-
[tutorials]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/doc/tutorials.md
|
|
124
|
-
[api-dev]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/doc/api.md
|
|
125
|
-
[api-release]:https://github.com/ZeroBot-net/tg-bot-api/tree/release/doc/api.md
|
|
126
|
-
[api-experimental]:https://github.com/ZeroBot-net/tg-bot-api/tree/experimental/doc/api.md
|
|
127
|
-
[contributing]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/CONTRIBUTING.md
|
|
128
|
-
[contributors]:https://github.com/ZeroBot-net/tg-bot-api/graphs/contributors
|
|
129
|
-
[experimental]:https://github.com/ZeroBot-net/tg-bot-api/tree/master/doc/experimental.md
|
|
130
|
-
[tg-channel]:https://telegram.me/node_telegram_bot_api
|
|
131
|
-
[tg-group]:https://t.me/+nc3A9Hs1S81mYzdk
|
package/index.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @zero-bot.net/tg-bot-api
|
|
3
|
+
*
|
|
4
|
+
* CommonJS entry point. The library ships native ES2020 source (no build or
|
|
5
|
+
* transpile step), so this simply re-exports it.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
-
if (majorVersion <= 5) {
|
|
8
|
-
const deprecate = require('./src/utils').deprecate;
|
|
9
|
-
deprecate('Node.js v5.x and below will no longer be supported in the future');
|
|
10
|
-
module.exports = require('./lib/telegram');
|
|
11
|
-
} else {
|
|
12
|
-
module.exports = require('./src/telegram');
|
|
13
|
-
}
|
|
7
|
+
module.exports = require('./src/telegram');
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zero-bot.net/tg-bot-api",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Telegram Bot API",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Lightweight, dependency-light Telegram Bot API library for Node.js",
|
|
5
5
|
"main": "./index.js",
|
|
6
|
-
"types": "./
|
|
6
|
+
"types": "./src/telegram.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"src/"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
7
15
|
"directories": {
|
|
8
16
|
"example": "examples",
|
|
9
17
|
"test": "test"
|
|
@@ -17,60 +25,34 @@
|
|
|
17
25
|
"zero-bot.net"
|
|
18
26
|
],
|
|
19
27
|
"scripts": {
|
|
20
|
-
"gen-doc": "echo 'WARNING: `npm run gen-doc` is deprecated. Use `npm run doc` instead.' && npm run doc",
|
|
21
28
|
"doc": "jsdoc2md --files src/telegram.js --template doc/api.hbs > doc/api.md",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"pretest": "npm run build",
|
|
27
|
-
"test": "npm run eslint && istanbul cover ./node_modules/mocha/bin/_mocha"
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"test": "mocha",
|
|
31
|
+
"test:watch": "mocha --watch",
|
|
32
|
+
"prepublishOnly": "npm run lint && npm test && npm run doc"
|
|
28
33
|
},
|
|
29
34
|
"author": "ZeroBot",
|
|
30
35
|
"license": "MIT",
|
|
31
36
|
"engines": {
|
|
32
|
-
"node": ">=
|
|
37
|
+
"node": ">=18"
|
|
33
38
|
},
|
|
34
39
|
"dependencies": {
|
|
35
40
|
"@zero-bot.net/request": "^1.2.1",
|
|
36
|
-
"array.prototype.findindex": "^2.2.4",
|
|
37
|
-
"bl": "^7.0.12",
|
|
38
41
|
"debug": "^4.4.3",
|
|
39
42
|
"eventemitter3": "^5.0.4",
|
|
40
|
-
"file-type": "^22.1.0",
|
|
41
|
-
"mime": "^4.1.0",
|
|
42
43
|
"pump": "^3.0.4"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"babel-cli": "^6.26.0",
|
|
46
|
-
"babel-eslint": "^10.1.0",
|
|
47
|
-
"babel-plugin-transform-class-properties": "^6.24.1",
|
|
48
|
-
"babel-plugin-transform-es2015-destructuring": "^6.23.0",
|
|
49
|
-
"babel-plugin-transform-es2015-parameters": "^6.24.1",
|
|
50
|
-
"babel-plugin-transform-es2015-shorthand-properties": "^6.24.1",
|
|
51
|
-
"babel-plugin-transform-es2015-spread": "^6.22.0",
|
|
52
|
-
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
|
53
|
-
"babel-plugin-transform-strict-mode": "^6.24.1",
|
|
54
|
-
"babel-preset-es2015": "^6.24.1",
|
|
55
|
-
"babel-register": "^6.26.0",
|
|
56
|
-
"concat-stream": "^2.0.0",
|
|
57
46
|
"eslint": "^10.10.0",
|
|
58
|
-
"eslint-config-airbnb": "^19.0.4",
|
|
59
|
-
"eslint-plugin-mocha": "^12.0.2",
|
|
60
|
-
"is": "^3.3.2",
|
|
61
|
-
"is-ci": "^4.1.0",
|
|
62
|
-
"istanbul": "^1.1.0-alpha.1",
|
|
63
47
|
"jsdoc-to-markdown": "^9.1.3",
|
|
64
|
-
"mocha": "^12.0.1"
|
|
65
|
-
"mocha-lcov-reporter": "^1.3.0",
|
|
66
|
-
"node-static": "^0.7.11"
|
|
48
|
+
"mocha": "^12.0.1"
|
|
67
49
|
},
|
|
68
50
|
"repository": {
|
|
69
51
|
"type": "git",
|
|
70
|
-
"url": "https://github.com/ZeroBot-net/tg-bot-api.git"
|
|
52
|
+
"url": "git+https://github.com/ZeroBot-net/tg-bot-api.git"
|
|
71
53
|
},
|
|
72
54
|
"bugs": {
|
|
73
55
|
"url": "https://github.com/ZeroBot-net/tg-bot-api/issues"
|
|
74
56
|
},
|
|
75
57
|
"homepage": "https://github.com/ZeroBot-net/tg-bot-api"
|
|
76
|
-
}
|
|
58
|
+
}
|
package/src/errors.js
CHANGED
|
@@ -19,6 +19,33 @@ exports.BaseError = class BaseError extends Error {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Build a readable message from an arbitrary thrown value. Handles the common
|
|
24
|
+
* case where a non-`Error` or an `AggregateError` has an empty `message`, which
|
|
25
|
+
* previously produced a blank `EFATAL:` error.
|
|
26
|
+
* @private
|
|
27
|
+
* @param {*} data
|
|
28
|
+
* @return {String}
|
|
29
|
+
*/
|
|
30
|
+
function describeError(data) {
|
|
31
|
+
if (typeof data === 'string') return data;
|
|
32
|
+
if (data === null || data === undefined) return String(data);
|
|
33
|
+
if (data instanceof Error) {
|
|
34
|
+
if (data.errors && data.errors.length) {
|
|
35
|
+
const nested = data.errors.map(describeError).join('; ');
|
|
36
|
+
return data.message ? `${data.message} (${nested})` : nested;
|
|
37
|
+
}
|
|
38
|
+
const label = data.code || data.name || 'Error';
|
|
39
|
+
return data.message ? `${label}: ${data.message}` : String(label);
|
|
40
|
+
}
|
|
41
|
+
if (typeof data.message === 'string' && data.message) return data.message;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(data);
|
|
44
|
+
} catch {
|
|
45
|
+
return String(data);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
exports.FatalError = class FatalError extends exports.BaseError {
|
|
23
50
|
/**
|
|
24
51
|
* Fatal Error. Error code is `"EFATAL"`.
|
|
@@ -28,9 +55,12 @@ exports.FatalError = class FatalError extends exports.BaseError {
|
|
|
28
55
|
*/
|
|
29
56
|
constructor(data) {
|
|
30
57
|
const error = (typeof data === 'string') ? null : data;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
58
|
+
super('EFATAL', describeError(data));
|
|
59
|
+
if (error) {
|
|
60
|
+
this.stack = error.stack || this.stack;
|
|
61
|
+
// Preserve the original error for programmatic inspection & logging.
|
|
62
|
+
this.cause = error;
|
|
63
|
+
}
|
|
34
64
|
}
|
|
35
65
|
};
|
|
36
66
|
|
package/src/fileTypes.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency file type detection and MIME lookup.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the `file-type` and `mime` packages so the library installs with a
|
|
5
|
+
* minimal, fully CommonJS dependency tree and no ESM/CJS interop surprises.
|
|
6
|
+
*
|
|
7
|
+
* @module fileTypes
|
|
8
|
+
* @private
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Magic-byte signatures for the file formats that can be uploaded through the
|
|
13
|
+
* Telegram Bot API. Each entry is `{ ext, mime, bytes? , ascii?, offset? }`.
|
|
14
|
+
*/
|
|
15
|
+
const SIGNATURES = [
|
|
16
|
+
{ bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], ext: 'png', mime: 'image/png' },
|
|
17
|
+
{ bytes: [0xff, 0xd8, 0xff], ext: 'jpg', mime: 'image/jpeg' },
|
|
18
|
+
{ ascii: 'GIF87a', ext: 'gif', mime: 'image/gif' },
|
|
19
|
+
{ ascii: 'GIF89a', ext: 'gif', mime: 'image/gif' },
|
|
20
|
+
{ ascii: 'RIFF', ext: 'webp', mime: 'image/webp', tail: { offset: 8, ascii: 'WEBP' } },
|
|
21
|
+
{ ascii: 'RIFF', ext: 'wav', mime: 'audio/wav', tail: { offset: 8, ascii: 'WAVE' } },
|
|
22
|
+
{ ascii: 'RIFF', ext: 'avi', mime: 'video/x-msvideo', tail: { offset: 8, ascii: 'AVI ' } },
|
|
23
|
+
{ bytes: [0x42, 0x4d], ext: 'bmp', mime: 'image/bmp' },
|
|
24
|
+
{ ascii: 'II*\u0000', ext: 'tiff', mime: 'image/tiff' },
|
|
25
|
+
{ ascii: 'MM\u0000*', ext: 'tiff', mime: 'image/tiff' },
|
|
26
|
+
{ ascii: '%PDF', ext: 'pdf', mime: 'application/pdf' },
|
|
27
|
+
{ bytes: [0x50, 0x4b, 0x03, 0x04], ext: 'zip', mime: 'application/zip' },
|
|
28
|
+
{ bytes: [0x1f, 0x8b], ext: 'gz', mime: 'application/gzip' },
|
|
29
|
+
{ bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07], ext: 'rar', mime: 'application/x-rar-compressed' },
|
|
30
|
+
{ bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c], ext: '7z', mime: 'application/x-7z-compressed' },
|
|
31
|
+
{ ascii: 'ID3', ext: 'mp3', mime: 'audio/mpeg' },
|
|
32
|
+
{ bytes: [0xff, 0xfb], ext: 'mp3', mime: 'audio/mpeg' },
|
|
33
|
+
{ bytes: [0xff, 0xf3], ext: 'mp3', mime: 'audio/mpeg' },
|
|
34
|
+
{ bytes: [0xff, 0xf2], ext: 'mp3', mime: 'audio/mpeg' },
|
|
35
|
+
{ ascii: 'OggS', ext: 'ogg', mime: 'audio/ogg' },
|
|
36
|
+
{ ascii: 'fLaC', ext: 'flac', mime: 'audio/flac' },
|
|
37
|
+
{ bytes: [0x1a, 0x45, 0xdf, 0xa3], ext: 'webm', mime: 'video/webm' },
|
|
38
|
+
{ ascii: 'MThd', ext: 'mid', mime: 'audio/midi' },
|
|
39
|
+
{ ascii: '{\\rtf', ext: 'rtf', mime: 'application/rtf' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** ISO-BMFF brands (`ftyp` box) mapped to a container extension + MIME type. */
|
|
43
|
+
const FTYP_BRANDS = {
|
|
44
|
+
'M4A ': { ext: 'm4a', mime: 'audio/mp4' },
|
|
45
|
+
'M4V ': { ext: 'm4v', mime: 'video/x-m4v' },
|
|
46
|
+
'qt ': { ext: 'mov', mime: 'video/quicktime' },
|
|
47
|
+
'3gp4': { ext: '3gp', mime: 'video/3gpp' },
|
|
48
|
+
'3gp5': { ext: '3gp', mime: 'video/3gpp' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Common filename extensions mapped to their MIME type. Used when an explicit
|
|
53
|
+
* content type is not provided for a path/stream upload.
|
|
54
|
+
*/
|
|
55
|
+
const EXTENSION_MIME = {
|
|
56
|
+
png: 'image/png',
|
|
57
|
+
jpg: 'image/jpeg',
|
|
58
|
+
jpeg: 'image/jpeg',
|
|
59
|
+
gif: 'image/gif',
|
|
60
|
+
webp: 'image/webp',
|
|
61
|
+
bmp: 'image/bmp',
|
|
62
|
+
tiff: 'image/tiff',
|
|
63
|
+
tif: 'image/tiff',
|
|
64
|
+
svg: 'image/svg+xml',
|
|
65
|
+
heic: 'image/heic',
|
|
66
|
+
ico: 'image/x-icon',
|
|
67
|
+
mp3: 'audio/mpeg',
|
|
68
|
+
m4a: 'audio/mp4',
|
|
69
|
+
aac: 'audio/aac',
|
|
70
|
+
ogg: 'audio/ogg',
|
|
71
|
+
oga: 'audio/ogg',
|
|
72
|
+
opus: 'audio/opus',
|
|
73
|
+
wav: 'audio/wav',
|
|
74
|
+
flac: 'audio/flac',
|
|
75
|
+
mp4: 'video/mp4',
|
|
76
|
+
m4v: 'video/x-m4v',
|
|
77
|
+
mov: 'video/quicktime',
|
|
78
|
+
avi: 'video/x-msvideo',
|
|
79
|
+
webm: 'video/webm',
|
|
80
|
+
mkv: 'video/x-matroska',
|
|
81
|
+
'3gp': 'video/3gpp',
|
|
82
|
+
pdf: 'application/pdf',
|
|
83
|
+
zip: 'application/zip',
|
|
84
|
+
gz: 'application/gzip',
|
|
85
|
+
tgz: 'application/gzip',
|
|
86
|
+
tar: 'application/x-tar',
|
|
87
|
+
rar: 'application/x-rar-compressed',
|
|
88
|
+
'7z': 'application/x-7z-compressed',
|
|
89
|
+
txt: 'text/plain',
|
|
90
|
+
csv: 'text/csv',
|
|
91
|
+
html: 'text/html',
|
|
92
|
+
htm: 'text/html',
|
|
93
|
+
css: 'text/css',
|
|
94
|
+
js: 'application/javascript',
|
|
95
|
+
json: 'application/json',
|
|
96
|
+
xml: 'application/xml',
|
|
97
|
+
md: 'text/markdown',
|
|
98
|
+
rtf: 'application/rtf',
|
|
99
|
+
doc: 'application/msword',
|
|
100
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
101
|
+
xls: 'application/vnd.ms-excel',
|
|
102
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
103
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
104
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
105
|
+
epub: 'application/epub+zip',
|
|
106
|
+
apk: 'application/vnd.android.package-archive',
|
|
107
|
+
tgs: 'application/x-tgsticker',
|
|
108
|
+
ttf: 'font/ttf',
|
|
109
|
+
otf: 'font/otf',
|
|
110
|
+
woff: 'font/woff',
|
|
111
|
+
woff2: 'font/woff2',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compare a byte sequence against the buffer at the given offset.
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
function matchesBytes(buffer, bytes, offset = 0) {
|
|
119
|
+
if (buffer.length < offset + bytes.length) return false;
|
|
120
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
121
|
+
if (buffer[offset + i] !== bytes[i]) return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compare an ASCII sequence against the buffer at the given offset.
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
function matchesAscii(buffer, ascii, offset = 0) {
|
|
131
|
+
if (buffer.length < offset + ascii.length) return false;
|
|
132
|
+
for (let i = 0; i < ascii.length; i += 1) {
|
|
133
|
+
if (buffer[offset + i] !== ascii.charCodeAt(i)) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Detect the file type of a Buffer from its magic bytes.
|
|
140
|
+
*
|
|
141
|
+
* @param {Buffer} buffer Data to inspect
|
|
142
|
+
* @return {Object|null} `{ ext, mime }` when recognized, otherwise `null`
|
|
143
|
+
*/
|
|
144
|
+
function detectFileType(buffer) {
|
|
145
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 3) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const sig of SIGNATURES) {
|
|
150
|
+
const headMatches = sig.bytes
|
|
151
|
+
? matchesBytes(buffer, sig.bytes)
|
|
152
|
+
: matchesAscii(buffer, sig.ascii);
|
|
153
|
+
|
|
154
|
+
if (!headMatches) continue;
|
|
155
|
+
|
|
156
|
+
if (sig.tail && !matchesAscii(buffer, sig.tail.ascii, sig.tail.offset)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { ext: sig.ext, mime: sig.mime };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ISO Base Media File Format (mp4/m4a/mov/3gp) — 'ftyp' box at offset 4.
|
|
164
|
+
if (buffer.length >= 12 && matchesAscii(buffer, 'ftyp', 4)) {
|
|
165
|
+
const brand = buffer.toString('ascii', 8, 12);
|
|
166
|
+
return FTYP_BRANDS[brand] || { ext: 'mp4', mime: 'video/mp4' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Look up a MIME type by filename or extension. Falls back to
|
|
174
|
+
* `application/octet-stream` for unknown extensions.
|
|
175
|
+
*
|
|
176
|
+
* @param {String} filename File name or extension (with or without a dot)
|
|
177
|
+
* @return {String} MIME type
|
|
178
|
+
*/
|
|
179
|
+
function lookupMime(filename) {
|
|
180
|
+
if (!filename || typeof filename !== 'string') {
|
|
181
|
+
return 'application/octet-stream';
|
|
182
|
+
}
|
|
183
|
+
const match = /\.([a-z0-9]+)$/i.exec(filename);
|
|
184
|
+
const ext = (match ? match[1] : filename).toLowerCase();
|
|
185
|
+
return EXTENSION_MIME[ext] || 'application/octet-stream';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { detectFileType, lookupMime };
|
package/src/network.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional, opt-in network tuning that removes the most common latency
|
|
3
|
+
* penalties when talking to the Telegram Bot API over a fresh connection.
|
|
4
|
+
*
|
|
5
|
+
* The biggest offender is Node's "happy eyeballs" behaviour: on networks with
|
|
6
|
+
* broken or absent IPv6 it waits `autoSelectFamilyAttemptTimeout` (250ms by
|
|
7
|
+
* default) before falling back to IPv4 — on *every* new connection. Preferring
|
|
8
|
+
* IPv4 and shortening that window can cut 100–250ms off cold requests.
|
|
9
|
+
*
|
|
10
|
+
* These helpers mutate process-global Node settings, so they are never applied
|
|
11
|
+
* automatically — call `applyNetworkTuning()` once at startup, or pass
|
|
12
|
+
* `{ ipv4First: true }` to the `TelegramBot` constructor.
|
|
13
|
+
*
|
|
14
|
+
* @module network
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Apply latency-oriented network defaults.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} [options]
|
|
21
|
+
* @param {Boolean} [options.ipv4First=true] Prefer IPv4 when resolving hosts
|
|
22
|
+
* (`dns.setDefaultResultOrder('ipv4first')`).
|
|
23
|
+
* @param {Number} [options.autoSelectFamilyAttemptTimeout=100] Milliseconds to
|
|
24
|
+
* wait for an IPv6 connection before falling back to IPv4 (Node default: 250).
|
|
25
|
+
* @return {Object} The applied settings.
|
|
26
|
+
*/
|
|
27
|
+
function applyNetworkTuning(options = {}) {
|
|
28
|
+
const ipv4First = options.ipv4First !== false;
|
|
29
|
+
const autoSelectFamilyAttemptTimeout = typeof options.autoSelectFamilyAttemptTimeout === 'number'
|
|
30
|
+
? options.autoSelectFamilyAttemptTimeout
|
|
31
|
+
: 100;
|
|
32
|
+
|
|
33
|
+
const dns = require('dns');
|
|
34
|
+
const net = require('net');
|
|
35
|
+
|
|
36
|
+
if (ipv4First && typeof dns.setDefaultResultOrder === 'function') {
|
|
37
|
+
dns.setDefaultResultOrder('ipv4first');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof net.setDefaultAutoSelectFamilyAttemptTimeout === 'function') {
|
|
41
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(autoSelectFamilyAttemptTimeout);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ipv4First, autoSelectFamilyAttemptTimeout };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { applyNetworkTuning };
|