@ihazz/bitrix24 1.0.3 → 1.1.1

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/src/i18n.ts CHANGED
@@ -18,12 +18,12 @@ function resolve<T>(dict: Record<string, T>, lang: string | undefined): T {
18
18
  // ─── Media download failed ───────────────────────────────────────────────────
19
19
 
20
20
  const I18N_MEDIA_DOWNLOAD_FAILED: Record<string, (files: string) => string> = {
21
- en: (f) => `\u26a0\ufe0f Could not download file(s): ${f}.\n\nFile processing is currently only available for the primary user (webhook owner). This limitation will be removed in a future release.`,
22
- ru: (f) => `\u26a0\ufe0f \u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b(\u044b): ${f}.\n\n\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435, \u0447\u0442\u043e \u0431\u043e\u0442 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0432 \u043d\u0443\u0436\u043d\u043e\u043c \u0434\u0438\u0430\u043b\u043e\u0433\u0435 \u0438 \u0443 \u043d\u0435\u0433\u043e \u0435\u0441\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u044d\u0442\u043e\u043c\u0443 \u0444\u0430\u0439\u043b\u0443.`,
23
- de: (f) => `\u26a0\ufe0f Datei(en) konnten nicht heruntergeladen werden: ${f}.\n\nDateiverarbeitung ist derzeit nur f\u00fcr den Hauptbenutzer (Webhook-Besitzer) verf\u00fcgbar. Diese Einschr\u00e4nkung wird in einer zuk\u00fcnftigen Version behoben.`,
24
- es: (f) => `\u26a0\ufe0f No se pudo descargar el/los archivo(s): ${f}.\n\nEl procesamiento de archivos actualmente solo est\u00e1 disponible para el usuario principal (propietario del webhook). Esta limitaci\u00f3n se eliminar\u00e1 en una versi\u00f3n futura.`,
25
- fr: (f) => `\u26a0\ufe0f Impossible de t\u00e9l\u00e9charger le(s) fichier(s) : ${f}.\n\nLe traitement des fichiers est actuellement r\u00e9serv\u00e9 \u00e0 l'utilisateur principal (propri\u00e9taire du webhook). Cette limitation sera lev\u00e9e dans une prochaine version.`,
26
- pt: (f) => `\u26a0\ufe0f N\u00e3o foi poss\u00edvel baixar o(s) arquivo(s): ${f}.\n\nO processamento de arquivos est\u00e1 dispon\u00edvel apenas para o usu\u00e1rio principal (dono do webhook). Essa limita\u00e7\u00e3o ser\u00e1 removida em uma vers\u00e3o futura.`,
21
+ en: (f) => `⚠️ Could not download the file(s): ${f}.\n\nPlease check that the bot is in the required chat and has access to these files.`,
22
+ ru: (f) => `⚠️ Не удалось загрузить файлы: ${f}.\n\nПроверьте, что бот состоит в нужном чате и у него есть доступ к этим файлам.`,
23
+ de: (f) => `⚠️ Die Datei(en) konnten nicht heruntergeladen werden: ${f}.\n\nBitte prüfen Sie, ob der Bot im erforderlichen Chat ist und Zugriff auf diese Dateien hat.`,
24
+ es: (f) => `⚠️ No se pudo descargar el/los archivo(s): ${f}.\n\nCompruebe que el bot este en el chat necesario y tenga acceso a estos archivos.`,
25
+ fr: (f) => `⚠️ Impossible de telecharger le ou les fichiers : ${f}.\n\nVerifiez que le bot se trouve dans le bon chat et qu il a acces a ces fichiers.`,
26
+ pt: (f) => `⚠️ Nao foi possivel baixar o(s) arquivo(s): ${f}.\n\nVerifique se o bot esta no chat correto e se tem acesso a esses arquivos.`,
27
27
  };
28
28
 
29
29
  export function mediaDownloadFailed(lang: string | undefined, fileNames: string): string {
@@ -33,12 +33,12 @@ export function mediaDownloadFailed(lang: string | undefined, fileNames: string)
33
33
  // ─── Group chat unsupported ──────────────────────────────────────────────────
34
34
 
35
35
  const I18N_GROUP_CHAT_UNSUPPORTED: Record<string, string> = {
36
- en: 'Sorry, I can only work in private messages. Please message me directly.',
37
- ru: '\u0418\u0437\u0432\u0438\u043d\u0438\u0442\u0435, \u044f \u0440\u0430\u0431\u043e\u0442\u0430\u044e \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u043b\u0438\u0447\u043d\u044b\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u0445. \u041d\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u043c\u043d\u0435 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.',
38
- de: 'Entschuldigung, ich arbeite nur in privaten Nachrichten. Bitte schreiben Sie mir direkt.',
39
- es: 'Lo siento, solo funciono en mensajes privados. Por favor, escr\u00edbeme directamente.',
40
- fr: 'D\u00e9sol\u00e9, je ne fonctionne qu\'en messages priv\u00e9s. Veuillez m\'\u00e9crire directement.',
41
- pt: 'Desculpe, s\u00f3 funciono em mensagens privadas. Por favor, escreva-me diretamente.',
36
+ en: 'I work only in a direct chat. Please message me there.',
37
+ ru: 'Я работаю только в личном чате. Напишите мне там.',
38
+ de: 'Ich arbeite nur in einem direkten Chat. Bitte schreiben Sie mir dort.',
39
+ es: 'Solo trabajo en un chat personal. Escribame alli.',
40
+ fr: 'Je travaille uniquement dans un chat direct. Ecrivez-moi la-bas.',
41
+ pt: 'Eu trabalho apenas em um chat direto. Escreva para mim por la.',
42
42
  };
43
43
 
44
44
  export function groupChatUnsupported(lang: string | undefined): string {
@@ -48,55 +48,145 @@ export function groupChatUnsupported(lang: string | undefined): string {
48
48
  // ─── Personal bot access denied ──────────────────────────────────────────────
49
49
 
50
50
  const I18N_PERSONAL_BOT_OWNER_ONLY: Record<string, string> = {
51
- en: 'This bot is personal and accepts messages only from the bot owner.',
52
- ru: 'Этот бот персональный и принимает сообщения только от владельца бота.',
53
- de: 'Dieser Bot ist persoenlich und akzeptiert Nachrichten nur vom Bot-Besitzer.',
54
- es: 'Este bot es personal y solo acepta mensajes del propietario del bot.',
55
- fr: 'Ce bot est personnel et accepte uniquement les messages du proprietaire du bot.',
56
- pt: 'Este bot e pessoal e aceita mensagens apenas do proprietario do bot.',
51
+ en: 'This is a personal bot. Only the bot owner can message it.',
52
+ ru: 'Это персональный бот. Писать ему может только владелец.',
53
+ de: 'Dies ist ein persoenlicher Bot. Nur der Bot-Besitzer kann ihm schreiben.',
54
+ es: 'Este es un bot personal. Solo el propietario del bot puede escribirle.',
55
+ fr: 'Ce bot est personnel. Seul son proprietaire peut lui ecrire.',
56
+ pt: 'Este e um bot pessoal. Apenas o dono do bot pode enviar mensagens para ele.',
57
57
  };
58
58
 
59
59
  export function personalBotOwnerOnly(lang: string | undefined): string {
60
60
  return resolve(I18N_PERSONAL_BOT_OWNER_ONLY, lang);
61
61
  }
62
62
 
63
- // ─── Reply-to-message unsupported ───────────────────────────────────────────
63
+ const I18N_ACCESS_APPROVED: Record<string, string> = {
64
+ en: 'Access to the bot has been approved.',
65
+ ru: 'Доступ к боту подтвержден.',
66
+ de: 'Der Zugriff auf den Bot wurde bestaetigt.',
67
+ es: 'El acceso al bot ha sido aprobado.',
68
+ fr: 'L acces au bot a ete approuve.',
69
+ pt: 'O acesso ao bot foi aprovado.',
70
+ };
71
+
72
+ export function accessApproved(lang: string | undefined): string {
73
+ return resolve(I18N_ACCESS_APPROVED, lang);
74
+ }
75
+
76
+ const I18N_OWNER_AND_ALLOWED_USERS_ONLY: Record<string, string> = {
77
+ en: 'This bot is available only to the bot owner and users with approved access.',
78
+ ru: 'Этот бот доступен только владельцу и пользователям с подтвержденным доступом.',
79
+ de: 'Dieser Bot ist nur fuer den Bot-Besitzer und Benutzer mit bestaetigtem Zugriff verfuegbar.',
80
+ es: 'Este bot esta disponible solo para el propietario del bot y los usuarios con acceso confirmado.',
81
+ fr: 'Ce bot est disponible uniquement pour le proprietaire du bot et les utilisateurs disposant d un acces confirme.',
82
+ pt: 'Este bot esta disponivel apenas para o dono do bot e para usuarios com acesso confirmado.',
83
+ };
84
+
85
+ export function ownerAndAllowedUsersOnly(lang: string | undefined): string {
86
+ return resolve(I18N_OWNER_AND_ALLOWED_USERS_ONLY, lang);
87
+ }
64
88
 
65
- const I18N_REPLY_MESSAGE_UNSUPPORTED: Record<string, string> = {
66
- en: 'Replies to specific messages are not supported in the Bitrix24 integration yet. Please send your request as a new message for now. Support will appear in a future version.',
67
- ru: 'Ответы на конкретные сообщения в интеграции Bitrix24 пока не поддерживаются. Пока что отправьте запрос отдельным новым сообщением. Поддержка появится в будущих версиях.',
68
- de: 'Antworten auf bestimmte Nachrichten werden in der Bitrix24-Integration derzeit noch nicht unterstuetzt. Bitte senden Sie Ihre Anfrage vorerst als neue Nachricht. Die Unterstuetzung kommt in einer zukuenftigen Version.',
69
- es: 'Las respuestas a mensajes concretos todavia no son compatibles en la integracion con Bitrix24. Por ahora, envia tu solicitud como un mensaje nuevo. Esta compatibilidad llegara en una version futura.',
70
- fr: 'Les reponses a des messages precis ne sont pas encore prises en charge dans l integration Bitrix24. Pour le moment, envoyez votre demande comme un nouveau message. Cette prise en charge arrivera dans une prochaine version.',
71
- pt: 'Respostas a mensagens especificas ainda nao sao suportadas na integracao com o Bitrix24. Por enquanto, envie sua solicitacao como uma nova mensagem. Esse suporte chegara em uma versao futura.',
89
+ const I18N_ACCESS_DENIED: Record<string, string> = {
90
+ en: 'You do not have access to this bot.',
91
+ ru: 'У вас нет доступа к этому боту.',
92
+ de: 'Sie haben keinen Zugriff auf diesen Bot.',
93
+ es: 'No tienes acceso a este bot.',
94
+ fr: 'Vous n avez pas acces a ce bot.',
95
+ pt: 'Voce nao tem acesso a este bot.',
72
96
  };
73
97
 
74
- export function replyMessageUnsupported(lang: string | undefined): string {
75
- return resolve(I18N_REPLY_MESSAGE_UNSUPPORTED, lang);
98
+ export function accessDenied(lang: string | undefined): string {
99
+ return resolve(I18N_ACCESS_DENIED, lang);
76
100
  }
77
101
 
78
- const I18N_FORWARDED_MESSAGE_UNSUPPORTED: Record<string, string> = {
79
- en: 'Forwarded messages are not supported in the Bitrix24 integration yet. Please send the text directly or upload the file/image as a regular message. Support will appear in a future version.',
80
- ru: 'Пересланные сообщения в интеграции Bitrix24 пока не поддерживаются. Пока что отправьте текст напрямую или загрузите файл либо изображение обычным сообщением. Поддержка появится в будущих версиях.',
81
- de: 'Weitergeleitete Nachrichten werden in der Bitrix24-Integration derzeit noch nicht unterstuetzt. Bitte senden Sie den Text direkt oder laden Sie die Datei bzw. das Bild als normale Nachricht hoch. Die Unterstuetzung kommt in einer zukuenftigen Version.',
82
- es: 'Los mensajes reenviados todavia no son compatibles en la integracion con Bitrix24. Por ahora, envia el texto directamente o sube el archivo o la imagen como un mensaje normal. Esta compatibilidad llegara en una version futura.',
83
- fr: 'Les messages transferes ne sont pas encore pris en charge dans l integration Bitrix24. Pour le moment, envoyez le texte directement ou telechargez le fichier ou l image comme un message normal. Cette prise en charge arrivera dans une prochaine version.',
84
- pt: 'Mensagens encaminhadas ainda nao sao suportadas na integracao com o Bitrix24. Por enquanto, envie o texto diretamente ou carregue o arquivo ou a imagem como uma mensagem normal. Esse suporte chegara em uma versao futura.',
102
+ const I18N_GROUP_PAIRING_PENDING: Record<string, string> = {
103
+ en: 'To use this bot in group chats, access must be approved first. Please ask the bot owner to confirm your Bitrix24 pairing in OpenClaw.',
104
+ ru: 'Чтобы пользоваться ботом в групповых чатах, сначала нужно подтвердить доступ. Попросите владельца бота подтвердить вашу привязку Bitrix24 в OpenClaw.',
105
+ de: 'Um diesen Bot in Gruppenchats zu verwenden, muss der Zugriff zuerst bestaetigt werden. Bitten Sie den Bot-Besitzer, Ihr Bitrix24-Pairing in OpenClaw zu bestaetigen.',
106
+ es: 'Para usar este bot en chats grupales, primero hay que confirmar el acceso. Pida al propietario del bot que confirme su vinculacion de Bitrix24 en OpenClaw.',
107
+ fr: 'Pour utiliser ce bot dans les chats de groupe, l acces doit d abord etre confirme. Demandez au proprietaire du bot de confirmer votre association Bitrix24 dans OpenClaw.',
108
+ pt: 'Para usar este bot em chats em grupo, o acesso precisa ser confirmado primeiro. Peca ao dono do bot para confirmar seu pareamento do Bitrix24 no OpenClaw.',
85
109
  };
86
110
 
87
- export function forwardedMessageUnsupported(lang: string | undefined): string {
88
- return resolve(I18N_FORWARDED_MESSAGE_UNSUPPORTED, lang);
111
+ export function groupPairingPending(lang: string | undefined): string {
112
+ return resolve(I18N_GROUP_PAIRING_PENDING, lang);
113
+ }
114
+
115
+ const I18N_WATCH_OWNER_DM_NOTICE: Record<string, (params: {
116
+ chatRef: string;
117
+ topicsRef?: string;
118
+ sourceKind?: 'chat' | 'dm';
119
+ }) => string> = {
120
+ en: ({ chatRef, topicsRef, sourceKind }) => {
121
+ const topicsSuffix = topicsRef
122
+ ? ` Matched topics: ${topicsRef}`
123
+ : '';
124
+ return sourceKind === 'dm'
125
+ ? `A watch rule was triggered in a direct chat with ${chatRef}.${topicsSuffix}`
126
+ : `A watch rule was triggered in chat ${chatRef}.${topicsSuffix}`;
127
+ },
128
+ ru: ({ chatRef, topicsRef, sourceKind }) => {
129
+ const topicsSuffix = topicsRef
130
+ ? ` Совпавшие темы: ${topicsRef}`
131
+ : '';
132
+ return sourceKind === 'dm'
133
+ ? `Сработало правило отслеживания в личном чате с ${chatRef}.${topicsSuffix}`
134
+ : `Сработало правило отслеживания в чате ${chatRef}.${topicsSuffix}`;
135
+ },
136
+ de: ({ chatRef, topicsRef, sourceKind }) => {
137
+ const topicsSuffix = topicsRef
138
+ ? ` Zugeordnete Themen: ${topicsRef}`
139
+ : '';
140
+ return sourceKind === 'dm'
141
+ ? `Eine Beobachtungsregel wurde im Direktchat mit ${chatRef} ausgeloest.${topicsSuffix}`
142
+ : `Eine Beobachtungsregel wurde im Chat ${chatRef} ausgeloest.${topicsSuffix}`;
143
+ },
144
+ es: ({ chatRef, topicsRef, sourceKind }) => {
145
+ const topicsSuffix = topicsRef
146
+ ? ` Temas coincidentes: ${topicsRef}`
147
+ : '';
148
+ return sourceKind === 'dm'
149
+ ? `Se activo una regla de seguimiento en un chat directo con ${chatRef}.${topicsSuffix}`
150
+ : `Se activo una regla de seguimiento en el chat ${chatRef}.${topicsSuffix}`;
151
+ },
152
+ fr: ({ chatRef, topicsRef, sourceKind }) => {
153
+ const topicsSuffix = topicsRef
154
+ ? ` Sujets correspondants : ${topicsRef}`
155
+ : '';
156
+ return sourceKind === 'dm'
157
+ ? `Une regle de surveillance a ete declenchee dans un chat direct avec ${chatRef}.${topicsSuffix}`
158
+ : `Une regle de surveillance a ete declenchee dans le chat ${chatRef}.${topicsSuffix}`;
159
+ },
160
+ pt: ({ chatRef, topicsRef, sourceKind }) => {
161
+ const topicsSuffix = topicsRef
162
+ ? ` Topicos correspondentes: ${topicsRef}`
163
+ : '';
164
+ return sourceKind === 'dm'
165
+ ? `Uma regra de monitoramento foi acionada em um chat direto com ${chatRef}.${topicsSuffix}`
166
+ : `Uma regra de monitoramento foi acionada no chat ${chatRef}.${topicsSuffix}`;
167
+ },
168
+ };
169
+
170
+ export function watchOwnerDmNotice(
171
+ lang: string | undefined,
172
+ params: {
173
+ chatRef: string;
174
+ topicsRef?: string;
175
+ sourceKind?: 'chat' | 'dm';
176
+ },
177
+ ): string {
178
+ return resolve(I18N_WATCH_OWNER_DM_NOTICE, lang)(params);
89
179
  }
90
180
 
91
181
  // ─── Onboarding messages ─────────────────────────────────────────────────────
92
182
 
93
183
  const I18N_WELCOME: Record<string, (botName: string) => string> = {
94
- en: (n) => `${n} ready. Send me a message or pick a command below.`,
95
- ru: (n) => `${n} \u0433\u043e\u0442\u043e\u0432. \u041d\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u043c\u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u043d\u0438\u0436\u0435.`,
96
- de: (n) => `${n} bereit. Senden Sie mir eine Nachricht oder w\u00e4hlen Sie einen Befehl unten.`,
97
- es: (n) => `${n} listo. Env\u00edame un mensaje o elige un comando abajo.`,
98
- fr: (n) => `${n} pr\u00eat. Envoyez-moi un message ou choisissez une commande ci-dessous.`,
99
- pt: (n) => `${n} pronto. Envie-me uma mensagem ou escolha um comando abaixo.`,
184
+ en: (n) => `${n} is ready. Send a message or choose a command below.`,
185
+ ru: (n) => `${n} готов к работе. Напишите сообщение или выберите команду ниже.`,
186
+ de: (n) => `${n} ist bereit. Senden Sie eine Nachricht oder waehlen Sie unten einen Befehl aus.`,
187
+ es: (n) => `${n} esta listo. Envie un mensaje o elija un comando a continuacion.`,
188
+ fr: (n) => `${n} est pret. Envoyez un message ou choisissez une commande ci-dessous.`,
189
+ pt: (n) => `${n} esta pronto. Envie uma mensagem ou escolha um comando abaixo.`,
100
190
  };
101
191
 
102
192
  export function welcomeMessage(lang: string | undefined, botName: string): string {
@@ -104,12 +194,12 @@ export function welcomeMessage(lang: string | undefined, botName: string): strin
104
194
  }
105
195
 
106
196
  const I18N_PAIRING_WELCOME: Record<string, (botName: string) => string> = {
107
- en: (n) => `${n} ready. To start, send me any message and I will return a pairing code. Then approve it in OpenClaw with: openclaw pairing approve bitrix24 <CODE>`,
108
- ru: (n) => `${n} \u0433\u043e\u0442\u043e\u0432. \u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044c, \u043e\u0442\u043f\u0440\u0430\u0432\u044c\u0442\u0435 \u043c\u043d\u0435 \u043b\u044e\u0431\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u0438 \u044f \u0432\u0435\u0440\u043d\u0443 \u043a\u043e\u0434 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438. \u0417\u0430\u0442\u0435\u043c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u0432 OpenClaw \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439: openclaw pairing approve bitrix24 <CODE>`,
109
- de: (n) => `${n} ist bereit. Senden Sie mir eine beliebige Nachricht, dann gebe ich einen Pairing-Code zurueck. Bestaetigen Sie ihn danach in OpenClaw mit: openclaw pairing approve bitrix24 <CODE>`,
110
- es: (n) => `${n} esta listo. Enviame cualquier mensaje y te devolvere un codigo de vinculacion. Luego apruebalo en OpenClaw con: openclaw pairing approve bitrix24 <CODE>`,
111
- fr: (n) => `${n} est pret. Envoyez-moi n'importe quel message et je vous renverrai un code d'association. Ensuite, validez-le dans OpenClaw avec : openclaw pairing approve bitrix24 <CODE>`,
112
- pt: (n) => `${n} esta pronto. Envie-me qualquer mensagem e eu retornarei um codigo de pareamento. Depois, aprove-o no OpenClaw com: openclaw pairing approve bitrix24 <CODE>`,
197
+ en: (n) => `${n} is ready. Send any message, and I will return a pairing code. Then approve it in OpenClaw with: openclaw pairing approve bitrix24 <CODE>`,
198
+ ru: (n) => `${n} готов к работе. Отправьте любое сообщение, и я пришлю код привязки. Затем подтвердите его в OpenClaw командой: openclaw pairing approve bitrix24 <CODE>`,
199
+ de: (n) => `${n} ist bereit. Senden Sie eine beliebige Nachricht, und ich sende einen Pairing-Code. Bestaetigen Sie ihn danach in OpenClaw mit: openclaw pairing approve bitrix24 <CODE>`,
200
+ es: (n) => `${n} esta listo. Envie cualquier mensaje y recibira un codigo de vinculacion. Despues, confirmelo en OpenClaw con: openclaw pairing approve bitrix24 <CODE>`,
201
+ fr: (n) => `${n} est pret. Envoyez n importe quel message et je vous enverrai un code d association. Confirmez-le ensuite dans OpenClaw avec : openclaw pairing approve bitrix24 <CODE>`,
202
+ pt: (n) => `${n} esta pronto. Envie qualquer mensagem e eu enviarei um codigo de pareamento. Depois, confirme-o no OpenClaw com: openclaw pairing approve bitrix24 <CODE>`,
113
203
  };
114
204
 
115
205
  export function pairingWelcomeMessage(lang: string | undefined, botName: string): string {
@@ -119,7 +209,7 @@ export function pairingWelcomeMessage(lang: string | undefined, botName: string)
119
209
  export function onboardingMessage(
120
210
  lang: string | undefined,
121
211
  botName: string,
122
- dmPolicy: 'pairing' | 'webhookUser' | undefined,
212
+ dmPolicy: 'pairing' | 'webhookUser' | 'allowlist' | 'open' | undefined,
123
213
  ): string {
124
214
  return dmPolicy === 'pairing'
125
215
  ? pairingWelcomeMessage(lang, botName)
@@ -3,10 +3,12 @@ import type {
3
3
  B24V2FetchEventItem,
4
4
  B24V2WebhookEvent,
5
5
  B24V2MessageEventData,
6
+ B24V2UserMessageEventData,
6
7
  B24V2JoinChatEventData,
7
8
  B24V2CommandEventData,
8
9
  B24V2DeleteEventData,
9
10
  B24V2Message,
11
+ B24V2MessageParams,
10
12
  B24MsgContext,
11
13
  B24MediaItem,
12
14
  FetchContext,
@@ -24,6 +26,7 @@ export interface FetchCommandContext {
24
26
  commandText: string;
25
27
  senderId: string;
26
28
  dialogId: string;
29
+ chatId: string;
27
30
  chatType: string;
28
31
  messageId: string;
29
32
  language?: string;
@@ -32,7 +35,9 @@ export interface FetchCommandContext {
32
35
 
33
36
  /** Normalized fetch join chat context */
34
37
  export interface FetchJoinChatContext {
38
+ senderId: string;
35
39
  dialogId: string;
40
+ chatId: string;
36
41
  chatType: string;
37
42
  language?: string;
38
43
  fetchCtx: FetchContext;
@@ -81,7 +86,10 @@ export class InboundHandler {
81
86
 
82
87
  switch (eventType) {
83
88
  case 'ONIMBOTV2MESSAGEADD':
84
- return this.handleV2Message(item, fetchCtx);
89
+ return this.handleV2Message(item, fetchCtx, 'bot');
90
+
91
+ case 'ONIMV2MESSAGEADD':
92
+ return this.handleV2Message(item, fetchCtx, 'user');
85
93
 
86
94
  case 'ONIMBOTV2JOINCHAT':
87
95
  return this.handleV2JoinChat(item, fetchCtx);
@@ -97,6 +105,10 @@ export class InboundHandler {
97
105
  case 'ONIMBOTV2MESSAGEDELETE':
98
106
  case 'ONIMBOTV2CONTEXTGET':
99
107
  case 'ONIMBOTV2REACTIONCHANGE':
108
+ case 'ONIMV2MESSAGEUPDATE':
109
+ case 'ONIMV2MESSAGEDELETE':
110
+ case 'ONIMV2REACTIONCHANGE':
111
+ case 'ONIMV2JOINCHAT':
100
112
  this.logger.debug(`Fetch: skipping ${eventType} (not handled)`);
101
113
  return true;
102
114
 
@@ -113,8 +125,9 @@ export class InboundHandler {
113
125
  private async handleV2Message(
114
126
  item: B24V2FetchEventItem,
115
127
  fetchCtx: FetchContext,
128
+ eventScope: 'bot' | 'user',
116
129
  ): Promise<boolean> {
117
- const data = item.data as B24V2MessageEventData;
130
+ const data = item.data as B24V2MessageEventData | B24V2UserMessageEventData;
118
131
 
119
132
  // Runtime guard: ensure essential V2 data fields exist
120
133
  if (!data?.message || !data?.user) {
@@ -129,8 +142,9 @@ export class InboundHandler {
129
142
  }
130
143
  const messageId = String(rawMessageId);
131
144
 
132
- if (this.dedup.isDuplicate(messageId)) {
133
- this.logger.debug(`Fetch: duplicate message ${messageId}, skipping`);
145
+ const dedupKey = `${eventScope}:${messageId}`;
146
+ if (this.dedup.isDuplicate(dedupKey)) {
147
+ this.logger.debug(`Fetch: duplicate message ${messageId} for scope ${eventScope}, skipping`);
134
148
  return true;
135
149
  }
136
150
 
@@ -156,19 +170,29 @@ export class InboundHandler {
156
170
 
157
171
  const ctx: B24MsgContext = {
158
172
  channel: 'bitrix24',
173
+ eventScope,
159
174
  senderId: String(data.user?.id ?? dialogId),
160
175
  senderName: data.user?.name ?? dialogId,
161
176
  senderFirstName: data.user?.firstName,
162
177
  chatId: dialogId,
163
178
  chatInternalId: String(data.message?.chatId ?? dialogId),
179
+ chatName: data.chat?.name,
180
+ chatType: data.chat?.type,
164
181
  messageId,
165
182
  replyToMessageId: extractReplyToMessageId(data.message.params),
166
183
  isForwarded: isForwardedMessage(data.message.forward),
184
+ wasMentioned: eventScope === 'bot' && !isP2P && isMessageMentioningBot({
185
+ text: data.message?.text ?? '',
186
+ botId: fetchCtx.botId,
187
+ botName: this.config.botName,
188
+ botCode: this.config.botCode,
189
+ }),
167
190
  text: data.message?.text ?? '',
168
191
  isDm: isP2P,
169
192
  isGroup: !isP2P,
170
193
  media,
171
194
  language: data.language,
195
+ timestamp: parseEventTimestamp(data.message?.date ?? item.date),
172
196
  raw: item,
173
197
  botId: fetchCtx.botId,
174
198
  memberId: '',
@@ -193,7 +217,9 @@ export class InboundHandler {
193
217
  }
194
218
 
195
219
  const joinCtx: FetchJoinChatContext = {
220
+ senderId: String(data.user?.id ?? data.dialogId),
196
221
  dialogId: data.dialogId,
222
+ chatId: String(data.chat.id ?? data.dialogId),
197
223
  chatType: data.chat.type,
198
224
  language: data.language,
199
225
  fetchCtx,
@@ -246,6 +272,7 @@ export class InboundHandler {
246
272
  commandText,
247
273
  senderId: String(data.user?.id ?? dialogId),
248
274
  dialogId,
275
+ chatId: String(data.chat?.id ?? data.message?.chatId ?? dialogId),
249
276
  chatType: isP2P ? 'P' : String(data.chat?.type ?? ''),
250
277
  messageId: String(data.message?.id ?? item.eventId),
251
278
  language: data.language,
@@ -331,9 +358,19 @@ export class InboundHandler {
331
358
  * In V2, file info may come in params.FILE_ID array.
332
359
  * File details are available via imbot.v2.File.download.
333
360
  */
334
- function extractFilesFromParams(params: Record<string, unknown>): B24MediaItem[] {
361
+ function normalizeMessageParams(params: B24V2MessageParams | undefined): Record<string, unknown> {
362
+ if (!params || Array.isArray(params) || typeof params !== 'object') {
363
+ return {};
364
+ }
365
+
366
+ return params;
367
+ }
368
+
369
+ function extractFilesFromParams(params: B24V2MessageParams | undefined): B24MediaItem[] {
370
+ const normalizedParams = normalizeMessageParams(params);
371
+
335
372
  // V2 events may include file IDs in params (array or single value)
336
- const rawFileIds = params?.FILE_ID;
373
+ const rawFileIds = normalizedParams.FILE_ID;
337
374
  if (rawFileIds == null) return [];
338
375
  const fileIds = Array.isArray(rawFileIds) ? rawFileIds : [rawFileIds];
339
376
  if (!fileIds.length) return [];
@@ -347,8 +384,9 @@ function extractFilesFromParams(params: Record<string, unknown>): B24MediaItem[]
347
384
  }));
348
385
  }
349
386
 
350
- function extractReplyToMessageId(params: Record<string, unknown>): string | undefined {
351
- const rawReplyId = params?.REPLY_ID;
387
+ function extractReplyToMessageId(params: B24V2MessageParams | undefined): string | undefined {
388
+ const normalizedParams = normalizeMessageParams(params);
389
+ const rawReplyId = normalizedParams.REPLY_ID;
352
390
  if (rawReplyId == null) {
353
391
  return undefined;
354
392
  }
@@ -373,3 +411,48 @@ function isForwardedMessage(forward: unknown): boolean {
373
411
 
374
412
  return true;
375
413
  }
414
+
415
+ function parseEventTimestamp(value: string | null | undefined): number | undefined {
416
+ if (!value) {
417
+ return undefined;
418
+ }
419
+
420
+ const parsed = Date.parse(value);
421
+ return Number.isFinite(parsed) ? parsed : undefined;
422
+ }
423
+
424
+ function isMessageMentioningBot(params: {
425
+ text: string;
426
+ botId: number;
427
+ botName?: string;
428
+ botCode?: string;
429
+ }): boolean {
430
+ const text = params.text.trim();
431
+ if (!text) {
432
+ return false;
433
+ }
434
+
435
+ if (params.botId > 0) {
436
+ const botIdPattern = new RegExp(`\\[[A-Z_]+=${params.botId}(?:[^\\]]*)\\]`, 'i');
437
+ if (botIdPattern.test(text)) {
438
+ return true;
439
+ }
440
+ }
441
+
442
+ const candidates = [
443
+ params.botCode,
444
+ params.botName,
445
+ ]
446
+ .map((candidate) => String(candidate ?? '').trim())
447
+ .filter(Boolean);
448
+
449
+ return candidates.some((candidate) => {
450
+ const escaped = escapeRegExp(candidate);
451
+ const mentionPattern = new RegExp(`(^|\\s)@?${escaped}(?=$|[\\s,.:;!?])`, 'i');
452
+ return mentionPattern.test(text);
453
+ });
454
+ }
455
+
456
+ function escapeRegExp(value: string): string {
457
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
458
+ }
@@ -13,6 +13,7 @@ export interface PollingServiceOptions {
13
13
  accountId: string;
14
14
  pollingIntervalMs: number;
15
15
  pollingFastIntervalMs: number;
16
+ withUserEvents?: boolean;
16
17
  onEvent: (event: B24V2FetchEventItem) => Promise<void>;
17
18
  abortSignal: AbortSignal;
18
19
  logger: Logger;
@@ -68,6 +69,7 @@ export class PollingService {
68
69
  private readonly accountId: string;
69
70
  private readonly pollingIntervalMs: number;
70
71
  private readonly pollingFastIntervalMs: number;
72
+ private readonly withUserEvents: boolean;
71
73
  private readonly onEvent: (event: B24V2FetchEventItem) => Promise<void>;
72
74
  private readonly abortSignal: AbortSignal;
73
75
  private readonly logger: Logger;
@@ -83,6 +85,7 @@ export class PollingService {
83
85
  this.accountId = opts.accountId;
84
86
  this.pollingIntervalMs = opts.pollingIntervalMs;
85
87
  this.pollingFastIntervalMs = opts.pollingFastIntervalMs;
88
+ this.withUserEvents = Boolean(opts.withUserEvents);
86
89
  this.onEvent = opts.onEvent;
87
90
  this.abortSignal = opts.abortSignal;
88
91
  this.logger = opts.logger;
@@ -106,6 +109,7 @@ export class PollingService {
106
109
  const result = await this.api.fetchEvents(this.webhookUrl, this.bot, {
107
110
  offset: this.offset,
108
111
  limit: 100,
112
+ withUserEvents: this.withUserEvents,
109
113
  });
110
114
 
111
115
  // Successful fetch — reset backoff and rate limit streak
@@ -39,7 +39,7 @@ export class SendService {
39
39
  async sendText(
40
40
  ctx: SendContext,
41
41
  text: string,
42
- options?: { keyboard?: B24Keyboard; convertMarkdown?: boolean },
42
+ options?: { keyboard?: B24Keyboard; convertMarkdown?: boolean; forwardMessages?: number[] },
43
43
  ): Promise<SendMessageResult> {
44
44
  const convertedText = options?.convertMarkdown !== false
45
45
  ? markdownToBbCode(text)
@@ -50,10 +50,19 @@ export class SendService {
50
50
  let lastMessageId: number | undefined;
51
51
 
52
52
  for (let i = 0; i < chunks.length; i++) {
53
+ const isFirst = i === 0;
53
54
  const isLast = i === chunks.length - 1;
54
- const msgOptions = isLast && options?.keyboard
55
- ? { keyboard: options.keyboard }
56
- : undefined;
55
+ const msgOptions: {
56
+ keyboard?: B24Keyboard;
57
+ forwardMessages?: number[];
58
+ } = {};
59
+
60
+ if (isLast && options?.keyboard) {
61
+ msgOptions.keyboard = options.keyboard;
62
+ }
63
+ if (isFirst && options?.forwardMessages?.length) {
64
+ msgOptions.forwardMessages = options.forwardMessages;
65
+ }
57
66
 
58
67
  try {
59
68
  lastMessageId = await this.api.sendMessage(
@@ -61,7 +70,7 @@ export class SendService {
61
70
  ctx.bot,
62
71
  ctx.dialogId,
63
72
  chunks[i],
64
- msgOptions,
73
+ Object.keys(msgOptions).length > 0 ? msgOptions : undefined,
65
74
  );
66
75
  } catch (error) {
67
76
  this.logger.error('Failed to send message', { error: serializeError(error) });