@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/README.md +236 -42
- package/package.json +1 -1
- package/src/access-control.ts +31 -8
- package/src/api.ts +76 -7
- package/src/channel.ts +1025 -137
- package/src/commands.ts +7 -7
- package/src/config-schema.ts +24 -1
- package/src/config.ts +4 -2
- package/src/group-access.ts +279 -0
- package/src/history-cache.ts +122 -0
- package/src/i18n.ts +140 -50
- package/src/inbound-handler.ts +91 -8
- package/src/polling-service.ts +4 -0
- package/src/send-service.ts +14 -5
- package/src/types.ts +67 -3
- package/tests/access-control.test.ts +43 -0
- package/tests/api.test.ts +131 -0
- package/tests/channel-flow.test.ts +1692 -0
- package/tests/channel.test.ts +88 -2
- package/tests/config.test.ts +120 -0
- package/tests/group-access.test.ts +340 -0
- package/tests/history-cache.test.ts +117 -0
- package/tests/i18n.test.ts +55 -12
- package/tests/inbound-handler.test.ts +388 -3
- package/tests/polling-service.test.ts +38 -0
- package/tests/send-service.test.ts +17 -0
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) =>
|
|
22
|
-
ru: (f) =>
|
|
23
|
-
de: (f) =>
|
|
24
|
-
es: (f) =>
|
|
25
|
-
fr: (f) =>
|
|
26
|
-
pt: (f) =>
|
|
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: '
|
|
37
|
-
ru: '
|
|
38
|
-
de: '
|
|
39
|
-
es: '
|
|
40
|
-
fr: '
|
|
41
|
-
pt: '
|
|
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
|
|
52
|
-
ru: '
|
|
53
|
-
de: '
|
|
54
|
-
es: 'Este
|
|
55
|
-
fr: 'Ce bot est personnel
|
|
56
|
-
pt: 'Este 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
|
-
|
|
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
|
|
66
|
-
en: '
|
|
67
|
-
ru: '
|
|
68
|
-
de: '
|
|
69
|
-
es: '
|
|
70
|
-
fr: '
|
|
71
|
-
pt: '
|
|
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
|
|
75
|
-
return resolve(
|
|
98
|
+
export function accessDenied(lang: string | undefined): string {
|
|
99
|
+
return resolve(I18N_ACCESS_DENIED, lang);
|
|
76
100
|
}
|
|
77
101
|
|
|
78
|
-
const
|
|
79
|
-
en: '
|
|
80
|
-
ru: '
|
|
81
|
-
de: '
|
|
82
|
-
es: '
|
|
83
|
-
fr: '
|
|
84
|
-
pt: '
|
|
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
|
|
88
|
-
return resolve(
|
|
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
|
|
95
|
-
ru: (n) => `${n}
|
|
96
|
-
de: (n) => `${n} bereit. Senden Sie
|
|
97
|
-
es: (n) => `${n} listo.
|
|
98
|
-
fr: (n) => `${n}
|
|
99
|
-
pt: (n) => `${n} pronto. Envie
|
|
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.
|
|
108
|
-
ru: (n) => `${n}
|
|
109
|
-
de: (n) => `${n} ist bereit. Senden Sie
|
|
110
|
-
es: (n) => `${n} esta listo.
|
|
111
|
-
fr: (n) => `${n} est pret. Envoyez
|
|
112
|
-
pt: (n) => `${n} esta pronto. Envie
|
|
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)
|
package/src/inbound-handler.ts
CHANGED
|
@@ -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
|
-
|
|
133
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
351
|
-
const
|
|
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
|
+
}
|
package/src/polling-service.ts
CHANGED
|
@@ -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
|
package/src/send-service.ts
CHANGED
|
@@ -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
|
|
55
|
-
|
|
56
|
-
|
|
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) });
|