neoagent 2.2.1-beta.7 → 2.3.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/docs/index.md +1 -0
- package/docs/migration.md +238 -0
- package/lib/manager.js +99 -2
- package/lib/migrations.js +409 -0
- package/package.json +1 -1
- package/server/catalog_sources/store-bundles/skills/productivity/migration/SKILL.md +173 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +72170 -70842
- package/server/routes/auth.js +13 -5
- package/server/routes/integrations.js +22 -0
- package/server/routes/messaging.js +41 -5
- package/server/routes/settings.js +1 -0
- package/server/services/integrations/google/provider.js +20 -2
- package/server/services/integrations/manager.js +79 -8
- package/server/services/messaging/access_policy.js +703 -0
- package/server/services/messaging/access_policy.test.js +228 -0
- package/server/services/messaging/automation.js +32 -95
- package/server/services/messaging/base.js +39 -0
- package/server/services/messaging/discord.js +61 -46
- package/server/services/messaging/http_platforms.js +178 -15
- package/server/services/messaging/manager.js +136 -69
- package/server/services/messaging/telegram.js +54 -40
- package/server/services/messaging/telnyx.js +43 -14
- package/server/services/messaging/whatsapp.js +27 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createDefaultAccessPolicy,
|
|
6
|
+
migrateLegacyWhitelist,
|
|
7
|
+
normalizeAccessPolicy,
|
|
8
|
+
parseStoredAccessPolicy,
|
|
9
|
+
evaluateAccessPolicy,
|
|
10
|
+
} = require('./access_policy');
|
|
11
|
+
const { BasePlatform } = require('./base');
|
|
12
|
+
|
|
13
|
+
test('discord defaults require allowlists and shared mentions', () => {
|
|
14
|
+
const policy = createDefaultAccessPolicy('discord');
|
|
15
|
+
assert.equal(policy.directPolicy, 'allowlist');
|
|
16
|
+
assert.equal(policy.sharedPolicy, 'allowlist');
|
|
17
|
+
assert.equal(policy.requireMentionInShared, true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('discord legacy whitelist migrates into canonical buckets', () => {
|
|
21
|
+
const policy = migrateLegacyWhitelist('discord', [
|
|
22
|
+
'user:123',
|
|
23
|
+
'guild:456',
|
|
24
|
+
'channel:789',
|
|
25
|
+
'role:999',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
assert.deepEqual(
|
|
29
|
+
policy.directRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
30
|
+
['user:123'],
|
|
31
|
+
);
|
|
32
|
+
assert.deepEqual(
|
|
33
|
+
policy.sharedSpaceRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
34
|
+
['server:456', 'channel:789'],
|
|
35
|
+
);
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
policy.sharedActorRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
38
|
+
['user:123', 'role:999'],
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('telegram legacy raw ids split users and groups', () => {
|
|
43
|
+
const policy = migrateLegacyWhitelist('telegram', ['123', '-100555']);
|
|
44
|
+
assert.deepEqual(
|
|
45
|
+
policy.directRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
46
|
+
['user:123'],
|
|
47
|
+
);
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
policy.sharedSpaceRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
50
|
+
['group:-100555'],
|
|
51
|
+
);
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
policy.sharedActorRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
54
|
+
['user:123'],
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('telnyx legacy numbers become direct phone rules', () => {
|
|
59
|
+
const policy = migrateLegacyWhitelist('telnyx', ['+1 (555) 1200']);
|
|
60
|
+
assert.equal(policy.sharedPolicy, 'disabled');
|
|
61
|
+
assert.deepEqual(policy.directRules, [
|
|
62
|
+
{ scope: 'phone_number', value: '+15551200' },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('shared access requires both space allowlist and mention when configured', () => {
|
|
67
|
+
const policy = normalizeAccessPolicy('discord', {
|
|
68
|
+
sharedPolicy: 'allowlist',
|
|
69
|
+
requireMentionInShared: true,
|
|
70
|
+
sharedSpaceRules: [{ scope: 'server', value: 'guild-1' }],
|
|
71
|
+
sharedActorRules: [{ scope: 'role', value: 'role-1' }],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const deniedByMention = evaluateAccessPolicy(policy, {
|
|
75
|
+
senderId: 'user-1',
|
|
76
|
+
chatId: 'chan-1',
|
|
77
|
+
isDirect: false,
|
|
78
|
+
isShared: true,
|
|
79
|
+
groupId: 'chan-1',
|
|
80
|
+
channelId: 'chan-1',
|
|
81
|
+
serverId: 'guild-1',
|
|
82
|
+
roleIds: ['role-1'],
|
|
83
|
+
wasMentioned: false,
|
|
84
|
+
}, 'discord');
|
|
85
|
+
assert.equal(deniedByMention.allowed, false);
|
|
86
|
+
assert.equal(deniedByMention.reason, 'mention_required');
|
|
87
|
+
|
|
88
|
+
const allowed = evaluateAccessPolicy(policy, {
|
|
89
|
+
senderId: 'user-1',
|
|
90
|
+
chatId: 'chan-1',
|
|
91
|
+
isDirect: false,
|
|
92
|
+
isShared: true,
|
|
93
|
+
groupId: 'chan-1',
|
|
94
|
+
channelId: 'chan-1',
|
|
95
|
+
serverId: 'guild-1',
|
|
96
|
+
roleIds: ['role-1'],
|
|
97
|
+
wasMentioned: true,
|
|
98
|
+
}, 'discord');
|
|
99
|
+
assert.equal(allowed.allowed, true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('direct allowlist requires a matching direct rule', () => {
|
|
103
|
+
const policy = normalizeAccessPolicy('telegram', {
|
|
104
|
+
directPolicy: 'allowlist',
|
|
105
|
+
directRules: [{ scope: 'user', value: '42' }],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(
|
|
109
|
+
evaluateAccessPolicy(policy, {
|
|
110
|
+
senderId: '42',
|
|
111
|
+
chatId: 'dm_42',
|
|
112
|
+
isDirect: true,
|
|
113
|
+
isShared: false,
|
|
114
|
+
wasMentioned: false,
|
|
115
|
+
}, 'telegram').allowed,
|
|
116
|
+
true,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
assert.equal(
|
|
120
|
+
evaluateAccessPolicy(policy, {
|
|
121
|
+
senderId: '99',
|
|
122
|
+
chatId: 'dm_99',
|
|
123
|
+
isDirect: true,
|
|
124
|
+
isShared: false,
|
|
125
|
+
wasMentioned: false,
|
|
126
|
+
}, 'telegram').allowed,
|
|
127
|
+
false,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('shared actor role rules gate allowed spaces', () => {
|
|
132
|
+
const policy = normalizeAccessPolicy('discord', {
|
|
133
|
+
sharedPolicy: 'open',
|
|
134
|
+
requireMentionInShared: false,
|
|
135
|
+
sharedActorRules: [{ scope: 'role', value: 'ops' }],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(
|
|
139
|
+
evaluateAccessPolicy(policy, {
|
|
140
|
+
senderId: 'user-1',
|
|
141
|
+
chatId: 'chan',
|
|
142
|
+
isDirect: false,
|
|
143
|
+
isShared: true,
|
|
144
|
+
channelId: 'chan',
|
|
145
|
+
serverId: 'guild',
|
|
146
|
+
roleIds: ['ops'],
|
|
147
|
+
wasMentioned: true,
|
|
148
|
+
}, 'discord').allowed,
|
|
149
|
+
true,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const denied = evaluateAccessPolicy(policy, {
|
|
153
|
+
senderId: 'user-2',
|
|
154
|
+
chatId: 'chan',
|
|
155
|
+
isDirect: false,
|
|
156
|
+
isShared: true,
|
|
157
|
+
channelId: 'chan',
|
|
158
|
+
serverId: 'guild',
|
|
159
|
+
roleIds: ['guest'],
|
|
160
|
+
wasMentioned: true,
|
|
161
|
+
}, 'discord');
|
|
162
|
+
assert.equal(denied.allowed, false);
|
|
163
|
+
assert.equal(denied.reason, 'shared_actor_not_allowed');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('parseStoredAccessPolicy prefers canonical JSON and falls back to legacy JSON', () => {
|
|
167
|
+
const canonical = parseStoredAccessPolicy(
|
|
168
|
+
'discord',
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
directPolicy: 'open',
|
|
171
|
+
sharedPolicy: 'disabled',
|
|
172
|
+
requireMentionInShared: false,
|
|
173
|
+
}),
|
|
174
|
+
JSON.stringify(['user:123']),
|
|
175
|
+
);
|
|
176
|
+
assert.equal(canonical.directPolicy, 'open');
|
|
177
|
+
assert.equal(canonical.sharedPolicy, 'disabled');
|
|
178
|
+
|
|
179
|
+
const legacy = parseStoredAccessPolicy(
|
|
180
|
+
'discord',
|
|
181
|
+
null,
|
|
182
|
+
JSON.stringify(['user:123']),
|
|
183
|
+
);
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
legacy.directRules.map((rule) => `${rule.scope}:${rule.value}`),
|
|
186
|
+
['user:123'],
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('live platforms can hot-update policy without reconnecting', () => {
|
|
191
|
+
class FakePlatform extends BasePlatform {
|
|
192
|
+
constructor() {
|
|
193
|
+
super('discord', {});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const platform = new FakePlatform();
|
|
198
|
+
platform.setAccessPolicy({
|
|
199
|
+
directPolicy: 'disabled',
|
|
200
|
+
sharedPolicy: 'disabled',
|
|
201
|
+
});
|
|
202
|
+
assert.equal(
|
|
203
|
+
platform.evaluateAccess({
|
|
204
|
+
senderId: 'user-1',
|
|
205
|
+
chatId: 'dm_user-1',
|
|
206
|
+
isDirect: true,
|
|
207
|
+
isShared: false,
|
|
208
|
+
wasMentioned: false,
|
|
209
|
+
}).allowed,
|
|
210
|
+
false,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
platform.setAccessPolicy({
|
|
214
|
+
directPolicy: 'allowlist',
|
|
215
|
+
directRules: [{ scope: 'user', value: 'user-1' }],
|
|
216
|
+
sharedPolicy: 'disabled',
|
|
217
|
+
});
|
|
218
|
+
assert.equal(
|
|
219
|
+
platform.evaluateAccess({
|
|
220
|
+
senderId: 'user-1',
|
|
221
|
+
chatId: 'dm_user-1',
|
|
222
|
+
isDirect: true,
|
|
223
|
+
isShared: false,
|
|
224
|
+
wasMentioned: false,
|
|
225
|
+
}).allowed,
|
|
226
|
+
true,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const db = require('../../db/database');
|
|
4
4
|
const { detectPromptInjection } = require('../../utils/security');
|
|
5
|
-
const { normalizeWhatsAppId } = require('../../utils/whatsapp');
|
|
6
5
|
const { randomUUID } = require('crypto');
|
|
7
6
|
const { isMainAgent } = require('../agents/manager');
|
|
8
7
|
const { buildPlatformFormattingGuide } = require('./formatting_guides');
|
|
8
|
+
const {
|
|
9
|
+
accessPolicyKey,
|
|
10
|
+
legacyWhitelistKey,
|
|
11
|
+
parseStoredAccessPolicy,
|
|
12
|
+
evaluateAccessPolicy,
|
|
13
|
+
buildBlockedSenderPayload,
|
|
14
|
+
contextFromMessage,
|
|
15
|
+
} = require('./access_policy');
|
|
9
16
|
const {
|
|
10
17
|
buildVoiceMessagingPrompt,
|
|
11
18
|
buildVoiceMessagingRunOptions,
|
|
@@ -362,120 +369,50 @@ function buildSenderIdentityBlock(msg) {
|
|
|
362
369
|
return `<sender_identity>\n${lines.join('\n')}\n</sender_identity>`;
|
|
363
370
|
}
|
|
364
371
|
|
|
365
|
-
function messagingAllowlistCandidates(msg) {
|
|
366
|
-
const sender = String(msg.sender || '').trim();
|
|
367
|
-
const chatId = String(msg.chatId || '').trim();
|
|
368
|
-
const values = new Set([sender, chatId].filter(Boolean));
|
|
369
|
-
const normalizedChatId = chatId.startsWith('dm_') ? chatId.slice(3) : chatId;
|
|
370
|
-
const guildId = String(msg.guildId || '').trim();
|
|
371
|
-
|
|
372
|
-
if (normalizedChatId) {
|
|
373
|
-
values.add(normalizedChatId);
|
|
374
|
-
}
|
|
375
|
-
if (sender) values.add(`user:${sender}`);
|
|
376
|
-
if (guildId) values.add(`guild:${guildId}`);
|
|
377
|
-
if (chatId) {
|
|
378
|
-
values.add(`chat:${chatId}`);
|
|
379
|
-
values.add(`channel:${chatId}`);
|
|
380
|
-
values.add(`room:${chatId}`);
|
|
381
|
-
values.add(`group:${chatId}`);
|
|
382
|
-
}
|
|
383
|
-
if (normalizedChatId) {
|
|
384
|
-
values.add(`chat:${normalizedChatId}`);
|
|
385
|
-
values.add(`channel:${normalizedChatId}`);
|
|
386
|
-
values.add(`room:${normalizedChatId}`);
|
|
387
|
-
values.add(`group:${normalizedChatId}`);
|
|
388
|
-
}
|
|
389
|
-
return [...values];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
372
|
async function isAllowedMessagingSender({ io, userId, msg }) {
|
|
393
373
|
const agentId = msg.agentId || null;
|
|
394
|
-
const
|
|
374
|
+
const policyRow = db
|
|
395
375
|
.prepare('SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?')
|
|
396
|
-
.get(userId, agentId,
|
|
376
|
+
.get(userId, agentId, accessPolicyKey(msg.platform))
|
|
397
377
|
|| (isMainAgent(userId, agentId)
|
|
398
378
|
? db
|
|
399
379
|
.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
|
|
400
|
-
.get(userId,
|
|
380
|
+
.get(userId, accessPolicyKey(msg.platform))
|
|
381
|
+
: null);
|
|
382
|
+
const legacyRow = db
|
|
383
|
+
.prepare('SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?')
|
|
384
|
+
.get(userId, agentId, legacyWhitelistKey(msg.platform))
|
|
385
|
+
|| (isMainAgent(userId, agentId)
|
|
386
|
+
? db
|
|
387
|
+
.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
|
|
388
|
+
.get(userId, legacyWhitelistKey(msg.platform))
|
|
401
389
|
: null);
|
|
402
390
|
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
: msg.platform === 'telnyx'
|
|
407
|
-
? (id) => String(id || '').replace(/[^0-9+]/g, '')
|
|
408
|
-
: (id) => String(id || '').trim();
|
|
409
|
-
|
|
410
|
-
let whitelist = [];
|
|
411
|
-
if (whitelistRow) {
|
|
412
|
-
try {
|
|
413
|
-
const parsed = JSON.parse(whitelistRow.value);
|
|
414
|
-
if (Array.isArray(parsed)) whitelist = parsed;
|
|
415
|
-
} catch {
|
|
416
|
-
whitelist = [];
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const shouldCheckWhitelist = whitelist.length > 0;
|
|
421
|
-
|
|
422
|
-
if (!shouldCheckWhitelist) {
|
|
423
|
-
if (!msg.isGroup) return true;
|
|
424
|
-
console.log(
|
|
425
|
-
`[Messaging] Blocked ${msg.platform} group message from ${msg.sender} (no group allowlist configured)`
|
|
426
|
-
);
|
|
427
|
-
emitBlockedSenderSuggestion({ io, userId, msg });
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const candidates = messagingAllowlistCandidates(msg).map(normalize).filter(Boolean);
|
|
432
|
-
const allowed = whitelist.some((entry) => {
|
|
433
|
-
const normalizedEntry = normalize(entry);
|
|
434
|
-
return normalizedEntry === '*' || candidates.includes(normalizedEntry);
|
|
435
|
-
});
|
|
436
|
-
if (allowed) {
|
|
391
|
+
const policy = parseStoredAccessPolicy(msg.platform, policyRow?.value, legacyRow?.value);
|
|
392
|
+
const decision = evaluateAccessPolicy(policy, contextFromMessage(msg), msg.platform);
|
|
393
|
+
if (decision.allowed) {
|
|
437
394
|
return true;
|
|
438
395
|
}
|
|
439
396
|
|
|
440
397
|
console.log(
|
|
441
|
-
`[Messaging] Blocked ${msg.platform} message from ${msg.sender} (
|
|
398
|
+
`[Messaging] Blocked ${msg.platform} message from ${msg.sender} (${decision.reason})`
|
|
442
399
|
);
|
|
443
400
|
emitBlockedSenderSuggestion({ io, userId, msg });
|
|
444
401
|
return false;
|
|
445
402
|
}
|
|
446
403
|
|
|
447
404
|
function emitBlockedSenderSuggestion({ io, userId, msg }) {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
} else {
|
|
458
|
-
const sender = String(msg.sender || '').trim();
|
|
459
|
-
const chatId = String(msg.chatId || '').trim();
|
|
460
|
-
if (sender) {
|
|
461
|
-
suggestions.push({
|
|
462
|
-
label: `Add sender (${msg.senderName || sender})`,
|
|
463
|
-
prefixedId: `user:${sender}`
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
if (chatId && chatId !== sender) {
|
|
467
|
-
suggestions.push({
|
|
468
|
-
label: `Add chat (${chatId})`,
|
|
469
|
-
prefixedId: msg.isGroup ? `group:${chatId}` : chatId
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
405
|
+
const payload = buildBlockedSenderPayload(msg.platform, contextFromMessage(msg), {
|
|
406
|
+
senderName: msg.senderName || null,
|
|
407
|
+
meta: msg.guildName ? `Server: ${msg.guildName}` : (msg.groupName ? `Group: ${msg.groupName}` : ''),
|
|
408
|
+
serverLabel: msg.guildName || '',
|
|
409
|
+
groupLabel: msg.groupName || '',
|
|
410
|
+
channelLabel: msg.channelName || '',
|
|
411
|
+
roomLabel: msg.roomName || '',
|
|
412
|
+
});
|
|
473
413
|
io.to(`user:${userId}`).emit('messaging:blocked_sender', {
|
|
474
414
|
platform: msg.platform,
|
|
475
|
-
|
|
476
|
-
chatId: msg.chatId,
|
|
477
|
-
senderName: msg.senderName || null,
|
|
478
|
-
suggestions: suggestions.length > 0 ? suggestions : null
|
|
415
|
+
...payload,
|
|
479
416
|
});
|
|
480
417
|
}
|
|
481
418
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
|
+
const {
|
|
3
|
+
createDefaultAccessPolicy,
|
|
4
|
+
normalizeAccessPolicy,
|
|
5
|
+
getPlatformAccessCapabilities,
|
|
6
|
+
evaluateAccessPolicy,
|
|
7
|
+
buildBlockedSenderPayload,
|
|
8
|
+
} = require('./access_policy');
|
|
2
9
|
|
|
3
10
|
class BasePlatform extends EventEmitter {
|
|
4
11
|
constructor(name, config = {}) {
|
|
@@ -10,6 +17,11 @@ class BasePlatform extends EventEmitter {
|
|
|
10
17
|
this.supportsMedia = false;
|
|
11
18
|
this.supportsVoice = false;
|
|
12
19
|
this.allowedEntries = new Set();
|
|
20
|
+
this.accessCapabilities = getPlatformAccessCapabilities(name);
|
|
21
|
+
this.accessPolicy = createDefaultAccessPolicy(name);
|
|
22
|
+
if (config.accessPolicy) {
|
|
23
|
+
this.setAccessPolicy(config.accessPolicy);
|
|
24
|
+
}
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
setAllowedEntries(entries) {
|
|
@@ -18,16 +30,43 @@ class BasePlatform extends EventEmitter {
|
|
|
18
30
|
}
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
setAccessPolicy(policy) {
|
|
34
|
+
this.accessPolicy = normalizeAccessPolicy(this.name, policy);
|
|
35
|
+
return this.accessPolicy;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getAccessPolicy() {
|
|
39
|
+
return this.accessPolicy;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getAccessCapabilities() {
|
|
43
|
+
return this.accessCapabilities;
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
_checkAccess(id) {
|
|
22
47
|
if (this.allowedEntries.size === 0) return true;
|
|
23
48
|
return this.allowedEntries.has(String(id));
|
|
24
49
|
}
|
|
25
50
|
|
|
51
|
+
evaluateAccess(context) {
|
|
52
|
+
return evaluateAccessPolicy(this.accessPolicy, context, this.name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_checkInboundAccess(context, options = {}) {
|
|
56
|
+
const result = this.evaluateAccess(context);
|
|
57
|
+
if (result.allowed) {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
this.emit('blocked_sender', buildBlockedSenderPayload(this.name, context, options));
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
26
64
|
async connect() { throw new Error('connect() not implemented'); }
|
|
27
65
|
async disconnect() { throw new Error('disconnect() not implemented'); }
|
|
28
66
|
async sendMessage(to, content, options) { throw new Error('sendMessage() not implemented'); }
|
|
29
67
|
async getContacts() { return []; }
|
|
30
68
|
async getChats() { return []; }
|
|
69
|
+
async listAccessTargets() { return []; }
|
|
31
70
|
getStatus() { return this.status; }
|
|
32
71
|
getAuthInfo() { return null; }
|
|
33
72
|
}
|
|
@@ -113,33 +113,6 @@ class DiscordPlatform extends BasePlatform {
|
|
|
113
113
|
getStatus() { return this.status; }
|
|
114
114
|
getAuthInfo() { return this._botUser ? { tag: this._botUser.tag, id: this._botUser.id } : null; }
|
|
115
115
|
|
|
116
|
-
// ── Whitelist ──────────────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
// Inherits setAllowedEntries from BasePlatform
|
|
119
|
-
|
|
120
|
-
/** Returns {allowed, requireMention} */
|
|
121
|
-
_checkAccess(message) {
|
|
122
|
-
const isDM = message.channel.type === ChannelType.DM;
|
|
123
|
-
const userId = message.author.id;
|
|
124
|
-
const guildId = message.guildId || null;
|
|
125
|
-
const channelId = message.channelId;
|
|
126
|
-
const userAllowed = super._checkAccess(`user:${userId}`) || super._checkAccess(userId);
|
|
127
|
-
|
|
128
|
-
if (isDM) {
|
|
129
|
-
return {
|
|
130
|
-
allowed: this.allowedEntries.size === 0 || userAllowed,
|
|
131
|
-
requireMention: false,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
allowed: userAllowed
|
|
137
|
-
|| (guildId && super._checkAccess(`guild:${guildId}`))
|
|
138
|
-
|| super._checkAccess(`channel:${channelId}`),
|
|
139
|
-
requireMention: true,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
116
|
_isMentioned(message) {
|
|
144
117
|
return this._botUser ? message.mentions.has(this._botUser.id) : false;
|
|
145
118
|
}
|
|
@@ -185,28 +158,30 @@ class DiscordPlatform extends BasePlatform {
|
|
|
185
158
|
const channelId = message.channelId;
|
|
186
159
|
const chatId = isDM ? `dm_${userId}` : channelId;
|
|
187
160
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
161
|
+
const access = this._checkInboundAccess({
|
|
162
|
+
platform: 'discord',
|
|
163
|
+
senderId: userId,
|
|
164
|
+
chatId,
|
|
165
|
+
isDirect: isDM,
|
|
166
|
+
isShared: !isDM,
|
|
167
|
+
groupId: !isDM ? channelId : '',
|
|
168
|
+
channelId: !isDM ? channelId : '',
|
|
169
|
+
serverId: guildId || '',
|
|
170
|
+
roleIds: !isDM && message.member ? [...message.member.roles.cache.keys()] : [],
|
|
171
|
+
phoneNumber: '',
|
|
172
|
+
wasMentioned: isDM ? false : this._isMentioned(message),
|
|
173
|
+
}, {
|
|
174
|
+
senderName: message.author.username || null,
|
|
175
|
+
meta: message.guild?.name ? `Server: ${message.guild.name}` : '',
|
|
176
|
+
serverLabel: message.guild?.name || '',
|
|
177
|
+
channelLabel: !isDM ? `#${message.channel.name || channelId}` : '',
|
|
178
|
+
});
|
|
198
179
|
|
|
199
|
-
|
|
200
|
-
sender: userId,
|
|
201
|
-
chatId,
|
|
202
|
-
senderName: message.author.username,
|
|
203
|
-
guildName: message.guild?.name || null,
|
|
204
|
-
suggestions,
|
|
205
|
-
});
|
|
180
|
+
if (!access.allowed) {
|
|
206
181
|
return;
|
|
207
182
|
}
|
|
208
183
|
|
|
209
|
-
let content =
|
|
184
|
+
let content = (!isDM && this._isMentioned(message)) ? this._stripMention(message.content) : (message.content || '');
|
|
210
185
|
if (message.attachments.size > 0) {
|
|
211
186
|
const urls = [...message.attachments.values()].map(a => a.url).join(', ');
|
|
212
187
|
content += (content ? '\n' : '') + `[Attachment: ${urls}]`;
|
|
@@ -223,7 +198,7 @@ class DiscordPlatform extends BasePlatform {
|
|
|
223
198
|
: `${senderDisplayName} in #${message.channel.name || channelId}${message.guild ? ` (${message.guild.name})` : ''}`;
|
|
224
199
|
|
|
225
200
|
// Fetch recent channel history for context on guild/channel mentions
|
|
226
|
-
const channelContext = (
|
|
201
|
+
const channelContext = (!isDM && this._isMentioned(message)) ? await this._fetchContext(message.channel, 20) : null;
|
|
227
202
|
|
|
228
203
|
this.emit('message', {
|
|
229
204
|
platform: 'discord',
|
|
@@ -279,6 +254,46 @@ class DiscordPlatform extends BasePlatform {
|
|
|
279
254
|
}
|
|
280
255
|
} catch { /* non-fatal */ }
|
|
281
256
|
}
|
|
257
|
+
|
|
258
|
+
async listAccessTargets() {
|
|
259
|
+
if (!this._client || this.status !== 'connected') return [];
|
|
260
|
+
const targets = [];
|
|
261
|
+
for (const guild of this._client.guilds.cache.values()) {
|
|
262
|
+
targets.push({
|
|
263
|
+
source: 'live',
|
|
264
|
+
bucket: 'sharedSpaceRules',
|
|
265
|
+
scope: 'server',
|
|
266
|
+
value: guild.id,
|
|
267
|
+
label: guild.name || guild.id,
|
|
268
|
+
subtitle: 'Discord server',
|
|
269
|
+
});
|
|
270
|
+
const channels = guild.channels?.cache?.values?.() || [];
|
|
271
|
+
for (const channel of channels) {
|
|
272
|
+
if (!channel?.isTextBased?.() || channel.type === ChannelType.DM) continue;
|
|
273
|
+
targets.push({
|
|
274
|
+
source: 'live',
|
|
275
|
+
bucket: 'sharedSpaceRules',
|
|
276
|
+
scope: 'channel',
|
|
277
|
+
value: channel.id,
|
|
278
|
+
label: `#${channel.name || channel.id}`,
|
|
279
|
+
subtitle: guild.name || 'Discord channel',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const roles = guild.roles?.cache?.values?.() || [];
|
|
283
|
+
for (const role of roles) {
|
|
284
|
+
if (!role || role.managed || role.name === '@everyone') continue;
|
|
285
|
+
targets.push({
|
|
286
|
+
source: 'live',
|
|
287
|
+
bucket: 'sharedActorRules',
|
|
288
|
+
scope: 'role',
|
|
289
|
+
value: role.id,
|
|
290
|
+
label: role.name || role.id,
|
|
291
|
+
subtitle: guild.name || 'Discord role',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return targets;
|
|
296
|
+
}
|
|
282
297
|
}
|
|
283
298
|
|
|
284
299
|
module.exports = { DiscordPlatform };
|