@nitra/telegram 1.7.0 → 1.8.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.
Files changed (3) hide show
  1. package/README.md +22 -10
  2. package/package.json +1 -1
  3. package/src/index.js +48 -81
package/README.md CHANGED
@@ -12,10 +12,11 @@ bun add @nitra/telegram
12
12
 
13
13
  Потрібні змінні середовища (перевіряються при імпорті через `@nitra/check-env`):
14
14
 
15
- | Змінна | Опис |
16
- | -------------------- | ---------------------------- |
17
- | `TELEGRAM_BOT_TOKEN` | токен бота |
18
- | `TELEGRAM_CHAT_ID` | id чату/каналу для відправки |
15
+ | Змінна | Опис |
16
+ | -------------------- | ------------------------------------------------- |
17
+ | `TELEGRAM_BOT_TOKEN` | токен бота |
18
+ | `TELEGRAM_CHAT_ID` | id чату/каналу для відправки |
19
+ | `TELEGRAM_THREAD_ID` | id топіка в супергрупі (опційно, дефолтний топік) |
19
20
 
20
21
  ## Формат за замовчуванням
21
22
 
@@ -48,14 +49,18 @@ await sendMessage('будь-який текст', { parse_mode: '' })
48
49
 
49
50
  // без звуку
50
51
  await sendMessage('тихо', { disable_notification: true })
52
+
53
+ // у топік супергрупи
54
+ await sendMessage('повідомлення в топік', { message_thread_id: 123 })
51
55
  ```
52
56
 
53
57
  `params`:
54
58
 
55
- | Поле | Тип | За замовчуванням | Опис |
56
- | ---------------------- | -------------------------------------------- | ---------------- | --------------------------------------- |
57
- | `parse_mode` | `'MarkdownV2' \| 'Markdown' \| 'HTML' \| ''` | `'MarkdownV2'` | формат розмітки; `''`/`null` — вимкнути |
58
- | `disable_notification` | `boolean` | | надіслати без звуку |
59
+ | Поле | Тип | За замовчуванням | Опис |
60
+ | ---------------------- | -------------------------------- | ---------------- | --------------------------------------- |
61
+ | `parse_mode` | `'MarkdownV2' \| 'HTML' \| ''` | `'MarkdownV2'` | формат розмітки; `''`/`null` — вимкнути |
62
+ | `message_thread_id` | `number` | `TELEGRAM_THREAD_ID` | id топіка; override для env змінної |
63
+ | `disable_notification` | `boolean` | — | надіслати без звуку |
59
64
 
60
65
  > У робочі години (08:00–18:00) сповіщення зі звуком; поза ними — автоматично тихо.
61
66
  > Повідомлення довші за 4096 символів обрізаються.
@@ -68,11 +73,18 @@ await sendDocument(Buffer.from(csv), {
68
73
  contentType: 'text/csv',
69
74
  caption: `*Звіт:* ${escapeMarkdownV2('users_2026.csv')}`
70
75
  })
76
+
77
+ // у топік супергрупи
78
+ await sendDocument(Buffer.from(csv), {
79
+ filename: 'report.csv',
80
+ contentType: 'text/csv',
81
+ message_thread_id: 123
82
+ })
71
83
  ```
72
84
 
73
85
  `params`: `filename`, `contentType`, `caption`, `parse_mode` (дефолт MarkdownV2, лише
74
- для `caption`), `disable_notification`. Як і в `sendMessage`, невалідна розмітка caption
75
- не блокує відправку — повтор без розмітки.
86
+ для `caption`), `message_thread_id`, `disable_notification`. Як і в `sendMessage`,
87
+ невалідна розмітка caption не блокує відправку — повтор без розмітки.
76
88
 
77
89
  ### `escapeMarkdownV2(text)`
