@ihazz/bitrix24 1.0.2 → 1.1.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 +236 -42
- package/package.json +1 -1
- package/skills/bitrix24/SKILL.md +28 -2
- package/src/access-control.ts +31 -8
- package/src/api.ts +76 -7
- package/src/channel.ts +1031 -138
- 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 +88 -6
- 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 +341 -3
- package/tests/polling-service.test.ts +38 -0
- package/tests/send-service.test.ts +17 -0
package/src/channel.ts
CHANGED
|
@@ -17,19 +17,30 @@ import {
|
|
|
17
17
|
checkAccessWithPairing,
|
|
18
18
|
getWebhookUserId,
|
|
19
19
|
} from './access-control.js';
|
|
20
|
+
import {
|
|
21
|
+
checkGroupAccessPassive,
|
|
22
|
+
checkGroupAccessWithPairing,
|
|
23
|
+
resolveAgentWatchRules,
|
|
24
|
+
resolveGroupAccess,
|
|
25
|
+
} from './group-access.js';
|
|
20
26
|
import { DEFAULT_AVATAR_BASE64 } from './bot-avatar.js';
|
|
21
27
|
import { Bitrix24ApiError, defaultLogger, CHANNEL_PREFIX_RE } from './utils.js';
|
|
22
28
|
import { getBitrix24Runtime } from './runtime.js';
|
|
23
29
|
import type { ChannelPairingAdapter } from './runtime.js';
|
|
24
30
|
import { OPENCLAW_COMMANDS, buildCommandsHelpText, formatModelsCommandReply } from './commands.js';
|
|
25
31
|
import {
|
|
26
|
-
|
|
32
|
+
accessApproved,
|
|
33
|
+
accessDenied,
|
|
34
|
+
groupPairingPending,
|
|
27
35
|
mediaDownloadFailed,
|
|
28
36
|
groupChatUnsupported,
|
|
29
37
|
onboardingMessage,
|
|
38
|
+
ownerAndAllowedUsersOnly,
|
|
30
39
|
personalBotOwnerOnly,
|
|
31
|
-
|
|
40
|
+
watchOwnerDmNotice,
|
|
32
41
|
} from './i18n.js';
|
|
42
|
+
import { HistoryCache } from './history-cache.js';
|
|
43
|
+
import type { ConversationMeta } from './history-cache.js';
|
|
33
44
|
import type {
|
|
34
45
|
B24MsgContext,
|
|
35
46
|
B24InputActionStatusCode,
|
|
@@ -37,6 +48,13 @@ import type {
|
|
|
37
48
|
B24V2DeleteEventData,
|
|
38
49
|
FetchContext,
|
|
39
50
|
Bitrix24AccountConfig,
|
|
51
|
+
Bitrix24AgentWatchConfig,
|
|
52
|
+
Bitrix24DmPolicy,
|
|
53
|
+
Bitrix24GroupPolicy,
|
|
54
|
+
Bitrix24GroupWatchConfig,
|
|
55
|
+
B24V2GetMessageContextResult,
|
|
56
|
+
B24V2GetMessageResult,
|
|
57
|
+
B24V2User,
|
|
40
58
|
B24Keyboard,
|
|
41
59
|
KeyboardButton,
|
|
42
60
|
Logger,
|
|
@@ -52,6 +70,13 @@ const ACCESS_DENIED_NOTICE_COOLDOWN_MS = 60000;
|
|
|
52
70
|
const AUTO_BOT_CODE_MAX_CANDIDATES = 100;
|
|
53
71
|
const MEDIA_DOWNLOAD_CONCURRENCY = 2;
|
|
54
72
|
const MAX_WEBHOOK_BODY_BYTES = 1024 * 1024;
|
|
73
|
+
const DEFAULT_HISTORY_LIMIT = 100;
|
|
74
|
+
const HISTORY_CACHE_MAX_KEYS = 1000;
|
|
75
|
+
const HISTORY_CONTEXT_MARKER = '[Chat messages since your last reply - for context]';
|
|
76
|
+
const CROSS_CHAT_HISTORY_LIMIT = 20;
|
|
77
|
+
const ACCESS_DENIED_REACTION = 'crossMark';
|
|
78
|
+
const FORWARDED_CONTEXT_RANGE = 5;
|
|
79
|
+
const REGISTERED_COMMANDS = new Set(OPENCLAW_COMMANDS.map((command) => command.command));
|
|
55
80
|
|
|
56
81
|
// ─── Emoji → B24 reaction code mapping ──────────────────────────────────
|
|
57
82
|
// B24 uses named reaction codes, not Unicode emoji.
|
|
@@ -127,6 +152,73 @@ function toMessageId(value: string | number | undefined): number | undefined {
|
|
|
127
152
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
128
153
|
}
|
|
129
154
|
|
|
155
|
+
function escapeBbCodeText(value: string): string {
|
|
156
|
+
return value
|
|
157
|
+
.replace(/\[/g, '(')
|
|
158
|
+
.replace(/\]/g, ')');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildChatContextUrl(dialogId: string, messageId: string, chatName: string): string {
|
|
162
|
+
const dialogParam = encodeURIComponent(dialogId);
|
|
163
|
+
const messageParam = encodeURIComponent(messageId);
|
|
164
|
+
const label = escapeBbCodeText(chatName);
|
|
165
|
+
return `[URL=/online/?IM_DIALOG=${dialogParam}&IM_MESSAGE=${messageParam}]${label}[/URL]`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildTopicsBbCode(topics: string[] | undefined): string | undefined {
|
|
169
|
+
if (!topics || topics.length === 0) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return topics
|
|
174
|
+
.map((topic) => topic.trim())
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.map((topic) => `[b]${escapeBbCodeText(topic)}[/b]`)
|
|
177
|
+
.join(', ');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatQuoteTimestamp(timestamp: number | undefined, language: string | undefined): string {
|
|
181
|
+
const locale = (language ?? 'ru').toLowerCase().slice(0, 2);
|
|
182
|
+
const value = timestamp ?? Date.now();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return new Intl.DateTimeFormat(locale, {
|
|
186
|
+
year: 'numeric',
|
|
187
|
+
month: '2-digit',
|
|
188
|
+
day: '2-digit',
|
|
189
|
+
hour: '2-digit',
|
|
190
|
+
minute: '2-digit',
|
|
191
|
+
}).format(new Date(value)).replace(',', '');
|
|
192
|
+
} catch {
|
|
193
|
+
return new Intl.DateTimeFormat('ru', {
|
|
194
|
+
year: 'numeric',
|
|
195
|
+
month: '2-digit',
|
|
196
|
+
day: '2-digit',
|
|
197
|
+
hour: '2-digit',
|
|
198
|
+
minute: '2-digit',
|
|
199
|
+
}).format(new Date(value)).replace(',', '');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildWatchQuoteText(params: {
|
|
204
|
+
senderName: string;
|
|
205
|
+
language?: string;
|
|
206
|
+
timestamp?: number;
|
|
207
|
+
anchor: string;
|
|
208
|
+
body: string;
|
|
209
|
+
}): string {
|
|
210
|
+
const separator = '------------------------------------------------------';
|
|
211
|
+
const senderLine = `${escapeBbCodeText(params.senderName)} [${formatQuoteTimestamp(params.timestamp, params.language)}] ${params.anchor}`;
|
|
212
|
+
const body = escapeBbCodeText(params.body);
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
separator,
|
|
216
|
+
senderLine,
|
|
217
|
+
body,
|
|
218
|
+
separator,
|
|
219
|
+
].join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
130
222
|
async function notifyStatus(
|
|
131
223
|
sendService: SendService,
|
|
132
224
|
sendCtx: SendContext,
|
|
@@ -283,8 +375,10 @@ export function mergeForwardedMessageContext(
|
|
|
283
375
|
const previousText = previousMsgCtx.text.trim();
|
|
284
376
|
const forwardedText = forwardedMsgCtx.text.trim();
|
|
285
377
|
const mergedText = [
|
|
378
|
+
previousText ? '[User question about the forwarded message]' : '',
|
|
286
379
|
previousText,
|
|
287
|
-
|
|
380
|
+
previousText ? '[/User question]' : '',
|
|
381
|
+
forwardedText,
|
|
288
382
|
].filter(Boolean).join('\n\n');
|
|
289
383
|
|
|
290
384
|
return {
|
|
@@ -292,6 +386,7 @@ export function mergeForwardedMessageContext(
|
|
|
292
386
|
text: mergedText,
|
|
293
387
|
media: [...previousMsgCtx.media, ...forwardedMsgCtx.media],
|
|
294
388
|
language: forwardedMsgCtx.language ?? previousMsgCtx.language,
|
|
389
|
+
replyToMessageId: undefined,
|
|
295
390
|
isForwarded: false,
|
|
296
391
|
};
|
|
297
392
|
}
|
|
@@ -373,6 +468,316 @@ function resolveSecurityConfig(params: {
|
|
|
373
468
|
return {};
|
|
374
469
|
}
|
|
375
470
|
|
|
471
|
+
function resolveHistoryLimit(config: Bitrix24AccountConfig): number {
|
|
472
|
+
return Math.max(0, config.historyLimit ?? DEFAULT_HISTORY_LIMIT);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface Bitrix24ConversationRef {
|
|
476
|
+
dialogId: string;
|
|
477
|
+
address: string;
|
|
478
|
+
historyKey: string;
|
|
479
|
+
peer: {
|
|
480
|
+
kind: 'direct' | 'group';
|
|
481
|
+
id: string;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function resolveConversationRef(params: {
|
|
486
|
+
accountId: string;
|
|
487
|
+
dialogId: string;
|
|
488
|
+
isDirect: boolean;
|
|
489
|
+
}): Bitrix24ConversationRef {
|
|
490
|
+
const dialogId = String(params.dialogId);
|
|
491
|
+
return {
|
|
492
|
+
dialogId,
|
|
493
|
+
address: `bitrix24:${dialogId}`,
|
|
494
|
+
historyKey: `${params.accountId}:${dialogId}`,
|
|
495
|
+
peer: {
|
|
496
|
+
kind: params.isDirect ? 'direct' : 'group',
|
|
497
|
+
id: dialogId,
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function buildConversationSessionKey(
|
|
503
|
+
routeSessionKey: string,
|
|
504
|
+
conversation: Pick<Bitrix24ConversationRef, 'address'>,
|
|
505
|
+
): string {
|
|
506
|
+
return `${routeSessionKey}:${conversation.address}`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function buildHistoryBody(msgCtx: B24MsgContext): string {
|
|
510
|
+
const text = msgCtx.text.trim();
|
|
511
|
+
if (text) {
|
|
512
|
+
return text;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (msgCtx.media.length === 0) {
|
|
516
|
+
return '';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const hasImage = msgCtx.media.some((mediaItem) => mediaItem.type === 'image');
|
|
520
|
+
return hasImage ? '<media:image>' : '<media:document>';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function buildConversationMeta(msgCtx: B24MsgContext): Omit<ConversationMeta, 'key'> {
|
|
524
|
+
return {
|
|
525
|
+
dialogId: msgCtx.chatId,
|
|
526
|
+
chatId: msgCtx.chatInternalId,
|
|
527
|
+
chatName: msgCtx.chatName,
|
|
528
|
+
chatType: msgCtx.chatType,
|
|
529
|
+
isGroup: msgCtx.isGroup,
|
|
530
|
+
lastActivityAt: msgCtx.timestamp ?? Date.now(),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function appendMessageToHistory(params: {
|
|
535
|
+
historyCache: HistoryCache;
|
|
536
|
+
historyKey: string;
|
|
537
|
+
historyLimit: number;
|
|
538
|
+
msgCtx: B24MsgContext;
|
|
539
|
+
body?: string;
|
|
540
|
+
}): void {
|
|
541
|
+
const historyBody = (params.body ?? buildHistoryBody(params.msgCtx)).trim();
|
|
542
|
+
if (!historyBody) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
params.historyCache.append({
|
|
547
|
+
key: params.historyKey,
|
|
548
|
+
limit: params.historyLimit,
|
|
549
|
+
entry: {
|
|
550
|
+
messageId: params.msgCtx.messageId,
|
|
551
|
+
sender: params.msgCtx.senderName,
|
|
552
|
+
senderId: params.msgCtx.senderId,
|
|
553
|
+
body: historyBody,
|
|
554
|
+
timestamp: params.msgCtx.timestamp ?? Date.now(),
|
|
555
|
+
wasMentioned: params.msgCtx.wasMentioned,
|
|
556
|
+
eventScope: params.msgCtx.eventScope ?? 'bot',
|
|
557
|
+
},
|
|
558
|
+
meta: buildConversationMeta(params.msgCtx),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function formatHistoryEntry(params: {
|
|
563
|
+
sender: string;
|
|
564
|
+
body: string;
|
|
565
|
+
messageId?: string;
|
|
566
|
+
}): string {
|
|
567
|
+
const idSuffix = params.messageId ? ` [id:${params.messageId}]` : '';
|
|
568
|
+
return `[${params.sender}] ${params.body}${idSuffix}`;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function buildHistoryContext(params: {
|
|
572
|
+
entries: Array<{ sender: string; body: string; messageId?: string }>;
|
|
573
|
+
currentBody: string;
|
|
574
|
+
}): string {
|
|
575
|
+
if (params.entries.length === 0) {
|
|
576
|
+
return params.currentBody;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const historyText = params.entries
|
|
580
|
+
.map((entry) => formatHistoryEntry(entry))
|
|
581
|
+
.join('\n');
|
|
582
|
+
|
|
583
|
+
return [
|
|
584
|
+
HISTORY_CONTEXT_MARKER,
|
|
585
|
+
historyText,
|
|
586
|
+
'',
|
|
587
|
+
params.currentBody,
|
|
588
|
+
].join('\n');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function formatReplyContext(params: {
|
|
592
|
+
body: string;
|
|
593
|
+
replyEntry?: { sender: string; body: string; messageId: string };
|
|
594
|
+
}): string {
|
|
595
|
+
if (!params.replyEntry) {
|
|
596
|
+
return params.body;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return [
|
|
600
|
+
params.body,
|
|
601
|
+
'',
|
|
602
|
+
`[Replying to ${params.replyEntry.sender} id:${params.replyEntry.messageId}]`,
|
|
603
|
+
params.replyEntry.body,
|
|
604
|
+
'[/Replying]',
|
|
605
|
+
].filter(Boolean).join('\n');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function resolveFetchedUserName(usersById: Map<number, B24V2User>, authorId: number): string {
|
|
609
|
+
return usersById.get(authorId)?.name?.trim() || `User ${authorId}`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function resolveFetchedMessageBody(text: string): string {
|
|
613
|
+
const trimmed = text.trim();
|
|
614
|
+
return trimmed.length > 0 ? trimmed : '<empty message>';
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function formatFetchedForwardContext(params: {
|
|
618
|
+
context: B24V2GetMessageContextResult;
|
|
619
|
+
currentMessageId: number;
|
|
620
|
+
}): string {
|
|
621
|
+
const usersById = new Map(params.context.users.map((user) => [user.id, user]));
|
|
622
|
+
const lines = params.context.messages
|
|
623
|
+
.filter((message) => message.id !== params.currentMessageId)
|
|
624
|
+
.map((message) => {
|
|
625
|
+
const sender = resolveFetchedUserName(usersById, message.authorId);
|
|
626
|
+
const body = resolveFetchedMessageBody(message.text);
|
|
627
|
+
return `[${sender} id:${message.id}] ${body}`;
|
|
628
|
+
})
|
|
629
|
+
.filter(Boolean);
|
|
630
|
+
|
|
631
|
+
if (lines.length === 0) {
|
|
632
|
+
return '';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return [
|
|
636
|
+
'[Bitrix24 surrounding context around this forwarded message - not the forwarded message body]',
|
|
637
|
+
...lines,
|
|
638
|
+
'[/Bitrix24 surrounding context]',
|
|
639
|
+
].join('\n');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function extractMentionedChatIds(text: string): string[] {
|
|
643
|
+
const matches = [...text.matchAll(/\[CHAT=(?:chat)?(\d+)(?:[^\]]*)\][\s\S]*?\[\/CHAT\]/gi)];
|
|
644
|
+
return [...new Set(matches.map((match) => match[1]).filter(Boolean))];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function formatReferencedGroupHistory(params: {
|
|
648
|
+
conversation: ConversationMeta;
|
|
649
|
+
entries: Array<{ sender: string; body: string; messageId?: string }>;
|
|
650
|
+
}): string {
|
|
651
|
+
const chatId = params.conversation.chatId ?? params.conversation.dialogId.replace(/^chat/i, '');
|
|
652
|
+
const chatName = params.conversation.chatName ?? params.conversation.dialogId;
|
|
653
|
+
const header = `[Visible group chat history: [CHAT=${chatId}]${chatName}[/CHAT]]`;
|
|
654
|
+
|
|
655
|
+
if (params.entries.length === 0) {
|
|
656
|
+
return [
|
|
657
|
+
header,
|
|
658
|
+
'No messages from this chat are currently available in RAM memory.',
|
|
659
|
+
].join('\n');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return [
|
|
663
|
+
header,
|
|
664
|
+
...params.entries.map((entry) => formatHistoryEntry(entry)),
|
|
665
|
+
].join('\n');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function buildCrossChatMemoryContext(params: {
|
|
669
|
+
query: string;
|
|
670
|
+
historyCache: HistoryCache;
|
|
671
|
+
}): string | undefined {
|
|
672
|
+
const chatMentions = extractMentionedChatIds(params.query);
|
|
673
|
+
if (chatMentions.length === 0) {
|
|
674
|
+
return undefined;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const visibleGroupChats = params.historyCache
|
|
678
|
+
.listConversations()
|
|
679
|
+
.filter((conversation) => conversation.isGroup)
|
|
680
|
+
.sort((left, right) => (right.lastActivityAt ?? 0) - (left.lastActivityAt ?? 0));
|
|
681
|
+
|
|
682
|
+
const referencedChats = visibleGroupChats.filter((conversation) => {
|
|
683
|
+
const chatId = conversation.chatId ?? '';
|
|
684
|
+
return chatMentions.includes(chatId);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const sections: string[] = [];
|
|
688
|
+
|
|
689
|
+
if (referencedChats.length > 0) {
|
|
690
|
+
sections.push(
|
|
691
|
+
...referencedChats.map((conversation) => formatReferencedGroupHistory({
|
|
692
|
+
conversation,
|
|
693
|
+
entries: params.historyCache.get(conversation.key, CROSS_CHAT_HISTORY_LIMIT),
|
|
694
|
+
})),
|
|
695
|
+
);
|
|
696
|
+
} else if (chatMentions.length > 0) {
|
|
697
|
+
sections.push('[Referenced group chats]\nThe requested group chat mention is not available in RAM memory right now.');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return [
|
|
701
|
+
'[OpenClaw cross-chat memory]',
|
|
702
|
+
'The following Bitrix24 group chat memory is already available to you from RAM history.',
|
|
703
|
+
'Use it as trusted context for your answer.',
|
|
704
|
+
'Do not say that you only see the current chat if chats or messages are listed below.',
|
|
705
|
+
'Do not call tools to list chats when this memory block already contains the answer.',
|
|
706
|
+
'',
|
|
707
|
+
'[BEGIN OPENCLAW CROSS-CHAT MEMORY]',
|
|
708
|
+
sections.join('\n\n'),
|
|
709
|
+
'[END OPENCLAW CROSS-CHAT MEMORY]',
|
|
710
|
+
].join('\n');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildAccessDeniedNotice(
|
|
714
|
+
lang: string | undefined,
|
|
715
|
+
policy: Bitrix24DmPolicy | Bitrix24GroupPolicy | undefined,
|
|
716
|
+
params?: { hasAllowList?: boolean },
|
|
717
|
+
): string {
|
|
718
|
+
const effectivePolicy = policy ?? 'webhookUser';
|
|
719
|
+
|
|
720
|
+
if (effectivePolicy === 'webhookUser') {
|
|
721
|
+
return personalBotOwnerOnly(lang);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (params?.hasAllowList) {
|
|
725
|
+
return ownerAndAllowedUsersOnly(lang);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return accessDenied(lang);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function normalizeTopicText(text: string): string {
|
|
732
|
+
return text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function tokenizeTopicText(text: string): string[] {
|
|
736
|
+
return normalizeTopicText(text)
|
|
737
|
+
.split(/[^a-zа-яё0-9]+/i)
|
|
738
|
+
.filter(Boolean);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function matchesWatchTopic(messageText: string, topic: string): boolean {
|
|
742
|
+
const normalizedMessage = normalizeTopicText(messageText);
|
|
743
|
+
const normalizedTopic = normalizeTopicText(topic);
|
|
744
|
+
if (!normalizedMessage || !normalizedTopic) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (normalizedMessage.includes(normalizedTopic)) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const messageTokens = tokenizeTopicText(messageText);
|
|
753
|
+
const topicTokens = tokenizeTopicText(topic);
|
|
754
|
+
return topicTokens.length > 0
|
|
755
|
+
&& topicTokens.every((topicToken) => messageTokens.some((messageToken) => messageToken.startsWith(topicToken)));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function findMatchingWatchRule<T extends { userId: string; topics?: string[] }>(
|
|
759
|
+
msgCtx: B24MsgContext,
|
|
760
|
+
watchRules: T[] | undefined,
|
|
761
|
+
): T | undefined {
|
|
762
|
+
if (!Array.isArray(watchRules) || watchRules.length === 0) {
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const senderId = normalizeAllowEntry(msgCtx.senderId);
|
|
767
|
+
return watchRules.find((rule) => {
|
|
768
|
+
const ruleUserId = normalizeAllowEntry(rule.userId);
|
|
769
|
+
if (ruleUserId !== '*' && ruleUserId !== senderId) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (!Array.isArray(rule.topics) || rule.topics.length === 0) {
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return rule.topics.some((topic) => matchesWatchTopic(msgCtx.text, topic));
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
376
781
|
interface BufferedDirectMessageEntry {
|
|
377
782
|
messages: B24MsgContext[];
|
|
378
783
|
startedAt: number;
|
|
@@ -535,21 +940,48 @@ export interface ChannelButton {
|
|
|
535
940
|
style?: string;
|
|
536
941
|
}
|
|
537
942
|
|
|
943
|
+
function parseRegisteredCommandTrigger(callbackData: string): { command: string; commandParams?: string } | undefined {
|
|
944
|
+
const trimmed = callbackData.trim();
|
|
945
|
+
const isSlashCommand = trimmed.startsWith('/');
|
|
946
|
+
const normalized = trimmed.replace(/^\/+/, '');
|
|
947
|
+
if (!normalized) {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const [commandName, ...params] = normalized.split(/\s+/);
|
|
952
|
+
if (!isSlashCommand && !REGISTERED_COMMANDS.has(commandName)) {
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return {
|
|
957
|
+
command: commandName,
|
|
958
|
+
...(params.length > 0 ? { commandParams: params.join(' ') } : {}),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
538
962
|
/**
|
|
539
963
|
* Convert OpenClaw button rows to B24 flat KEYBOARD array.
|
|
540
964
|
*/
|
|
541
|
-
export function convertButtonsToKeyboard(
|
|
965
|
+
export function convertButtonsToKeyboard(input: ChannelButton[][] | ChannelButton[]): B24Keyboard {
|
|
966
|
+
// Normalize: accept both [[btn, btn], [btn]] (rows) and [btn, btn] (flat)
|
|
967
|
+
const isNested = input.length > 0 && Array.isArray(input[0]);
|
|
968
|
+
const rows: ChannelButton[][] = isNested
|
|
969
|
+
? (input as ChannelButton[][])
|
|
970
|
+
: [input as ChannelButton[]];
|
|
542
971
|
const keyboard: B24Keyboard = [];
|
|
543
972
|
|
|
544
973
|
for (let i = 0; i < rows.length; i++) {
|
|
545
974
|
for (const btn of rows[i]) {
|
|
546
975
|
const b24Btn: KeyboardButton = { TEXT: btn.text, DISPLAY: 'LINE' };
|
|
547
976
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
977
|
+
const parsedCommand = btn.callback_data
|
|
978
|
+
? parseRegisteredCommandTrigger(btn.callback_data)
|
|
979
|
+
: undefined;
|
|
980
|
+
|
|
981
|
+
if (parsedCommand) {
|
|
982
|
+
b24Btn.COMMAND = parsedCommand.command;
|
|
983
|
+
if (parsedCommand.commandParams) {
|
|
984
|
+
b24Btn.COMMAND_PARAMS = parsedCommand.commandParams;
|
|
553
985
|
}
|
|
554
986
|
} else if (btn.callback_data) {
|
|
555
987
|
b24Btn.ACTION = 'SEND';
|
|
@@ -1027,7 +1459,7 @@ export const bitrix24Plugin = {
|
|
|
1027
1459
|
},
|
|
1028
1460
|
|
|
1029
1461
|
capabilities: {
|
|
1030
|
-
chatTypes: ['direct'] as const,
|
|
1462
|
+
chatTypes: ['direct', 'group'] as const,
|
|
1031
1463
|
media: true,
|
|
1032
1464
|
reactions: true,
|
|
1033
1465
|
threads: false,
|
|
@@ -1055,9 +1487,10 @@ export const bitrix24Plugin = {
|
|
|
1055
1487
|
security: {
|
|
1056
1488
|
resolveDmPolicy: (params: { cfg?: Record<string, unknown>; accountId?: string; account?: { config?: Record<string, unknown> } }) => {
|
|
1057
1489
|
const securityConfig = resolveSecurityConfig(params);
|
|
1490
|
+
const policy = securityConfig.dmPolicy ?? 'webhookUser';
|
|
1058
1491
|
|
|
1059
1492
|
return {
|
|
1060
|
-
policy
|
|
1493
|
+
policy,
|
|
1061
1494
|
allowFrom: normalizeAllowList(securityConfig.allowFrom),
|
|
1062
1495
|
policyPath: 'channels.bitrix24.dmPolicy',
|
|
1063
1496
|
allowFromPath: 'channels.bitrix24.allowFrom',
|
|
@@ -1081,7 +1514,7 @@ export const bitrix24Plugin = {
|
|
|
1081
1514
|
dialogId: params.id,
|
|
1082
1515
|
};
|
|
1083
1516
|
try {
|
|
1084
|
-
await gatewayState.sendService.sendText(sendCtx,
|
|
1517
|
+
await gatewayState.sendService.sendText(sendCtx, `\u2705 ${accessApproved(undefined)}`);
|
|
1085
1518
|
} catch (err) {
|
|
1086
1519
|
defaultLogger.warn('Failed to notify approved Bitrix24 user', err);
|
|
1087
1520
|
}
|
|
@@ -1378,12 +1811,13 @@ export const bitrix24Plugin = {
|
|
|
1378
1811
|
return;
|
|
1379
1812
|
}
|
|
1380
1813
|
const welcomedDialogs = new Set<string>();
|
|
1381
|
-
const
|
|
1814
|
+
const dialogNoticeTimestamps = new Map<string, number>();
|
|
1815
|
+
const historyCache = new HistoryCache({ maxKeys: HISTORY_CACHE_MAX_KEYS });
|
|
1382
1816
|
|
|
1383
1817
|
// Cleanup stale denied dialog entries once per day
|
|
1384
1818
|
const DENIED_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
1385
1819
|
const deniedCleanupTimer = setInterval(() => {
|
|
1386
|
-
|
|
1820
|
+
dialogNoticeTimestamps.clear();
|
|
1387
1821
|
}, DENIED_CLEANUP_INTERVAL_MS);
|
|
1388
1822
|
if (deniedCleanupTimer && typeof deniedCleanupTimer === 'object' && 'unref' in deniedCleanupTimer) {
|
|
1389
1823
|
deniedCleanupTimer.unref();
|
|
@@ -1404,6 +1838,7 @@ export const bitrix24Plugin = {
|
|
|
1404
1838
|
}
|
|
1405
1839
|
|
|
1406
1840
|
const bot: BotContext = { botId: botRegistration.botId, botToken };
|
|
1841
|
+
const webhookOwnerId = getWebhookUserId(config.webhookUrl);
|
|
1407
1842
|
|
|
1408
1843
|
// Sync user event subscription with agent mode setting
|
|
1409
1844
|
if (eventMode === 'fetch') {
|
|
@@ -1422,15 +1857,126 @@ export const bitrix24Plugin = {
|
|
|
1422
1857
|
|
|
1423
1858
|
const sendService = new SendService(api, logger);
|
|
1424
1859
|
const mediaService = new MediaService(api, logger);
|
|
1860
|
+
const hydrateReplyEntry = async (
|
|
1861
|
+
replyToMessageId: string | undefined,
|
|
1862
|
+
): Promise<{ sender: string; body: string; messageId: string } | undefined> => {
|
|
1863
|
+
const bitrixMessageId = toMessageId(replyToMessageId);
|
|
1864
|
+
if (!bitrixMessageId) {
|
|
1865
|
+
return undefined;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
try {
|
|
1869
|
+
const result: B24V2GetMessageResult = await api.getMessage(
|
|
1870
|
+
webhookUrl,
|
|
1871
|
+
bot,
|
|
1872
|
+
bitrixMessageId,
|
|
1873
|
+
);
|
|
1874
|
+
|
|
1875
|
+
return {
|
|
1876
|
+
sender: result.user?.name?.trim() || `User ${result.message.authorId}`,
|
|
1877
|
+
body: resolveFetchedMessageBody(result.message.text),
|
|
1878
|
+
messageId: String(result.message.id),
|
|
1879
|
+
};
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
logger.debug('Failed to hydrate reply context from Bitrix24 API', {
|
|
1882
|
+
replyToMessageId,
|
|
1883
|
+
error: err,
|
|
1884
|
+
});
|
|
1885
|
+
return undefined;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
const hydrateForwardedMessageContext = async (
|
|
1889
|
+
msgCtx: B24MsgContext,
|
|
1890
|
+
): Promise<B24MsgContext> => {
|
|
1891
|
+
const currentMessageId = toMessageId(msgCtx.messageId);
|
|
1892
|
+
if (!currentMessageId) {
|
|
1893
|
+
return {
|
|
1894
|
+
...msgCtx,
|
|
1895
|
+
isForwarded: false,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
try {
|
|
1900
|
+
const result = await api.getMessageContext(
|
|
1901
|
+
webhookUrl,
|
|
1902
|
+
bot,
|
|
1903
|
+
currentMessageId,
|
|
1904
|
+
FORWARDED_CONTEXT_RANGE,
|
|
1905
|
+
);
|
|
1906
|
+
const currentMessage = result.messages.find((message) => message.id === currentMessageId);
|
|
1907
|
+
const currentBody = resolveFetchedMessageBody(currentMessage?.text ?? msgCtx.text);
|
|
1908
|
+
const fetchedContext = formatFetchedForwardContext({
|
|
1909
|
+
context: result,
|
|
1910
|
+
currentMessageId,
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
if (!currentBody && !fetchedContext) {
|
|
1914
|
+
return {
|
|
1915
|
+
...msgCtx,
|
|
1916
|
+
isForwarded: false,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
return {
|
|
1921
|
+
...msgCtx,
|
|
1922
|
+
text: [
|
|
1923
|
+
currentBody ? '[Forwarded message body]' : '',
|
|
1924
|
+
currentBody,
|
|
1925
|
+
currentBody ? '[/Forwarded message body]' : '',
|
|
1926
|
+
fetchedContext,
|
|
1927
|
+
].filter(Boolean).join('\n\n'),
|
|
1928
|
+
isForwarded: false,
|
|
1929
|
+
};
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
logger.debug('Failed to hydrate forwarded message context from Bitrix24 API', {
|
|
1932
|
+
messageId: msgCtx.messageId,
|
|
1933
|
+
chatId: msgCtx.chatId,
|
|
1934
|
+
error: err,
|
|
1935
|
+
});
|
|
1936
|
+
return {
|
|
1937
|
+
...msgCtx,
|
|
1938
|
+
isForwarded: false,
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1425
1942
|
const processAllowedMessage = async (msgCtx: B24MsgContext): Promise<void> => {
|
|
1426
1943
|
const runtime = getBitrix24Runtime();
|
|
1427
1944
|
const cfg = runtime.config.loadConfig();
|
|
1945
|
+
const conversation = resolveConversationRef({
|
|
1946
|
+
accountId: ctx.accountId,
|
|
1947
|
+
dialogId: msgCtx.chatId,
|
|
1948
|
+
isDirect: msgCtx.isDm,
|
|
1949
|
+
});
|
|
1428
1950
|
const sendCtx: SendContext = {
|
|
1429
1951
|
webhookUrl,
|
|
1430
1952
|
bot,
|
|
1431
|
-
dialogId:
|
|
1953
|
+
dialogId: conversation.dialogId,
|
|
1432
1954
|
};
|
|
1955
|
+
const historyKey = conversation.historyKey;
|
|
1956
|
+
const historyLimit = resolveHistoryLimit(config);
|
|
1957
|
+
const fallbackHistoryBody = buildHistoryBody(msgCtx);
|
|
1433
1958
|
let downloadedMedia: DownloadedMedia[] = [];
|
|
1959
|
+
let historyRecorded = false;
|
|
1960
|
+
|
|
1961
|
+
const recordHistory = (bodyOverride?: string): void => {
|
|
1962
|
+
if (historyRecorded) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const historyBody = (bodyOverride ?? fallbackHistoryBody).trim();
|
|
1967
|
+
if (!historyBody) {
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
appendMessageToHistory({
|
|
1972
|
+
historyCache,
|
|
1973
|
+
historyKey,
|
|
1974
|
+
historyLimit,
|
|
1975
|
+
msgCtx,
|
|
1976
|
+
body: historyBody,
|
|
1977
|
+
});
|
|
1978
|
+
historyRecorded = true;
|
|
1979
|
+
};
|
|
1434
1980
|
|
|
1435
1981
|
try {
|
|
1436
1982
|
const replyStatusHeartbeat = createReplyStatusHeartbeat({
|
|
@@ -1454,7 +2000,7 @@ export const bitrix24Plugin = {
|
|
|
1454
2000
|
extension: mediaItem.extension,
|
|
1455
2001
|
webhookUrl,
|
|
1456
2002
|
bot,
|
|
1457
|
-
dialogId:
|
|
2003
|
+
dialogId: conversation.dialogId,
|
|
1458
2004
|
}),
|
|
1459
2005
|
)).filter(Boolean) as DownloadedMedia[];
|
|
1460
2006
|
|
|
@@ -1470,6 +2016,7 @@ export const bitrix24Plugin = {
|
|
|
1470
2016
|
} else {
|
|
1471
2017
|
const fileNames = msgCtx.media.map((mediaItem) => mediaItem.name).join(', ');
|
|
1472
2018
|
logger.warn('All media downloads failed, notifying user', { fileNames });
|
|
2019
|
+
recordHistory();
|
|
1473
2020
|
await sendService.sendText(
|
|
1474
2021
|
sendCtx,
|
|
1475
2022
|
mediaDownloadFailed(msgCtx.language, fileNames),
|
|
@@ -1489,15 +2036,35 @@ export const bitrix24Plugin = {
|
|
|
1489
2036
|
body = hasImage ? '<media:image>' : '<media:document>';
|
|
1490
2037
|
}
|
|
1491
2038
|
|
|
2039
|
+
const previousEntries = historyCache.get(historyKey, historyLimit);
|
|
2040
|
+
const replyEntry = historyCache.findByMessageId(historyKey, msgCtx.replyToMessageId)
|
|
2041
|
+
?? await hydrateReplyEntry(msgCtx.replyToMessageId);
|
|
2042
|
+
const bodyWithReply = formatReplyContext({
|
|
2043
|
+
body,
|
|
2044
|
+
replyEntry,
|
|
2045
|
+
});
|
|
2046
|
+
const crossChatContext = buildCrossChatMemoryContext({
|
|
2047
|
+
query: bodyWithReply,
|
|
2048
|
+
historyCache,
|
|
2049
|
+
});
|
|
2050
|
+
const bodyForAgent = crossChatContext
|
|
2051
|
+
? [crossChatContext, bodyWithReply].filter(Boolean).join('\n\n')
|
|
2052
|
+
: bodyWithReply;
|
|
2053
|
+
const combinedBody = msgCtx.isGroup
|
|
2054
|
+
? buildHistoryContext({
|
|
2055
|
+
entries: previousEntries,
|
|
2056
|
+
currentBody: bodyForAgent,
|
|
2057
|
+
})
|
|
2058
|
+
: bodyForAgent;
|
|
2059
|
+
|
|
2060
|
+
recordHistory(body);
|
|
2061
|
+
|
|
1492
2062
|
// Resolve which agent handles this conversation
|
|
1493
2063
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
1494
2064
|
cfg,
|
|
1495
2065
|
channel: 'bitrix24',
|
|
1496
2066
|
accountId: ctx.accountId,
|
|
1497
|
-
peer:
|
|
1498
|
-
kind: msgCtx.isDm ? 'direct' : 'group',
|
|
1499
|
-
id: msgCtx.chatId,
|
|
1500
|
-
},
|
|
2067
|
+
peer: conversation.peer,
|
|
1501
2068
|
});
|
|
1502
2069
|
|
|
1503
2070
|
logger.debug('Resolved route', {
|
|
@@ -1507,12 +2074,19 @@ export const bitrix24Plugin = {
|
|
|
1507
2074
|
});
|
|
1508
2075
|
|
|
1509
2076
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
1510
|
-
Body:
|
|
1511
|
-
BodyForAgent:
|
|
2077
|
+
Body: combinedBody,
|
|
2078
|
+
BodyForAgent: bodyForAgent,
|
|
2079
|
+
InboundHistory: msgCtx.isGroup && previousEntries.length > 0
|
|
2080
|
+
? previousEntries.map((entry) => ({
|
|
2081
|
+
sender: entry.sender,
|
|
2082
|
+
body: entry.body,
|
|
2083
|
+
timestamp: entry.timestamp,
|
|
2084
|
+
}))
|
|
2085
|
+
: undefined,
|
|
1512
2086
|
RawBody: body,
|
|
1513
|
-
From:
|
|
1514
|
-
To:
|
|
1515
|
-
SessionKey:
|
|
2087
|
+
From: conversation.address,
|
|
2088
|
+
To: conversation.address,
|
|
2089
|
+
SessionKey: buildConversationSessionKey(route.sessionKey, conversation),
|
|
1516
2090
|
AccountId: route.accountId,
|
|
1517
2091
|
ChatType: msgCtx.isDm ? 'direct' : 'group',
|
|
1518
2092
|
ConversationLabel: msgCtx.senderName,
|
|
@@ -1521,11 +2095,14 @@ export const bitrix24Plugin = {
|
|
|
1521
2095
|
Provider: 'bitrix24',
|
|
1522
2096
|
Surface: 'bitrix24',
|
|
1523
2097
|
MessageSid: msgCtx.messageId,
|
|
1524
|
-
Timestamp: Date.now(),
|
|
1525
|
-
|
|
2098
|
+
Timestamp: msgCtx.timestamp ?? Date.now(),
|
|
2099
|
+
ReplyToId: replyEntry?.messageId ?? msgCtx.replyToMessageId,
|
|
2100
|
+
ReplyToBody: replyEntry?.body,
|
|
2101
|
+
ReplyToSender: replyEntry?.sender,
|
|
2102
|
+
WasMentioned: msgCtx.wasMentioned ?? false,
|
|
1526
2103
|
CommandAuthorized: true,
|
|
1527
2104
|
OriginatingChannel: 'bitrix24',
|
|
1528
|
-
OriginatingTo:
|
|
2105
|
+
OriginatingTo: conversation.address,
|
|
1529
2106
|
...mediaFields,
|
|
1530
2107
|
});
|
|
1531
2108
|
|
|
@@ -1543,7 +2120,7 @@ export const bitrix24Plugin = {
|
|
|
1543
2120
|
fileName: basename(mediaUrl),
|
|
1544
2121
|
webhookUrl,
|
|
1545
2122
|
bot,
|
|
1546
|
-
dialogId:
|
|
2123
|
+
dialogId: conversation.dialogId,
|
|
1547
2124
|
});
|
|
1548
2125
|
|
|
1549
2126
|
if (!uploadResult.ok) {
|
|
@@ -1589,26 +2166,113 @@ export const bitrix24Plugin = {
|
|
|
1589
2166
|
onFlush: processAllowedMessage,
|
|
1590
2167
|
logger,
|
|
1591
2168
|
});
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
language: string | undefined,
|
|
2169
|
+
const maybeSendDialogNotice = async (
|
|
2170
|
+
noticeKey: string,
|
|
1595
2171
|
sendCtx: SendContext,
|
|
2172
|
+
text: string,
|
|
1596
2173
|
): Promise<void> => {
|
|
1597
2174
|
const now = Date.now();
|
|
1598
|
-
const lastSentAt =
|
|
2175
|
+
const lastSentAt = dialogNoticeTimestamps.get(noticeKey) ?? 0;
|
|
1599
2176
|
if ((now - lastSentAt) < ACCESS_DENIED_NOTICE_COOLDOWN_MS) {
|
|
1600
2177
|
return;
|
|
1601
2178
|
}
|
|
1602
2179
|
|
|
1603
|
-
|
|
2180
|
+
dialogNoticeTimestamps.set(noticeKey, now);
|
|
1604
2181
|
try {
|
|
1605
|
-
await sendService.sendText(
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
2182
|
+
await sendService.sendText(sendCtx, text, { convertMarkdown: false });
|
|
2183
|
+
} catch (err) {
|
|
2184
|
+
logger.warn('Failed to send dialog notice', err);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
const maybeReactToDeniedMention = async (msgCtx: B24MsgContext): Promise<void> => {
|
|
2188
|
+
if (!msgCtx.isGroup || !msgCtx.wasMentioned) {
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const messageId = toMessageId(msgCtx.messageId);
|
|
2193
|
+
if (!messageId) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
try {
|
|
2198
|
+
await api.addReaction(
|
|
2199
|
+
webhookUrl,
|
|
2200
|
+
bot,
|
|
2201
|
+
messageId,
|
|
2202
|
+
ACCESS_DENIED_REACTION,
|
|
2203
|
+
);
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
logger.debug('Failed to add access-denied reaction', {
|
|
2206
|
+
chatId: msgCtx.chatId,
|
|
2207
|
+
messageId: msgCtx.messageId,
|
|
2208
|
+
error: err,
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
const notifyWebhookOwnerAboutWatchMatch = async (
|
|
2213
|
+
msgCtx: B24MsgContext,
|
|
2214
|
+
watchRule: Bitrix24GroupWatchConfig | Bitrix24AgentWatchConfig,
|
|
2215
|
+
): Promise<boolean> => {
|
|
2216
|
+
const ownerId = webhookOwnerId;
|
|
2217
|
+
const forwardedMessageId = toMessageId(msgCtx.messageId);
|
|
2218
|
+
if (!ownerId || !forwardedMessageId) {
|
|
2219
|
+
logger.warn('Skipping owner watch notification: missing owner dialog or message id', {
|
|
2220
|
+
ownerId,
|
|
2221
|
+
messageId: msgCtx.messageId,
|
|
2222
|
+
chatId: msgCtx.chatId,
|
|
2223
|
+
});
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const ownerSendCtx: SendContext = {
|
|
2228
|
+
webhookUrl,
|
|
2229
|
+
bot,
|
|
2230
|
+
dialogId: ownerId,
|
|
2231
|
+
};
|
|
2232
|
+
|
|
2233
|
+
const noticeText = watchOwnerDmNotice(msgCtx.language, {
|
|
2234
|
+
chatRef: buildChatContextUrl(
|
|
2235
|
+
msgCtx.chatId,
|
|
2236
|
+
msgCtx.messageId,
|
|
2237
|
+
msgCtx.isDm
|
|
2238
|
+
? (msgCtx.senderName || msgCtx.chatName || msgCtx.chatId)
|
|
2239
|
+
: (msgCtx.chatName ?? msgCtx.chatId),
|
|
2240
|
+
),
|
|
2241
|
+
topicsRef: buildTopicsBbCode(watchRule.topics),
|
|
2242
|
+
sourceKind: msgCtx.isDm ? 'dm' : 'chat',
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
try {
|
|
2246
|
+
await sendService.sendText(ownerSendCtx, noticeText, {
|
|
2247
|
+
convertMarkdown: false,
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
if (msgCtx.eventScope === 'user' && msgCtx.isDm) {
|
|
2251
|
+
const quoteText = buildWatchQuoteText({
|
|
2252
|
+
senderName: msgCtx.senderName || msgCtx.chatName || msgCtx.chatId,
|
|
2253
|
+
language: msgCtx.language,
|
|
2254
|
+
timestamp: msgCtx.timestamp,
|
|
2255
|
+
anchor: `#${msgCtx.chatId}:${ownerId}/${msgCtx.messageId}`,
|
|
2256
|
+
body: msgCtx.text.trim(),
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
await sendService.sendText(ownerSendCtx, quoteText, {
|
|
2260
|
+
convertMarkdown: false,
|
|
2261
|
+
});
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
await api.sendMessage(
|
|
2266
|
+
webhookUrl,
|
|
2267
|
+
bot,
|
|
2268
|
+
ownerId,
|
|
2269
|
+
null,
|
|
2270
|
+
{ forwardMessages: [forwardedMessageId] },
|
|
1609
2271
|
);
|
|
2272
|
+
return true;
|
|
1610
2273
|
} catch (err) {
|
|
1611
|
-
logger.warn('Failed to send
|
|
2274
|
+
logger.warn('Failed to send owner watch notification with native forward', err);
|
|
2275
|
+
return false;
|
|
1612
2276
|
}
|
|
1613
2277
|
};
|
|
1614
2278
|
|
|
@@ -1644,20 +2308,6 @@ export const bitrix24Plugin = {
|
|
|
1644
2308
|
? directTextCoalescer.take(ctx.accountId, msgCtx.chatId)
|
|
1645
2309
|
: null;
|
|
1646
2310
|
|
|
1647
|
-
if (msgCtx.isGroup) {
|
|
1648
|
-
logger.warn('Group chat is not supported, leaving chat', {
|
|
1649
|
-
chatId: msgCtx.chatId,
|
|
1650
|
-
senderId: msgCtx.senderId,
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
try {
|
|
1654
|
-
await api.leaveChat(webhookUrl, bot, msgCtx.chatId);
|
|
1655
|
-
} catch (err) {
|
|
1656
|
-
logger.error('Failed to leave group chat after message', err);
|
|
1657
|
-
}
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
2311
|
const runtime = getBitrix24Runtime();
|
|
1662
2312
|
|
|
1663
2313
|
// Pairing-aware access control
|
|
@@ -1666,73 +2316,233 @@ export const bitrix24Plugin = {
|
|
|
1666
2316
|
bot,
|
|
1667
2317
|
dialogId: msgCtx.chatId,
|
|
1668
2318
|
};
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
senderId: msgCtx.senderId,
|
|
2319
|
+
const conversation = resolveConversationRef({
|
|
2320
|
+
accountId: ctx.accountId,
|
|
1672
2321
|
dialogId: msgCtx.chatId,
|
|
1673
2322
|
isDirect: msgCtx.isDm,
|
|
1674
|
-
config,
|
|
1675
|
-
runtime,
|
|
1676
|
-
accountId: ctx.accountId,
|
|
1677
|
-
pairingAdapter: bitrix24Plugin.pairing,
|
|
1678
|
-
sendReply: async (text: string) => {
|
|
1679
|
-
await sendService.sendText(sendCtx, text, { convertMarkdown: false });
|
|
1680
|
-
},
|
|
1681
|
-
logger,
|
|
1682
2323
|
});
|
|
2324
|
+
const historyKey = conversation.historyKey;
|
|
2325
|
+
const historyLimit = resolveHistoryLimit(config);
|
|
2326
|
+
const groupAccess = msgCtx.isGroup
|
|
2327
|
+
? resolveGroupAccess({
|
|
2328
|
+
config,
|
|
2329
|
+
dialogId: msgCtx.chatId,
|
|
2330
|
+
chatId: msgCtx.chatInternalId,
|
|
2331
|
+
})
|
|
2332
|
+
: null;
|
|
2333
|
+
const agentWatchRules = config.agentMode && msgCtx.eventScope === 'user'
|
|
2334
|
+
? resolveAgentWatchRules({
|
|
2335
|
+
config,
|
|
2336
|
+
dialogId: msgCtx.chatId,
|
|
2337
|
+
chatId: msgCtx.chatInternalId,
|
|
2338
|
+
})
|
|
2339
|
+
: [];
|
|
2340
|
+
const watchRule = msgCtx.isGroup && groupAccess?.groupAllowed
|
|
2341
|
+
? findMatchingWatchRule(msgCtx, groupAccess?.watch)
|
|
2342
|
+
: undefined;
|
|
2343
|
+
const activeWatchRule = watchRule?.mode === 'notifyOwnerDm'
|
|
2344
|
+
&& webhookOwnerId
|
|
2345
|
+
&& msgCtx.senderId === webhookOwnerId
|
|
2346
|
+
? undefined
|
|
2347
|
+
: watchRule;
|
|
2348
|
+
const agentWatchRule = msgCtx.eventScope === 'user'
|
|
2349
|
+
? findMatchingWatchRule(msgCtx, agentWatchRules)
|
|
2350
|
+
: undefined;
|
|
2351
|
+
|
|
2352
|
+
if (msgCtx.eventScope === 'user') {
|
|
2353
|
+
const isBotDialogUserEvent = msgCtx.isDm && msgCtx.chatId === String(bot.botId);
|
|
2354
|
+
const isBotAuthoredUserEvent = msgCtx.senderId === String(bot.botId);
|
|
2355
|
+
|
|
2356
|
+
if (isBotDialogUserEvent || isBotAuthoredUserEvent) {
|
|
2357
|
+
logger.debug('Skipping agent-mode user event for bot-owned conversation', {
|
|
2358
|
+
senderId: msgCtx.senderId,
|
|
2359
|
+
chatId: msgCtx.chatId,
|
|
2360
|
+
messageId: msgCtx.messageId,
|
|
2361
|
+
isBotDialogUserEvent,
|
|
2362
|
+
isBotAuthoredUserEvent,
|
|
2363
|
+
});
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
1683
2366
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2367
|
+
appendMessageToHistory({
|
|
2368
|
+
historyCache,
|
|
2369
|
+
historyKey,
|
|
2370
|
+
historyLimit,
|
|
2371
|
+
msgCtx,
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
if (webhookOwnerId && msgCtx.senderId !== webhookOwnerId && agentWatchRule?.mode === 'notifyOwnerDm') {
|
|
2375
|
+
await notifyWebhookOwnerAboutWatchMatch(msgCtx, agentWatchRule);
|
|
2376
|
+
logger.debug('User-event watch matched and notified webhook owner in DM', {
|
|
2377
|
+
senderId: msgCtx.senderId,
|
|
2378
|
+
chatId: msgCtx.chatId,
|
|
2379
|
+
messageId: msgCtx.messageId,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (msgCtx.isGroup && groupAccess?.groupPolicy === 'disabled') {
|
|
2386
|
+
logger.info('Group chat is disabled by policy, leaving chat', {
|
|
2387
|
+
chatId: msgCtx.chatId,
|
|
2388
|
+
senderId: msgCtx.senderId,
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
try {
|
|
2392
|
+
await sendService.sendText(sendCtx, groupChatUnsupported(msgCtx.language));
|
|
2393
|
+
await api.leaveChat(webhookUrl, bot, msgCtx.chatId);
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
logger.error('Failed to leave disabled group chat', err);
|
|
2396
|
+
}
|
|
1688
2397
|
return;
|
|
1689
2398
|
}
|
|
1690
2399
|
|
|
1691
2400
|
await sendService.markRead(sendCtx, toMessageId(msgCtx.messageId));
|
|
1692
|
-
await sendService.sendTyping(sendCtx);
|
|
1693
2401
|
|
|
1694
|
-
if (
|
|
1695
|
-
|
|
2402
|
+
if (
|
|
2403
|
+
msgCtx.isGroup
|
|
2404
|
+
&& !activeWatchRule
|
|
2405
|
+
&& groupAccess?.requireMention
|
|
2406
|
+
&& !msgCtx.wasMentioned
|
|
2407
|
+
) {
|
|
2408
|
+
appendMessageToHistory({
|
|
2409
|
+
historyCache,
|
|
2410
|
+
historyKey,
|
|
2411
|
+
historyLimit,
|
|
2412
|
+
msgCtx,
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
logger.info('Skipping group message without mention', {
|
|
2416
|
+
chatId: msgCtx.chatId,
|
|
2417
|
+
senderId: msgCtx.senderId,
|
|
2418
|
+
messageId: msgCtx.messageId,
|
|
2419
|
+
});
|
|
1696
2420
|
return;
|
|
1697
2421
|
}
|
|
1698
2422
|
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
});
|
|
1707
|
-
await processAllowedMessage(mergeForwardedMessageContext(pendingForwardContext, msgCtx));
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
2423
|
+
if (activeWatchRule?.mode === 'notifyOwnerDm') {
|
|
2424
|
+
appendMessageToHistory({
|
|
2425
|
+
historyCache,
|
|
2426
|
+
historyKey,
|
|
2427
|
+
historyLimit,
|
|
2428
|
+
msgCtx,
|
|
2429
|
+
});
|
|
1710
2430
|
|
|
1711
|
-
|
|
2431
|
+
await notifyWebhookOwnerAboutWatchMatch(msgCtx, activeWatchRule);
|
|
2432
|
+
logger.debug('Group watch matched and notified webhook owner in DM', {
|
|
1712
2433
|
senderId: msgCtx.senderId,
|
|
1713
2434
|
chatId: msgCtx.chatId,
|
|
1714
2435
|
messageId: msgCtx.messageId,
|
|
1715
2436
|
});
|
|
1716
|
-
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const accessResult = activeWatchRule
|
|
2441
|
+
? 'allow'
|
|
2442
|
+
: msgCtx.isGroup
|
|
2443
|
+
? await checkGroupAccessWithPairing({
|
|
2444
|
+
senderId: msgCtx.senderId,
|
|
2445
|
+
dialogId: msgCtx.chatId,
|
|
2446
|
+
chatId: msgCtx.chatInternalId,
|
|
2447
|
+
config,
|
|
2448
|
+
runtime,
|
|
2449
|
+
accountId: ctx.accountId,
|
|
2450
|
+
pairingAdapter: bitrix24Plugin.pairing,
|
|
2451
|
+
logger,
|
|
2452
|
+
})
|
|
2453
|
+
: await checkAccessWithPairing({
|
|
2454
|
+
senderId: msgCtx.senderId,
|
|
2455
|
+
dialogId: msgCtx.chatId,
|
|
2456
|
+
isDirect: msgCtx.isDm,
|
|
2457
|
+
config,
|
|
2458
|
+
runtime,
|
|
2459
|
+
accountId: ctx.accountId,
|
|
2460
|
+
pairingAdapter: bitrix24Plugin.pairing,
|
|
2461
|
+
sendReply: async (text: string) => {
|
|
2462
|
+
await sendService.sendText(sendCtx, text, { convertMarkdown: false });
|
|
2463
|
+
},
|
|
2464
|
+
logger,
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
if (accessResult === 'deny') {
|
|
2468
|
+
if (msgCtx.isGroup) {
|
|
2469
|
+
appendMessageToHistory({
|
|
2470
|
+
historyCache,
|
|
2471
|
+
historyKey,
|
|
2472
|
+
historyLimit,
|
|
2473
|
+
msgCtx,
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
if (!msgCtx.wasMentioned) {
|
|
2477
|
+
logger.debug('Group message blocked silently without mention', {
|
|
2478
|
+
senderId: msgCtx.senderId,
|
|
2479
|
+
chatId: msgCtx.chatId,
|
|
2480
|
+
messageId: msgCtx.messageId,
|
|
2481
|
+
});
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
await maybeReactToDeniedMention(msgCtx);
|
|
2487
|
+
|
|
2488
|
+
const noticeKey = msgCtx.isDm
|
|
2489
|
+
? `deny:${msgCtx.chatId}`
|
|
2490
|
+
: `group-deny:${msgCtx.chatId}:${msgCtx.senderId}`;
|
|
2491
|
+
const policy = msgCtx.isDm
|
|
2492
|
+
? config.dmPolicy
|
|
2493
|
+
: groupAccess?.groupPolicy;
|
|
2494
|
+
|
|
2495
|
+
await maybeSendDialogNotice(
|
|
2496
|
+
noticeKey,
|
|
1717
2497
|
sendCtx,
|
|
1718
|
-
|
|
1719
|
-
|
|
2498
|
+
buildAccessDeniedNotice(msgCtx.language, policy, {
|
|
2499
|
+
hasAllowList: msgCtx.isDm
|
|
2500
|
+
? normalizeAllowList(config.allowFrom).length > 0
|
|
2501
|
+
: Boolean(groupAccess?.senderAllowFrom.length),
|
|
2502
|
+
}),
|
|
1720
2503
|
);
|
|
2504
|
+
logger.debug('Message blocked (deny)', { senderId: msgCtx.senderId, chatId: msgCtx.chatId });
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
if (accessResult === 'pairing') {
|
|
2509
|
+
if (msgCtx.isGroup) {
|
|
2510
|
+
appendMessageToHistory({
|
|
2511
|
+
historyCache,
|
|
2512
|
+
historyKey,
|
|
2513
|
+
historyLimit,
|
|
2514
|
+
msgCtx,
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
await maybeSendDialogNotice(
|
|
2518
|
+
`group-pairing:${msgCtx.chatId}:${msgCtx.senderId}`,
|
|
2519
|
+
sendCtx,
|
|
2520
|
+
groupPairingPending(msgCtx.language),
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
logger.debug(`Message blocked (${accessResult})`, { senderId: msgCtx.senderId });
|
|
1721
2524
|
return;
|
|
1722
2525
|
}
|
|
1723
2526
|
|
|
1724
|
-
|
|
1725
|
-
|
|
2527
|
+
await sendService.sendTyping(sendCtx);
|
|
2528
|
+
|
|
2529
|
+
if (msgCtx.isForwarded) {
|
|
2530
|
+
if (pendingForwardContext) {
|
|
2531
|
+
const hydratedForwardContext = await hydrateForwardedMessageContext(msgCtx);
|
|
2532
|
+
const mergedForwardContext = mergeForwardedMessageContext(
|
|
2533
|
+
pendingForwardContext,
|
|
2534
|
+
hydratedForwardContext,
|
|
2535
|
+
);
|
|
2536
|
+
await processAllowedMessage(mergedForwardContext);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
logger.info('Hydrating forwarded message context from Bitrix24 API', {
|
|
1726
2541
|
senderId: msgCtx.senderId,
|
|
1727
2542
|
chatId: msgCtx.chatId,
|
|
1728
2543
|
messageId: msgCtx.messageId,
|
|
1729
|
-
replyToMessageId: msgCtx.replyToMessageId,
|
|
1730
2544
|
});
|
|
1731
|
-
await
|
|
1732
|
-
sendCtx,
|
|
1733
|
-
replyMessageUnsupported(msgCtx.language),
|
|
1734
|
-
{ convertMarkdown: false },
|
|
1735
|
-
);
|
|
2545
|
+
await processAllowedMessage(await hydrateForwardedMessageContext(msgCtx));
|
|
1736
2546
|
return;
|
|
1737
2547
|
}
|
|
1738
2548
|
|
|
@@ -1753,11 +2563,16 @@ export const bitrix24Plugin = {
|
|
|
1753
2563
|
commandText,
|
|
1754
2564
|
senderId,
|
|
1755
2565
|
dialogId,
|
|
2566
|
+
chatId,
|
|
1756
2567
|
chatType,
|
|
1757
2568
|
messageId,
|
|
1758
2569
|
} = cmdCtx;
|
|
1759
2570
|
const isDm = chatType === 'P';
|
|
1760
|
-
const
|
|
2571
|
+
const conversation = resolveConversationRef({
|
|
2572
|
+
accountId: ctx.accountId,
|
|
2573
|
+
dialogId,
|
|
2574
|
+
isDirect: isDm,
|
|
2575
|
+
});
|
|
1761
2576
|
|
|
1762
2577
|
logger.info('Inbound command', {
|
|
1763
2578
|
commandId,
|
|
@@ -1766,13 +2581,14 @@ export const bitrix24Plugin = {
|
|
|
1766
2581
|
commandText,
|
|
1767
2582
|
senderId,
|
|
1768
2583
|
dialogId,
|
|
1769
|
-
|
|
2584
|
+
chatId,
|
|
2585
|
+
conversationDialogId: conversation.dialogId,
|
|
1770
2586
|
});
|
|
1771
2587
|
|
|
1772
2588
|
const sendCtx: SendContext = {
|
|
1773
2589
|
webhookUrl,
|
|
1774
2590
|
bot,
|
|
1775
|
-
dialogId:
|
|
2591
|
+
dialogId: conversation.dialogId,
|
|
1776
2592
|
};
|
|
1777
2593
|
|
|
1778
2594
|
let runtime;
|
|
@@ -1794,28 +2610,46 @@ export const bitrix24Plugin = {
|
|
|
1794
2610
|
commandDialogId: dialogId,
|
|
1795
2611
|
}
|
|
1796
2612
|
: null;
|
|
2613
|
+
const groupAccess = !isDm
|
|
2614
|
+
? resolveGroupAccess({
|
|
2615
|
+
config,
|
|
2616
|
+
dialogId,
|
|
2617
|
+
chatId,
|
|
2618
|
+
})
|
|
2619
|
+
: null;
|
|
1797
2620
|
|
|
1798
2621
|
// Access control
|
|
1799
2622
|
let accessResult;
|
|
1800
2623
|
try {
|
|
1801
|
-
accessResult =
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2624
|
+
accessResult = !isDm
|
|
2625
|
+
? await checkGroupAccessWithPairing({
|
|
2626
|
+
senderId,
|
|
2627
|
+
dialogId,
|
|
2628
|
+
chatId,
|
|
2629
|
+
config,
|
|
2630
|
+
runtime,
|
|
2631
|
+
accountId: ctx.accountId,
|
|
2632
|
+
pairingAdapter: bitrix24Plugin.pairing,
|
|
2633
|
+
logger,
|
|
2634
|
+
})
|
|
2635
|
+
: await checkAccessWithPairing({
|
|
2636
|
+
senderId,
|
|
2637
|
+
dialogId,
|
|
2638
|
+
isDirect: isDm,
|
|
2639
|
+
config,
|
|
2640
|
+
runtime,
|
|
2641
|
+
accountId: ctx.accountId,
|
|
2642
|
+
pairingAdapter: bitrix24Plugin.pairing,
|
|
2643
|
+
sendReply: async (text: string) => {
|
|
2644
|
+
if (commandSendCtx) {
|
|
2645
|
+
await sendService.answerCommandText(commandSendCtx, text, { convertMarkdown: false });
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
1814
2648
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2649
|
+
await sendService.sendText(sendCtx, text, { convertMarkdown: false });
|
|
2650
|
+
},
|
|
2651
|
+
logger,
|
|
2652
|
+
});
|
|
1819
2653
|
} catch (err) {
|
|
1820
2654
|
logger.error('Access check failed for command', err);
|
|
1821
2655
|
return;
|
|
@@ -1825,7 +2659,16 @@ export const bitrix24Plugin = {
|
|
|
1825
2659
|
logger.warn('Command event has invalid messageId, skipping response', { commandId, messageId, dialogId });
|
|
1826
2660
|
return;
|
|
1827
2661
|
}
|
|
1828
|
-
const canMarkRead =
|
|
2662
|
+
const canMarkRead = true;
|
|
2663
|
+
|
|
2664
|
+
if (!isDm && groupAccess?.groupPolicy === 'disabled') {
|
|
2665
|
+
await sendService.answerCommandText(
|
|
2666
|
+
commandSendCtx,
|
|
2667
|
+
groupChatUnsupported(cmdCtx.language),
|
|
2668
|
+
{ convertMarkdown: false },
|
|
2669
|
+
);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
1829
2672
|
|
|
1830
2673
|
await sendService.sendStatus(sendCtx, 'IMBOT_AGENT_ACTION_THINKING', 8);
|
|
1831
2674
|
|
|
@@ -1835,7 +2678,15 @@ export const bitrix24Plugin = {
|
|
|
1835
2678
|
}
|
|
1836
2679
|
await sendService.answerCommandText(
|
|
1837
2680
|
commandSendCtx,
|
|
1838
|
-
|
|
2681
|
+
buildAccessDeniedNotice(
|
|
2682
|
+
cmdCtx.language,
|
|
2683
|
+
isDm ? config.dmPolicy : groupAccess?.groupPolicy,
|
|
2684
|
+
{
|
|
2685
|
+
hasAllowList: isDm
|
|
2686
|
+
? normalizeAllowList(config.allowFrom).length > 0
|
|
2687
|
+
: Boolean(groupAccess?.senderAllowFrom.length),
|
|
2688
|
+
},
|
|
2689
|
+
),
|
|
1839
2690
|
{ convertMarkdown: false },
|
|
1840
2691
|
);
|
|
1841
2692
|
logger.debug('Command blocked (deny)', { senderId, dialogId });
|
|
@@ -1846,16 +2697,23 @@ export const bitrix24Plugin = {
|
|
|
1846
2697
|
await sendService.markRead(sendCtx, commandMessageId);
|
|
1847
2698
|
}
|
|
1848
2699
|
|
|
1849
|
-
if (accessResult
|
|
2700
|
+
if (accessResult === 'pairing') {
|
|
2701
|
+
if (!isDm) {
|
|
2702
|
+
await sendService.answerCommandText(
|
|
2703
|
+
commandSendCtx,
|
|
2704
|
+
groupPairingPending(cmdCtx.language),
|
|
2705
|
+
{ convertMarkdown: false },
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
1850
2708
|
logger.debug(`Command blocked (${accessResult})`, { senderId });
|
|
1851
2709
|
return;
|
|
1852
2710
|
}
|
|
1853
2711
|
|
|
1854
|
-
await directTextCoalescer.flush(ctx.accountId,
|
|
2712
|
+
await directTextCoalescer.flush(ctx.accountId, conversation.dialogId);
|
|
1855
2713
|
|
|
1856
2714
|
if (commandName === 'help' || commandName === 'commands') {
|
|
1857
|
-
await sendService.
|
|
1858
|
-
|
|
2715
|
+
await sendService.answerCommandText(
|
|
2716
|
+
commandSendCtx,
|
|
1859
2717
|
buildCommandsHelpText(cmdCtx.language, { concise: commandName === 'help' }),
|
|
1860
2718
|
{ keyboard: DEFAULT_COMMAND_KEYBOARD, convertMarkdown: false },
|
|
1861
2719
|
);
|
|
@@ -1866,7 +2724,7 @@ export const bitrix24Plugin = {
|
|
|
1866
2724
|
cfg,
|
|
1867
2725
|
channel: 'bitrix24',
|
|
1868
2726
|
accountId: ctx.accountId,
|
|
1869
|
-
peer:
|
|
2727
|
+
peer: conversation.peer,
|
|
1870
2728
|
});
|
|
1871
2729
|
|
|
1872
2730
|
const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
|
|
@@ -1878,8 +2736,8 @@ export const bitrix24Plugin = {
|
|
|
1878
2736
|
CommandBody: commandText,
|
|
1879
2737
|
CommandAuthorized: true,
|
|
1880
2738
|
CommandSource: 'native',
|
|
1881
|
-
CommandTargetSessionKey:
|
|
1882
|
-
From:
|
|
2739
|
+
CommandTargetSessionKey: buildConversationSessionKey(route.sessionKey, conversation),
|
|
2740
|
+
From: conversation.address,
|
|
1883
2741
|
To: `slash:${senderId}`,
|
|
1884
2742
|
SessionKey: slashSessionKey,
|
|
1885
2743
|
AccountId: route.accountId,
|
|
@@ -1893,7 +2751,7 @@ export const bitrix24Plugin = {
|
|
|
1893
2751
|
Timestamp: Date.now(),
|
|
1894
2752
|
WasMentioned: true,
|
|
1895
2753
|
OriginatingChannel: 'bitrix24',
|
|
1896
|
-
OriginatingTo:
|
|
2754
|
+
OriginatingTo: conversation.address,
|
|
1897
2755
|
});
|
|
1898
2756
|
|
|
1899
2757
|
const replyStatusHeartbeat = createReplyStatusHeartbeat({
|
|
@@ -1921,14 +2779,6 @@ export const bitrix24Plugin = {
|
|
|
1921
2779
|
});
|
|
1922
2780
|
if (!commandReplyDelivered) {
|
|
1923
2781
|
commandReplyDelivered = true;
|
|
1924
|
-
if (isDm) {
|
|
1925
|
-
await sendService.sendText(sendCtx, formattedPayload.text, {
|
|
1926
|
-
keyboard,
|
|
1927
|
-
convertMarkdown: formattedPayload.convertMarkdown,
|
|
1928
|
-
});
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
2782
|
await sendService.answerCommandText(commandSendCtx, formattedPayload.text, {
|
|
1933
2783
|
keyboard,
|
|
1934
2784
|
convertMarkdown: formattedPayload.convertMarkdown,
|
|
@@ -1958,12 +2808,16 @@ export const bitrix24Plugin = {
|
|
|
1958
2808
|
},
|
|
1959
2809
|
|
|
1960
2810
|
onJoinChat: async (joinCtx: FetchJoinChatContext) => {
|
|
1961
|
-
const { dialogId, chatType, language } = joinCtx;
|
|
1962
|
-
logger.info('Bot joined chat', { dialogId, chatType });
|
|
2811
|
+
const { senderId, dialogId, chatId, chatType, language } = joinCtx;
|
|
2812
|
+
logger.info('Bot joined chat', { senderId, dialogId, chatId, chatType });
|
|
1963
2813
|
|
|
1964
2814
|
if (!dialogId) return;
|
|
2815
|
+
const isGroupChat = chatType === 'chat' || chatType === 'open';
|
|
2816
|
+
const groupAccess = isGroupChat
|
|
2817
|
+
? resolveGroupAccess({ config, dialogId, chatId })
|
|
2818
|
+
: null;
|
|
1965
2819
|
|
|
1966
|
-
if (shouldSkipJoinChatWelcome({
|
|
2820
|
+
if (!isGroupChat && shouldSkipJoinChatWelcome({
|
|
1967
2821
|
dialogId,
|
|
1968
2822
|
chatType,
|
|
1969
2823
|
webhookUrl,
|
|
@@ -1979,9 +2833,8 @@ export const bitrix24Plugin = {
|
|
|
1979
2833
|
dialogId,
|
|
1980
2834
|
};
|
|
1981
2835
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
logger.info('Group chat not supported, leaving', { dialogId });
|
|
2836
|
+
if (isGroupChat && (!groupAccess?.groupAllowed || groupAccess.groupPolicy === 'disabled')) {
|
|
2837
|
+
logger.info('Group chat blocked by policy, leaving', { dialogId });
|
|
1985
2838
|
try {
|
|
1986
2839
|
await sendService.sendText(sendCtx, groupChatUnsupported(language));
|
|
1987
2840
|
await api.leaveChat(webhookUrl, bot, dialogId);
|
|
@@ -1991,6 +2844,45 @@ export const bitrix24Plugin = {
|
|
|
1991
2844
|
return;
|
|
1992
2845
|
}
|
|
1993
2846
|
|
|
2847
|
+
if (isGroupChat) {
|
|
2848
|
+
const runtime = getBitrix24Runtime();
|
|
2849
|
+
const inviterAccessResult = await checkGroupAccessPassive({
|
|
2850
|
+
senderId,
|
|
2851
|
+
dialogId,
|
|
2852
|
+
chatId,
|
|
2853
|
+
config,
|
|
2854
|
+
runtime,
|
|
2855
|
+
accountId: ctx.accountId,
|
|
2856
|
+
logger,
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
if (inviterAccessResult !== 'allow') {
|
|
2860
|
+
const noticeText = inviterAccessResult === 'pairing'
|
|
2861
|
+
? groupPairingPending(language)
|
|
2862
|
+
: buildAccessDeniedNotice(language, groupAccess?.groupPolicy, {
|
|
2863
|
+
hasAllowList: Boolean(groupAccess?.senderAllowFrom.length),
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
logger.info('Leaving group chat invited by user without access', {
|
|
2867
|
+
dialogId,
|
|
2868
|
+
chatId,
|
|
2869
|
+
senderId,
|
|
2870
|
+
inviterAccessResult,
|
|
2871
|
+
});
|
|
2872
|
+
|
|
2873
|
+
try {
|
|
2874
|
+
await sendService.sendText(sendCtx, noticeText, { convertMarkdown: false });
|
|
2875
|
+
await api.leaveChat(webhookUrl, bot, dialogId);
|
|
2876
|
+
} catch (err) {
|
|
2877
|
+
logger.error('Failed to leave group chat after inviter access check', err);
|
|
2878
|
+
}
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
logger.info('Group chat enabled by policy, skipping auto-welcome', { dialogId });
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
1994
2886
|
if (welcomedDialogs.has(dialogId)) {
|
|
1995
2887
|
logger.info('Skipping duplicate welcome for already welcomed dialog', { dialogId });
|
|
1996
2888
|
return;
|
|
@@ -2032,6 +2924,7 @@ export const bitrix24Plugin = {
|
|
|
2032
2924
|
accountId: ctx.accountId,
|
|
2033
2925
|
pollingIntervalMs: config.pollingIntervalMs ?? 3000,
|
|
2034
2926
|
pollingFastIntervalMs: config.pollingFastIntervalMs ?? 100,
|
|
2927
|
+
withUserEvents: Boolean(config.agentMode),
|
|
2035
2928
|
onEvent: async (event: B24V2FetchEventItem) => {
|
|
2036
2929
|
const fetchCtx: FetchContext = {
|
|
2037
2930
|
webhookUrl,
|