@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/commands.ts
CHANGED
|
@@ -43,9 +43,9 @@ const COMMAND_GROUP_LABELS = {
|
|
|
43
43
|
export const OPENCLAW_COMMANDS: BotCommandDef[] = [
|
|
44
44
|
// ── Status ──
|
|
45
45
|
{ command: 'help', en: 'Show available commands', ru: 'Показать доступные команды', group: 'status' },
|
|
46
|
-
{ command: 'commands', en: 'List all slash commands', ru: '
|
|
46
|
+
{ command: 'commands', en: 'List all slash commands', ru: 'Показать все команды', group: 'status' },
|
|
47
47
|
{ command: 'status', en: 'Show current status', ru: 'Показать текущий статус', group: 'status' },
|
|
48
|
-
{ command: 'context', en: 'Explain how context is built', ru: '
|
|
48
|
+
{ command: 'context', en: 'Explain how context is built', ru: 'Объяснить, как формируется контекст', group: 'status' },
|
|
49
49
|
{ command: 'whoami', en: 'Show your sender ID', ru: 'Показать ваш ID', group: 'status' },
|
|
50
50
|
{ command: 'usage', en: 'Usage and cost summary', ru: 'Использование и стоимость', params: 'off | tokens | full | cost', group: 'status' },
|
|
51
51
|
|
|
@@ -57,8 +57,8 @@ export const OPENCLAW_COMMANDS: BotCommandDef[] = [
|
|
|
57
57
|
{ command: 'session', en: 'Manage session settings', ru: 'Настройки сессии', params: 'ttl | ...', group: 'session' },
|
|
58
58
|
|
|
59
59
|
// ── Options ──
|
|
60
|
-
{ command: 'model', en: 'Show or set the model', ru: '
|
|
61
|
-
{ command: 'models', en: 'List available models', ru: '
|
|
60
|
+
{ command: 'model', en: 'Show or set the model', ru: 'Показать или сменить модель', params: 'model name', group: 'options' },
|
|
61
|
+
{ command: 'models', en: 'List available models', ru: 'Показать модели', params: 'provider', group: 'options' },
|
|
62
62
|
{ command: 'think', en: 'Set thinking level', ru: 'Уровень размышлений', params: 'off | low | medium | high', group: 'options' },
|
|
63
63
|
{ command: 'verbose', en: 'Toggle verbose mode', ru: 'Подробный режим', params: 'on | off', group: 'options' },
|
|
64
64
|
{ command: 'reasoning', en: 'Toggle reasoning visibility', ru: 'Видимость рассуждений', params: 'on | off | stream', group: 'options' },
|
|
@@ -67,9 +67,9 @@ export const OPENCLAW_COMMANDS: BotCommandDef[] = [
|
|
|
67
67
|
{ command: 'queue', en: 'Adjust queue settings', ru: 'Настройки очереди', params: 'mode | debounce | cap | drop', group: 'options' },
|
|
68
68
|
|
|
69
69
|
// ── Management ──
|
|
70
|
-
{ command: 'config', en: 'Show or set config values', ru: '
|
|
70
|
+
{ command: 'config', en: 'Show or set config values', ru: 'Показать или изменить конфигурацию', params: 'show | get | set | unset', group: 'management' },
|
|
71
71
|
{ command: 'debug', en: 'Set runtime debug overrides', ru: 'Отладочные настройки', params: 'show | reset | set | unset', group: 'management' },
|
|
72
|
-
{ command: 'approve', en: 'Approve or deny exec requests', ru: '
|
|
72
|
+
{ command: 'approve', en: 'Approve or deny exec requests', ru: 'Одобрить или отклонить запросы', group: 'management' },
|
|
73
73
|
{ command: 'activation', en: 'Set group activation mode', ru: 'Режим активации в группах', params: 'mention | always', group: 'management' },
|
|
74
74
|
{ command: 'send', en: 'Set send policy', ru: 'Политика отправки', params: 'on | off | inherit', group: 'management' },
|
|
75
75
|
{ command: 'subagents', en: 'Manage subagent runs', ru: 'Управление субагентами', group: 'management' },
|
|
@@ -153,7 +153,7 @@ export function formatModelsCommandReply(
|
|
|
153
153
|
const isRu = lang?.toLowerCase().startsWith('ru') ?? false;
|
|
154
154
|
const header = isRu ? '[B]Провайдеры[/B]' : '[B]Providers[/B]';
|
|
155
155
|
const showModelsLabel = isRu
|
|
156
|
-
? `[COLOR=${COMMAND_META_COLOR}]
|
|
156
|
+
? `[COLOR=${COMMAND_META_COLOR}]Модели провайдера:[/COLOR]`
|
|
157
157
|
: `[COLOR=${COMMAND_META_COLOR}]Show provider models:[/COLOR]`;
|
|
158
158
|
const switchModelLabel = isRu
|
|
159
159
|
? `[COLOR=${COMMAND_META_COLOR}]Сменить модель:[/COLOR]`
|
package/src/config-schema.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
const GroupSchema = z.object({
|
|
4
|
+
groupPolicy: z.enum(['disabled', 'webhookUser', 'pairing', 'allowlist', 'open']).optional(),
|
|
5
|
+
requireMention: z.boolean().optional().default(true),
|
|
6
|
+
allowFrom: z.array(z.string()).optional(),
|
|
7
|
+
watch: z.array(z.object({
|
|
8
|
+
userId: z.string().min(1),
|
|
9
|
+
topics: z.array(z.string().min(1)).optional(),
|
|
10
|
+
mode: z.enum(['reply', 'notifyOwnerDm']).optional(),
|
|
11
|
+
})).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const AgentWatchRuleSchema = z.object({
|
|
15
|
+
userId: z.string().min(1),
|
|
16
|
+
topics: z.array(z.string().min(1)).optional(),
|
|
17
|
+
mode: z.enum(['notifyOwnerDm']).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
3
20
|
const AccountSchema = z.object({
|
|
4
21
|
enabled: z.boolean().optional().default(true),
|
|
5
22
|
webhookUrl: z.string().url().optional(),
|
|
@@ -13,7 +30,13 @@ const AccountSchema = z.object({
|
|
|
13
30
|
agentMode: z.boolean().optional().default(false),
|
|
14
31
|
pollingIntervalMs: z.number().int().min(500).optional().default(3000),
|
|
15
32
|
pollingFastIntervalMs: z.number().int().min(50).optional().default(100),
|
|
16
|
-
dmPolicy: z.enum(['pairing', 'webhookUser']).optional().default('webhookUser'),
|
|
33
|
+
dmPolicy: z.enum(['pairing', 'webhookUser', 'allowlist', 'open']).optional().default('webhookUser'),
|
|
34
|
+
groupPolicy: z.enum(['disabled', 'webhookUser', 'pairing', 'allowlist', 'open']).optional().default('webhookUser'),
|
|
35
|
+
groupAllowFrom: z.array(z.string()).optional(),
|
|
36
|
+
requireMention: z.boolean().optional().default(true),
|
|
37
|
+
historyLimit: z.number().int().min(0).optional().default(100),
|
|
38
|
+
groups: z.record(z.string(), GroupSchema).optional(),
|
|
39
|
+
agentWatch: z.record(z.string(), z.array(AgentWatchRuleSchema)).optional(),
|
|
17
40
|
showTyping: z.boolean().optional().default(true),
|
|
18
41
|
streamUpdates: z.boolean().optional().default(false),
|
|
19
42
|
updateIntervalMs: z.number().int().min(500).optional().default(10000),
|
package/src/config.ts
CHANGED
|
@@ -51,15 +51,17 @@ export function resolveAccount(
|
|
|
51
51
|
enabled: boolean;
|
|
52
52
|
} {
|
|
53
53
|
const id = accountId || DEFAULT_ACCOUNT_ID;
|
|
54
|
-
|
|
54
|
+
let config = getConfig(cfg, id);
|
|
55
55
|
|
|
56
|
-
// Validate config against Zod schema
|
|
56
|
+
// Validate and normalize config against Zod schema.
|
|
57
57
|
if (config.webhookUrl) {
|
|
58
58
|
const result = Bitrix24ConfigSchema.safeParse(config);
|
|
59
59
|
if (!result.success) {
|
|
60
60
|
const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
61
61
|
throw new Error(`[bitrix24] Invalid config for account "${id}": ${issues}`);
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
config = result.data;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
return {
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Bitrix24AccountConfig,
|
|
3
|
+
Bitrix24AgentWatchConfig,
|
|
4
|
+
Bitrix24GroupConfig,
|
|
5
|
+
Bitrix24GroupPolicy,
|
|
6
|
+
Bitrix24GroupWatchConfig,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
import type { PluginRuntime, ChannelPairingAdapter } from './runtime.js';
|
|
9
|
+
import type { AccessResult } from './access-control.js';
|
|
10
|
+
import {
|
|
11
|
+
getWebhookUserId,
|
|
12
|
+
isIdentityAllowed,
|
|
13
|
+
normalizeAllowEntry,
|
|
14
|
+
normalizeAllowList,
|
|
15
|
+
} from './access-control.js';
|
|
16
|
+
|
|
17
|
+
export interface ResolvedBitrix24GroupAccess {
|
|
18
|
+
groupPolicy: Bitrix24GroupPolicy;
|
|
19
|
+
requireMention: boolean;
|
|
20
|
+
senderAllowFrom: string[];
|
|
21
|
+
watch: Bitrix24GroupWatchConfig[];
|
|
22
|
+
matchedGroupKey?: string;
|
|
23
|
+
matchedGroupConfig?: Bitrix24GroupConfig;
|
|
24
|
+
groupAllowed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeWatchRules<T extends { userId: string; topics?: string[]; mode?: string }>(
|
|
28
|
+
rules: T[] | undefined,
|
|
29
|
+
): T[] {
|
|
30
|
+
return (rules ?? []).map((rule) => ({
|
|
31
|
+
...rule,
|
|
32
|
+
userId: normalizeAllowEntry(String(rule.userId)),
|
|
33
|
+
topics: Array.isArray(rule.topics)
|
|
34
|
+
? rule.topics.map((topic) => String(topic).trim()).filter(Boolean)
|
|
35
|
+
: undefined,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeGroupEntry(entry: string): string {
|
|
40
|
+
const normalized = String(entry).trim().toLowerCase();
|
|
41
|
+
if (/^\d+$/.test(normalized)) {
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
if (/^chat\d+$/.test(normalized)) {
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeGroupAllowList(entries: string[] | undefined): string[] {
|
|
51
|
+
if (!Array.isArray(entries)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [...new Set(
|
|
56
|
+
entries
|
|
57
|
+
.map((entry) => normalizeGroupEntry(String(entry)))
|
|
58
|
+
.filter(Boolean),
|
|
59
|
+
)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildGroupMatchKeys(params: {
|
|
63
|
+
dialogId?: string;
|
|
64
|
+
chatId?: string;
|
|
65
|
+
}): string[] {
|
|
66
|
+
const keys: string[] = [];
|
|
67
|
+
const normalizedDialogId = params.dialogId ? normalizeGroupEntry(params.dialogId) : '';
|
|
68
|
+
const normalizedChatId = params.chatId ? normalizeGroupEntry(params.chatId) : '';
|
|
69
|
+
|
|
70
|
+
if (normalizedDialogId) {
|
|
71
|
+
keys.push(normalizedDialogId);
|
|
72
|
+
const dialogMatch = normalizedDialogId.match(/^chat(\d+)$/);
|
|
73
|
+
if (dialogMatch) {
|
|
74
|
+
keys.push(dialogMatch[1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (normalizedChatId) {
|
|
79
|
+
keys.push(normalizedChatId);
|
|
80
|
+
if (/^\d+$/.test(normalizedChatId)) {
|
|
81
|
+
keys.push(`chat${normalizedChatId}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return [...new Set(keys)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveGroupAccess(params: {
|
|
89
|
+
config: Bitrix24AccountConfig;
|
|
90
|
+
dialogId?: string;
|
|
91
|
+
chatId?: string;
|
|
92
|
+
}): ResolvedBitrix24GroupAccess {
|
|
93
|
+
const matchKeys = buildGroupMatchKeys(params);
|
|
94
|
+
const groups = params.config.groups ?? {};
|
|
95
|
+
const wildcardGroupConfig = groups['*'];
|
|
96
|
+
let matchedGroupKey: string | undefined;
|
|
97
|
+
let matchedGroupConfig: Bitrix24GroupConfig | undefined;
|
|
98
|
+
|
|
99
|
+
for (const key of matchKeys) {
|
|
100
|
+
if (groups[key]) {
|
|
101
|
+
matchedGroupKey = key;
|
|
102
|
+
matchedGroupConfig = groups[key];
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!matchedGroupConfig && wildcardGroupConfig) {
|
|
108
|
+
matchedGroupKey = '*';
|
|
109
|
+
matchedGroupConfig = wildcardGroupConfig;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const explicitGroupMatched = Boolean(
|
|
113
|
+
matchedGroupKey
|
|
114
|
+
&& matchedGroupKey !== '*'
|
|
115
|
+
&& matchedGroupConfig,
|
|
116
|
+
);
|
|
117
|
+
const groupAllowFrom = normalizeGroupAllowList(params.config.groupAllowFrom);
|
|
118
|
+
const groupAllowed = groupAllowFrom.length === 0
|
|
119
|
+
|| explicitGroupMatched
|
|
120
|
+
|| matchKeys.some((key) => groupAllowFrom.includes(key));
|
|
121
|
+
|
|
122
|
+
const watchRules = [
|
|
123
|
+
...(matchedGroupKey && matchedGroupKey !== '*' ? (matchedGroupConfig?.watch ?? []) : []),
|
|
124
|
+
...(wildcardGroupConfig?.watch ?? []),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
groupPolicy: matchedGroupConfig?.groupPolicy ?? params.config.groupPolicy ?? 'webhookUser',
|
|
129
|
+
requireMention: matchedGroupConfig?.requireMention ?? params.config.requireMention ?? true,
|
|
130
|
+
senderAllowFrom: normalizeAllowList([
|
|
131
|
+
...(params.config.allowFrom ?? []),
|
|
132
|
+
...(matchedGroupConfig?.allowFrom ?? []),
|
|
133
|
+
]),
|
|
134
|
+
watch: normalizeWatchRules(watchRules).map((rule) => ({
|
|
135
|
+
...rule,
|
|
136
|
+
mode: rule.mode === 'notifyOwnerDm' ? 'notifyOwnerDm' : 'reply',
|
|
137
|
+
})),
|
|
138
|
+
matchedGroupKey,
|
|
139
|
+
matchedGroupConfig,
|
|
140
|
+
groupAllowed,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveAgentWatchRules(params: {
|
|
145
|
+
config: Bitrix24AccountConfig;
|
|
146
|
+
dialogId?: string;
|
|
147
|
+
chatId?: string;
|
|
148
|
+
}): Bitrix24AgentWatchConfig[] {
|
|
149
|
+
const matchKeys = buildGroupMatchKeys(params);
|
|
150
|
+
const scopes = params.config.agentWatch ?? {};
|
|
151
|
+
const resolvedRules: Bitrix24AgentWatchConfig[] = [];
|
|
152
|
+
|
|
153
|
+
for (const key of matchKeys) {
|
|
154
|
+
if (scopes[key]) {
|
|
155
|
+
resolvedRules.push(...normalizeWatchRules(scopes[key]).map((rule) => ({
|
|
156
|
+
...rule,
|
|
157
|
+
mode: 'notifyOwnerDm' as const,
|
|
158
|
+
})));
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (scopes['*']) {
|
|
164
|
+
resolvedRules.push(...normalizeWatchRules(scopes['*']).map((rule) => ({
|
|
165
|
+
...rule,
|
|
166
|
+
mode: 'notifyOwnerDm' as const,
|
|
167
|
+
})));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return resolvedRules;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function checkGroupAccessPassive(params: {
|
|
174
|
+
senderId: string;
|
|
175
|
+
dialogId?: string;
|
|
176
|
+
chatId?: string;
|
|
177
|
+
config: Bitrix24AccountConfig;
|
|
178
|
+
runtime: PluginRuntime;
|
|
179
|
+
accountId: string;
|
|
180
|
+
logger?: { debug: (...args: unknown[]) => void };
|
|
181
|
+
}): Promise<AccessResult> {
|
|
182
|
+
const {
|
|
183
|
+
senderId,
|
|
184
|
+
dialogId,
|
|
185
|
+
chatId,
|
|
186
|
+
config,
|
|
187
|
+
runtime,
|
|
188
|
+
accountId,
|
|
189
|
+
logger,
|
|
190
|
+
} = params;
|
|
191
|
+
const identity = normalizeAllowEntry(String(senderId));
|
|
192
|
+
const group = resolveGroupAccess({ config, dialogId, chatId });
|
|
193
|
+
|
|
194
|
+
if (!group.groupAllowed || group.groupPolicy === 'disabled') {
|
|
195
|
+
logger?.debug('Group passive access denied (group disabled or not allowed)', {
|
|
196
|
+
senderId,
|
|
197
|
+
dialogId,
|
|
198
|
+
chatId,
|
|
199
|
+
groupAllowed: group.groupAllowed,
|
|
200
|
+
groupPolicy: group.groupPolicy,
|
|
201
|
+
});
|
|
202
|
+
return 'deny';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (group.groupPolicy === 'open') {
|
|
206
|
+
return 'allow';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (group.groupPolicy === 'webhookUser') {
|
|
210
|
+
const webhookUserId = getWebhookUserId(config.webhookUrl);
|
|
211
|
+
return webhookUserId && webhookUserId === identity ? 'allow' : 'deny';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (group.groupPolicy === 'allowlist') {
|
|
215
|
+
return isIdentityAllowed(identity, group.senderAllowFrom) ? 'allow' : 'deny';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const storeAllowFrom = await runtime.channel.pairing.readAllowFromStore('bitrix24', '', accountId);
|
|
219
|
+
const approved = [...new Set([
|
|
220
|
+
...group.senderAllowFrom,
|
|
221
|
+
...normalizeAllowList(storeAllowFrom),
|
|
222
|
+
])];
|
|
223
|
+
|
|
224
|
+
return approved.includes(identity) ? 'allow' : 'pairing';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function checkGroupAccessWithPairing(params: {
|
|
228
|
+
senderId: string;
|
|
229
|
+
dialogId?: string;
|
|
230
|
+
chatId?: string;
|
|
231
|
+
config: Bitrix24AccountConfig;
|
|
232
|
+
runtime: PluginRuntime;
|
|
233
|
+
accountId: string;
|
|
234
|
+
pairingAdapter: ChannelPairingAdapter;
|
|
235
|
+
logger?: { debug: (...args: unknown[]) => void };
|
|
236
|
+
}): Promise<AccessResult> {
|
|
237
|
+
const passiveResult = await checkGroupAccessPassive({
|
|
238
|
+
senderId: params.senderId,
|
|
239
|
+
dialogId: params.dialogId,
|
|
240
|
+
chatId: params.chatId,
|
|
241
|
+
config: params.config,
|
|
242
|
+
runtime: params.runtime,
|
|
243
|
+
accountId: params.accountId,
|
|
244
|
+
logger: params.logger,
|
|
245
|
+
});
|
|
246
|
+
if (passiveResult !== 'pairing') {
|
|
247
|
+
return passiveResult;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const identity = normalizeAllowEntry(String(params.senderId));
|
|
251
|
+
const group = resolveGroupAccess({
|
|
252
|
+
config: params.config,
|
|
253
|
+
dialogId: params.dialogId,
|
|
254
|
+
chatId: params.chatId,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await params.runtime.channel.pairing.upsertPairingRequest({
|
|
259
|
+
channel: 'bitrix24',
|
|
260
|
+
id: identity,
|
|
261
|
+
accountId: params.accountId,
|
|
262
|
+
meta: {
|
|
263
|
+
dialogId: params.dialogId,
|
|
264
|
+
chatId: params.chatId,
|
|
265
|
+
scope: 'group',
|
|
266
|
+
},
|
|
267
|
+
pairingAdapter: params.pairingAdapter,
|
|
268
|
+
});
|
|
269
|
+
return 'pairing';
|
|
270
|
+
} catch (err) {
|
|
271
|
+
params.logger?.debug('Group pairing request failed, falling back to deny', {
|
|
272
|
+
senderId: params.senderId,
|
|
273
|
+
dialogId: params.dialogId,
|
|
274
|
+
chatId: params.chatId,
|
|
275
|
+
error: err,
|
|
276
|
+
});
|
|
277
|
+
return 'deny';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export interface HistoryEntry {
|
|
2
|
+
messageId: string;
|
|
3
|
+
sender: string;
|
|
4
|
+
senderId?: string;
|
|
5
|
+
body: string;
|
|
6
|
+
timestamp?: number;
|
|
7
|
+
wasMentioned?: boolean;
|
|
8
|
+
eventScope?: 'bot' | 'user';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConversationMeta {
|
|
12
|
+
key: string;
|
|
13
|
+
dialogId: string;
|
|
14
|
+
chatId?: string;
|
|
15
|
+
chatName?: string;
|
|
16
|
+
chatType?: string;
|
|
17
|
+
isGroup?: boolean;
|
|
18
|
+
lastActivityAt?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class HistoryCache {
|
|
22
|
+
private readonly entries = new Map<string, HistoryEntry[]>();
|
|
23
|
+
private readonly conversations = new Map<string, ConversationMeta>();
|
|
24
|
+
private readonly maxKeys: number;
|
|
25
|
+
|
|
26
|
+
constructor(params?: { maxKeys?: number }) {
|
|
27
|
+
this.maxKeys = Math.max(1, params?.maxKeys ?? 1000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
append(params: {
|
|
31
|
+
key: string;
|
|
32
|
+
entry: HistoryEntry;
|
|
33
|
+
limit: number;
|
|
34
|
+
meta?: Omit<ConversationMeta, 'key'>;
|
|
35
|
+
}): HistoryEntry[] {
|
|
36
|
+
const key = params.key;
|
|
37
|
+
const limit = Math.max(0, params.limit);
|
|
38
|
+
const currentEntries = this.entries.get(key) ?? [];
|
|
39
|
+
const nextEntries = currentEntries.some((entry) => entry.messageId === params.entry.messageId)
|
|
40
|
+
? currentEntries
|
|
41
|
+
: [...currentEntries, params.entry];
|
|
42
|
+
const trimmedEntries = limit > 0
|
|
43
|
+
? nextEntries.slice(-limit)
|
|
44
|
+
: [];
|
|
45
|
+
|
|
46
|
+
if (params.meta) {
|
|
47
|
+
this.conversations.set(key, {
|
|
48
|
+
...this.conversations.get(key),
|
|
49
|
+
...params.meta,
|
|
50
|
+
key,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.touch(key, trimmedEntries);
|
|
55
|
+
this.evictIfNeeded();
|
|
56
|
+
return trimmedEntries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get(key: string, limit?: number): HistoryEntry[] {
|
|
60
|
+
const entries = this.entries.get(key) ?? [];
|
|
61
|
+
if (entries.length === 0) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.touch(key, entries);
|
|
66
|
+
if (typeof limit !== 'number' || limit < 0) {
|
|
67
|
+
return [...entries];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return entries.slice(-limit);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
findByMessageId(key: string, messageId: string | undefined): HistoryEntry | undefined {
|
|
74
|
+
if (!messageId) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const entries = this.entries.get(key);
|
|
79
|
+
if (!entries || entries.length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.touch(key, entries);
|
|
84
|
+
return entries.find((entry) => entry.messageId === messageId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
clear(key: string): void {
|
|
88
|
+
this.entries.delete(key);
|
|
89
|
+
this.conversations.delete(key);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
clearAll(): void {
|
|
93
|
+
this.entries.clear();
|
|
94
|
+
this.conversations.clear();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
size(): number {
|
|
98
|
+
return this.entries.size;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
listConversations(): ConversationMeta[] {
|
|
102
|
+
return [...this.entries.keys()]
|
|
103
|
+
.map((key) => this.conversations.get(key))
|
|
104
|
+
.filter((conversation): conversation is ConversationMeta => Boolean(conversation));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private touch(key: string, value: HistoryEntry[]): void {
|
|
108
|
+
this.entries.delete(key);
|
|
109
|
+
this.entries.set(key, value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private evictIfNeeded(): void {
|
|
113
|
+
while (this.entries.size > this.maxKeys) {
|
|
114
|
+
const oldestKey = this.entries.keys().next().value;
|
|
115
|
+
if (typeof oldestKey !== 'string') {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
this.entries.delete(oldestKey);
|
|
119
|
+
this.conversations.delete(oldestKey);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|