@nitra/telegram 1.5.4 → 1.7.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 +87 -0
- package/package.json +12 -12
- package/src/index.js +88 -3
package/README.md
CHANGED
|
@@ -1 +1,88 @@
|
|
|
1
1
|
# @nitra/telegram
|
|
2
|
+
|
|
3
|
+
Мінімальний хелпер для надсилання повідомлень і документів у Telegram.
|
|
4
|
+
|
|
5
|
+
## Встановлення
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @nitra/telegram
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Налаштування
|
|
12
|
+
|
|
13
|
+
Потрібні змінні середовища (перевіряються при імпорті через `@nitra/check-env`):
|
|
14
|
+
|
|
15
|
+
| Змінна | Опис |
|
|
16
|
+
| -------------------- | ---------------------------- |
|
|
17
|
+
| `TELEGRAM_BOT_TOKEN` | токен бота |
|
|
18
|
+
| `TELEGRAM_CHAT_ID` | id чату/каналу для відправки |
|
|
19
|
+
|
|
20
|
+
## Формат за замовчуванням
|
|
21
|
+
|
|
22
|
+
Дефолтний `parse_mode` — **MarkdownV2**. Telegram вимагає екранувати спецсимволи
|
|
23
|
+
`_ * [ ] ( ) ~ \` > # + - = | { } . !`— для динамічного контенту (тексти помилок,
|
|
24
|
+
змінні) використовуйте`escapeMarkdownV2()`:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { sendMessage, escapeMarkdownV2 } from '@nitra/telegram'
|
|
28
|
+
|
|
29
|
+
await sendMessage(`*Помилка:* ${escapeMarkdownV2(err.message)}`)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Якщо розмітка все одно невалідна, повідомлення **не губиться** — бібліотека один раз
|
|
33
|
+
повторює запит без розмітки (plain text).
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `sendMessage(text, params?)`
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// MarkdownV2 (дефолт)
|
|
41
|
+
await sendMessage('*жирний* текст')
|
|
42
|
+
|
|
43
|
+
// HTML
|
|
44
|
+
await sendMessage('<b>жирний</b>', { parse_mode: 'HTML' })
|
|
45
|
+
|
|
46
|
+
// без розмітки (plain text)
|
|
47
|
+
await sendMessage('будь-який текст', { parse_mode: '' })
|
|
48
|
+
|
|
49
|
+
// без звуку
|
|
50
|
+
await sendMessage('тихо', { disable_notification: true })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`params`:
|
|
54
|
+
|
|
55
|
+
| Поле | Тип | За замовчуванням | Опис |
|
|
56
|
+
| ---------------------- | -------------------------------------------- | ---------------- | --------------------------------------- |
|
|
57
|
+
| `parse_mode` | `'MarkdownV2' \| 'Markdown' \| 'HTML' \| ''` | `'MarkdownV2'` | формат розмітки; `''`/`null` — вимкнути |
|
|
58
|
+
| `disable_notification` | `boolean` | — | надіслати без звуку |
|
|
59
|
+
|
|
60
|
+
> У робочі години (08:00–18:00) сповіщення зі звуком; поза ними — автоматично тихо.
|
|
61
|
+
> Повідомлення довші за 4096 символів обрізаються.
|
|
62
|
+
|
|
63
|
+
### `sendDocument(document, params?)`
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
await sendDocument(Buffer.from(csv), {
|
|
67
|
+
filename: 'report.csv',
|
|
68
|
+
contentType: 'text/csv',
|
|
69
|
+
caption: `*Звіт:* ${escapeMarkdownV2('users_2026.csv')}`
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`params`: `filename`, `contentType`, `caption`, `parse_mode` (дефолт MarkdownV2, лише
|
|
74
|
+
для `caption`), `disable_notification`. Як і в `sendMessage`, невалідна розмітка caption
|
|
75
|
+
не блокує відправку — повтор без розмітки.
|
|
76
|
+
|
|
77
|
+
### `escapeMarkdownV2(text)`
|
|
78
|
+
|
|
79
|
+
Екранує всі зарезервовані символи MarkdownV2. Застосовуйте до **динамічних** частин
|
|
80
|
+
(не до всього повідомлення — інакше зникне навмисна розмітка).
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
escapeMarkdownV2('a_b.c!') // → 'a\\_b\\.c\\!'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `DEFAULT_PARSE_MODE`
|
|
87
|
+
|
|
88
|
+
Константа з дефолтним форматом (`'MarkdownV2'`).
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/telegram",
|
|
3
|
+
"version": "1.7.0",
|
|
3
4
|
"description": "telegram helper",
|
|
4
|
-
"version": "1.5.4",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/index.js",
|
|
7
|
-
"files": [
|
|
8
|
-
"src"
|
|
9
|
-
],
|
|
10
|
-
"exports": {
|
|
11
|
-
".": "./src/index.js"
|
|
12
|
-
},
|
|
13
5
|
"keywords": [
|
|
14
6
|
"nitra",
|
|
15
7
|
"telegram"
|
|
16
8
|
],
|
|
9
|
+
"homepage": "https://github.com/nitra/telegram",
|
|
10
|
+
"bugs": "https://github.com/nitra/telegram/issues",
|
|
11
|
+
"license": "MIT",
|
|
17
12
|
"repository": {
|
|
18
13
|
"type": "git",
|
|
19
14
|
"url": "https://github.com/nitra/telegram.git"
|
|
20
15
|
},
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
"files": [
|
|
17
|
+
"src/index.js"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "src/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/index.js"
|
|
23
|
+
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@nitra/check-env": "^4.1.0",
|
|
26
26
|
"@nitra/pino": "^2.8.1"
|
package/src/index.js
CHANGED
|
@@ -6,20 +6,46 @@ checkEnv(['TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID'])
|
|
|
6
6
|
|
|
7
7
|
export const MAX_TELEGRAM_MSG_LENGTH = 4096
|
|
8
8
|
|
|
9
|
+
// Дефолтний формат повідомлень. Викликач може перевизначити через params.parse_mode
|
|
10
|
+
// ('HTML' | 'Markdown' | 'MarkdownV2'), або вимкнути розмітку ('' / null).
|
|
11
|
+
export const DEFAULT_PARSE_MODE = 'MarkdownV2'
|
|
12
|
+
|
|
13
|
+
// Екранує спецсимволи MarkdownV2. Застосовувати до ДИНАМІЧНОГО контенту (тексти
|
|
14
|
+
// помилок, змінні) — інакше Telegram падає з "can't parse entities". Навмисно не
|
|
15
|
+
// застосовується авто до всього тексту, бо це знищило б навмисну розмітку.
|
|
16
|
+
export const escapeMarkdownV2 = text => String(text).replaceAll(/[_*[\]()~`>#+\-=|{}.!\\]/g, String.raw`\$&`)
|
|
17
|
+
|
|
18
|
+
// Дефолт — MarkdownV2; явні '' / null / undefined → без розмітки (plain text).
|
|
19
|
+
const resolveParseMode = params => {
|
|
20
|
+
const raw = params && 'parse_mode' in params ? params.parse_mode : DEFAULT_PARSE_MODE
|
|
21
|
+
if (!raw) {
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
const known = { html: 'HTML', markdown: 'Markdown', markdownv2: 'MarkdownV2' }
|
|
25
|
+
return known[String(raw).toLowerCase()] ?? raw
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export const sendMessage = async (text, params) => {
|
|
10
29
|
const currentHour = new Date().getHours()
|
|
11
|
-
// if (process.env.TELEGRAM_ROUND_CLOCK || (currentHour >= 8 && currentHour <= 18)) {
|
|
12
30
|
// Max length of a Telegram message is 4096 characters
|
|
13
31
|
if (text.length >= MAX_TELEGRAM_MSG_LENGTH) {
|
|
14
32
|
text = text.slice(0, MAX_TELEGRAM_MSG_LENGTH)
|
|
15
33
|
}
|
|
16
34
|
|
|
35
|
+
const parseMode = resolveParseMode(params)
|
|
36
|
+
|
|
37
|
+
// Telegram HTML не підтримує <br> (і його варіанти </br>, <br/>) — конвертуємо
|
|
38
|
+
// у звичайний перенос рядка, інакше парсер падає з "can't parse entities".
|
|
39
|
+
if (parseMode === 'HTML') {
|
|
40
|
+
text = text.replaceAll(/<\/?br\s*\/?>/gi, '\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
let url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=${
|
|
18
44
|
env.TELEGRAM_CHAT_ID
|
|
19
45
|
}&text=${encodeURIComponent(text)}`
|
|
20
46
|
|
|
21
|
-
if (
|
|
22
|
-
url +=
|
|
47
|
+
if (parseMode) {
|
|
48
|
+
url += `&parse_mode=${parseMode}`
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
// Якщо в неробочий час або відключено сповіщення, то додаємо параметр disable_notification
|
|
@@ -38,7 +64,66 @@ export const sendMessage = async (text, params) => {
|
|
|
38
64
|
if (res.status >= 400) {
|
|
39
65
|
const data = await res.json()
|
|
40
66
|
|
|
67
|
+
// Якщо парсер розмітки не впорався з довільним текстом — повторюємо один раз
|
|
68
|
+
// як plain text (без parse_mode), щоб повідомлення гарантовано дійшло й не
|
|
69
|
+
// плодило каскад помилок. Повтор без розмітки вже не дасть "can't parse entities".
|
|
70
|
+
if (parseMode && /can't parse entities/i.test(data.description ?? '')) {
|
|
71
|
+
return sendMessage(text, { ...params, parse_mode: '' })
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
log.error(data.description, text)
|
|
42
75
|
return false
|
|
43
76
|
}
|
|
44
77
|
}
|
|
78
|
+
|
|
79
|
+
export const sendDocument = async (document, params = {}) => {
|
|
80
|
+
const currentHour = new Date().getHours()
|
|
81
|
+
const parseMode = resolveParseMode(params)
|
|
82
|
+
|
|
83
|
+
const formData = new FormData()
|
|
84
|
+
formData.append('chat_id', env.TELEGRAM_CHAT_ID)
|
|
85
|
+
|
|
86
|
+
// Додаємо документ як Blob з параметрами
|
|
87
|
+
const blob = new Blob([document], { type: params.contentType || 'application/octet-stream' })
|
|
88
|
+
formData.append('document', blob, params.filename || 'document.txt')
|
|
89
|
+
|
|
90
|
+
// Додаємо опціональні параметри. parse_mode стосується лише caption,
|
|
91
|
+
// тож додаємо його тільки коли є підпис.
|
|
92
|
+
if (params.caption) {
|
|
93
|
+
formData.append('caption', params.caption)
|
|
94
|
+
if (parseMode) {
|
|
95
|
+
formData.append('parse_mode', parseMode)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Якщо в неробочий час або відключено сповіщення, то додаємо параметр disable_notification
|
|
100
|
+
if (!(currentHour >= 8 && currentHour <= 18) || params?.disable_notification === true) {
|
|
101
|
+
formData.append('disable_notification', 'true')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendDocument`
|
|
105
|
+
|
|
106
|
+
let res
|
|
107
|
+
try {
|
|
108
|
+
res = await fetch(url, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: formData
|
|
111
|
+
})
|
|
112
|
+
} catch (error) {
|
|
113
|
+
log.error(error)
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (res.status >= 400) {
|
|
118
|
+
const data = await res.json()
|
|
119
|
+
|
|
120
|
+
// Як і в sendMessage — невалідна розмітка caption не має блокувати відправку
|
|
121
|
+
// документа: повторюємо один раз без parse_mode.
|
|
122
|
+
if (parseMode && params.caption && /can't parse entities/i.test(data.description ?? '')) {
|
|
123
|
+
return sendDocument(document, { ...params, parse_mode: '' })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log.error(data.description)
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
}
|