78
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/telegram",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "telegram helper",
5
5
  "keywords": [
6
6
  "nitra",
package/src/index.js CHANGED
@@ -6,56 +6,26 @@ 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
9
  export const DEFAULT_PARSE_MODE = 'MarkdownV2'
12
10
 
13
11
  // Екранує спецсимволи MarkdownV2. Застосовувати до ДИНАМІЧНОГО контенту (тексти
14
- // помилок, змінні) — інакше Telegram падає з "can't parse entities". Навмисно не
15
- // застосовується авто до всього тексту, бо це знищило б навмисну розмітку.
12
+ // помилок, змінні) — інакше Telegram падає з "can't parse entities".
16
13
  export const escapeMarkdownV2 = text => String(text).replaceAll(/[_*[\]()~`>#+\-=|{}.!\\]/g, String.raw`\$&`)
17
14
 
18
15
  // Дефолт — MarkdownV2; явні '' / null / undefined → без розмітки (plain text).
19
16
  const resolveParseMode = params => {
20
17
  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' }
18
+ if (!raw) return ''
19
+ const known = { html: 'HTML', markdownv2: 'MarkdownV2' }
25
20
  return known[String(raw).toLowerCase()] ?? raw
26
21
  }
27
22
 
28
- export const sendMessage = async (text, params) => {
29
- const currentHour = new Date().getHours()
30
- // Max length of a Telegram message is 4096 characters
31
- if (text.length >= MAX_TELEGRAM_MSG_LENGTH) {
32
- text = text.slice(0, MAX_TELEGRAM_MSG_LENGTH)
33
- }
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
-
43
- let url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=${
44
- env.TELEGRAM_CHAT_ID
45
- }&text=${encodeURIComponent(text)}`
46
-
47
- if (parseMode) {
48
- url += `&parse_mode=${parseMode}`
49
- }
50
-
51
- // Якщо в неробочий час або відключено сповіщення, то додаємо параметр disable_notification
52
- if (!(currentHour >= 8 && currentHour <= 18) || params?.disable_notification === true) {
53
- url += '&disable_notification=true'
54
- }
23
+ const isWorkingHour = () => { const h = new Date().getHours(); return h >= 8 && h <= 18 }
55
24
 
25
+ const telegramRequest = async (url, init, { params, parseMode, onParseError }) => {
56
26
  let res
57
27
  try {
58
- res = await fetch(url)
28
+ res = await fetch(url, init)
59
29
  } catch (error) {
60
30
  log.error(error)
61
31
  return false
@@ -63,67 +33,64 @@ export const sendMessage = async (text, params) => {
63
33
 
64
34
  if (res.status >= 400) {
65
35
  const data = await res.json()
66
-
67
- // Якщо парсер розмітки не впорався з довільним текстом — повторюємо один раз
68
- // як plain text (без parse_mode), щоб повідомлення гарантовано дійшло й не
69
- // плодило каскад помилок. Повтор без розмітки вже не дасть "can't parse entities".
70
36
  if (parseMode && /can't parse entities/i.test(data.description ?? '')) {
71
- return sendMessage(text, { ...params, parse_mode: '' })
37
+ return onParseError()
72
38
  }
73
-
74
- log.error(data.description, text)
39
+ log.error(data.description)
75
40
  return false
76
41
  }
77
42
  }
78
43
 
44
+ const resolveCommonParams = params => ({
45
+ threadId: params?.message_thread_id ?? env.TELEGRAM_THREAD_ID,
46
+ silent: !isWorkingHour() || params?.disable_notification === true,
47
+ })
48
+
49
+ export const sendMessage = async (text, params) => {
50
+ if (text.length >= MAX_TELEGRAM_MSG_LENGTH) {
51
+ text = text.slice(0, MAX_TELEGRAM_MSG_LENGTH)
52
+ }
53
+
54
+ const parseMode = resolveParseMode(params)
55
+ const { threadId, silent } = resolveCommonParams(params)
56
+
57
+ // Telegram HTML не підтримує <br> — конвертуємо у перенос рядка.
58
+ if (parseMode === 'HTML') {
59
+ text = text.replaceAll(/<\/?br\s*\/?>/gi, '\n')
60
+ }
61
+
62
+ let url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=${env.TELEGRAM_CHAT_ID}&text=${encodeURIComponent(text)}`
63
+ if (parseMode) url += `&parse_mode=${parseMode}`
64
+ if (threadId) url += `&message_thread_id=${threadId}`
65
+ if (silent) url += '&disable_notification=true'
66
+
67
+ return telegramRequest(url, undefined, {
68
+ params,
69
+ parseMode,
70
+ onParseError: () => sendMessage(text, { ...params, parse_mode: '' }),
71
+ })
72
+ }
73
+
79
74
  export const sendDocument = async (document, params = {}) => {
80
- const currentHour = new Date().getHours()
81
75
  const parseMode = resolveParseMode(params)
76
+ const { threadId, silent } = resolveCommonParams(params)
82
77
 
83
78
  const formData = new FormData()
84
79
  formData.append('chat_id', env.TELEGRAM_CHAT_ID)
80
+ formData.append('document', new Blob([document], { type: params.contentType || 'application/octet-stream' }), params.filename || 'document.txt')
85
81
 
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
82
  if (params.caption) {
93
83
  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')
84
+ if (parseMode) formData.append('parse_mode', parseMode)
102
85
  }
86
+ if (threadId) formData.append('message_thread_id', String(threadId))
87
+ if (silent) formData.append('disable_notification', 'true')
103
88
 
104
89
  const url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendDocument`
105
90
 
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
- }
91
+ return telegramRequest(url, { method: 'POST', body: formData }, {
92
+ params,
93
+ parseMode,
94
+ onParseError: () => sendDocument(document, { ...params, parse_mode: '' }),
95
+ })
129
96
  }