@0xmaxma/claude-gateway 1.3.23 → 1.3.25
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 +125 -49
- package/dist/api/line-pending-senders.d.ts +7 -2
- package/dist/api/line-pending-senders.d.ts.map +1 -1
- package/dist/api/line-pending-senders.js +7 -2
- package/dist/api/line-pending-senders.js.map +1 -1
- package/dist/api/packages.d.ts.map +1 -1
- package/dist/api/packages.js +92 -16
- package/dist/api/packages.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +563 -172
- package/dist/api/router.js.map +1 -1
- package/dist/api/wizard-state.d.ts +1 -4
- package/dist/api/wizard-state.d.ts.map +1 -1
- package/dist/api/wizard-state.js.map +1 -1
- package/dist/discord/receiver.d.ts +1 -0
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +24 -6
- package/dist/discord/receiver.js.map +1 -1
- package/dist/telegram/receiver.d.ts +1 -0
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +18 -6
- package/dist/telegram/receiver.js.map +1 -1
- package/mcp/tools/discord/access.ts +136 -20
- package/mcp/tools/discord/client.ts +3 -1
- package/mcp/tools/discord/module.ts +75 -4
- package/mcp/tools/discord/skills/access/SKILL.md +66 -8
- package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
- package/mcp/tools/discord/types.ts +21 -2
- package/mcp/tools/telegram/dedup.ts +4 -1
- package/mcp/tools/telegram/module.ts +5 -14
- package/mcp/tools/telegram/pure.ts +169 -31
- package/mcp/tools/telegram/receiver-server.ts +108 -78
- package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
- package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
- package/package.json +1 -1
package/dist/discord/receiver.js
CHANGED
|
@@ -39,6 +39,11 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const logger_1 = require("../logger");
|
|
40
40
|
const AUTO_RESTART_DELAY_MS = 5000;
|
|
41
41
|
const MAX_RESTARTS = 3;
|
|
42
|
+
// After MAX_RESTARTS fast attempts, fall back to a slow indefinite retry
|
|
43
|
+
// instead of giving up permanently — self-heals if the admin fixes the
|
|
44
|
+
// underlying cause later (e.g. a Discord Developer Portal intent toggle)
|
|
45
|
+
// without needing to notice and manually reconnect.
|
|
46
|
+
const SLOW_RESTART_DELAY_MS = 5 * 60000;
|
|
42
47
|
class DiscordReceiver {
|
|
43
48
|
constructor(agentConfig, callbackPort, logDir) {
|
|
44
49
|
this.agentConfig = agentConfig;
|
|
@@ -47,12 +52,14 @@ class DiscordReceiver {
|
|
|
47
52
|
this.process = null;
|
|
48
53
|
this.stopping = false;
|
|
49
54
|
this.restartCount = 0;
|
|
55
|
+
this.slowPhaseAnnounced = false;
|
|
50
56
|
this.restartTimer = null;
|
|
51
57
|
this.logger = (0, logger_1.createLogger)(`${agentConfig.id}:discord-receiver`, logDir);
|
|
52
58
|
}
|
|
53
59
|
start() {
|
|
54
60
|
this.stopping = false;
|
|
55
61
|
this.restartCount = 0;
|
|
62
|
+
this.slowPhaseAnnounced = false;
|
|
56
63
|
this.spawnProcess();
|
|
57
64
|
}
|
|
58
65
|
spawnProcess() {
|
|
@@ -63,6 +70,10 @@ class DiscordReceiver {
|
|
|
63
70
|
...process.env,
|
|
64
71
|
DISCORD_BOT_TOKEN: this.agentConfig.discord?.botToken ?? '',
|
|
65
72
|
DISCORD_STATE_DIR: stateDir,
|
|
73
|
+
// Legacy seed value: when no explicit dmPolicy is configured (the common
|
|
74
|
+
// token-only case), 'pairing' flows through access.ts:migrateAccess to
|
|
75
|
+
// { dmPolicy:'allowlist', pairing:true } — i.e. a new agent comes up with
|
|
76
|
+
// pairing ON so the owner can DM the bot and self-approve via a code.
|
|
66
77
|
DISCORD_DM_POLICY: this.agentConfig.discord?.dmPolicy ?? 'pairing',
|
|
67
78
|
DISCORD_DM_ALLOWLIST: (this.agentConfig.discord?.dmAllowlist ?? []).join(','),
|
|
68
79
|
DISCORD_GUILD_ALLOWLIST: (this.agentConfig.discord?.guildAllowlist ?? []).join(','),
|
|
@@ -84,19 +95,26 @@ class DiscordReceiver {
|
|
|
84
95
|
this.logger.info('DiscordReceiver started');
|
|
85
96
|
}
|
|
86
97
|
scheduleRestart() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
const slowPhase = this.restartCount >= MAX_RESTARTS;
|
|
99
|
+
const delay = slowPhase ? SLOW_RESTART_DELAY_MS : AUTO_RESTART_DELAY_MS;
|
|
100
|
+
if (!slowPhase)
|
|
101
|
+
this.restartCount++;
|
|
102
|
+
// Surface the transition into indefinite slow-retry once at error level so a
|
|
103
|
+
// permanently-broken config (bad token, missing intent) is visible instead
|
|
104
|
+
// of hiding behind a steady stream of warn-level retry lines.
|
|
105
|
+
if (slowPhase && !this.slowPhaseAnnounced) {
|
|
106
|
+
this.slowPhaseAnnounced = true;
|
|
107
|
+
this.logger.error(`DiscordReceiver failed ${MAX_RESTARTS} fast restarts; switching to slow retry every ${SLOW_RESTART_DELAY_MS}ms — check the bot token and Developer Portal intents`);
|
|
90
108
|
}
|
|
91
|
-
this.
|
|
92
|
-
this.logger.warn(`Restarting DiscordReceiver in ${AUTO_RESTART_DELAY_MS}ms`, {
|
|
109
|
+
this.logger.warn(`Restarting DiscordReceiver in ${delay}ms`, {
|
|
93
110
|
attempt: this.restartCount,
|
|
111
|
+
slowPhase,
|
|
94
112
|
});
|
|
95
113
|
this.restartTimer = setTimeout(() => {
|
|
96
114
|
this.restartTimer = null;
|
|
97
115
|
if (!this.stopping)
|
|
98
116
|
this.spawnProcess();
|
|
99
|
-
},
|
|
117
|
+
}, delay);
|
|
100
118
|
}
|
|
101
119
|
stop() {
|
|
102
120
|
this.stopping = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/discord/receiver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iDAAoD;AACpD,2CAA6B;AAE7B,sCAAyC;AAEzC,MAAM,qBAAqB,GAAG,IAAK,CAAC;AACpC,MAAM,YAAY,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/discord/receiver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receiver.d.ts","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"receiver.d.ts","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AASvC,qBAAa,gBAAgB;IASzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVzB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;gBAGtC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM;IAKjC,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,eAAe;IAuBvB,IAAI,IAAI,IAAI;IASZ,SAAS,IAAI,OAAO;CAGrB"}
|
|
@@ -39,6 +39,9 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const logger_1 = require("../logger");
|
|
40
40
|
const AUTO_RESTART_DELAY_MS = 5000;
|
|
41
41
|
const MAX_RESTARTS = 3;
|
|
42
|
+
// After MAX_RESTARTS fast attempts, fall back to a slow indefinite retry
|
|
43
|
+
// instead of giving up permanently — mirrors discord/receiver.ts.
|
|
44
|
+
const SLOW_RESTART_DELAY_MS = 5 * 60000;
|
|
42
45
|
class TelegramReceiver {
|
|
43
46
|
constructor(agentConfig, callbackPort, logDir) {
|
|
44
47
|
this.agentConfig = agentConfig;
|
|
@@ -47,12 +50,14 @@ class TelegramReceiver {
|
|
|
47
50
|
this.process = null;
|
|
48
51
|
this.stopping = false;
|
|
49
52
|
this.restartCount = 0;
|
|
53
|
+
this.slowPhaseAnnounced = false;
|
|
50
54
|
this.restartTimer = null;
|
|
51
55
|
this.logger = (0, logger_1.createLogger)(`${agentConfig.id}:receiver`, logDir);
|
|
52
56
|
}
|
|
53
57
|
start() {
|
|
54
58
|
this.stopping = false;
|
|
55
59
|
this.restartCount = 0;
|
|
60
|
+
this.slowPhaseAnnounced = false;
|
|
56
61
|
this.spawnProcess();
|
|
57
62
|
}
|
|
58
63
|
spawnProcess() {
|
|
@@ -80,19 +85,26 @@ class TelegramReceiver {
|
|
|
80
85
|
this.logger.info('TelegramReceiver started');
|
|
81
86
|
}
|
|
82
87
|
scheduleRestart() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
const slowPhase = this.restartCount >= MAX_RESTARTS;
|
|
89
|
+
const delay = slowPhase ? SLOW_RESTART_DELAY_MS : AUTO_RESTART_DELAY_MS;
|
|
90
|
+
if (!slowPhase)
|
|
91
|
+
this.restartCount++;
|
|
92
|
+
// Surface the transition into indefinite slow-retry once at error level so a
|
|
93
|
+
// permanently-broken config (bad/revoked token) is visible instead of hiding
|
|
94
|
+
// behind a steady stream of warn-level retry lines.
|
|
95
|
+
if (slowPhase && !this.slowPhaseAnnounced) {
|
|
96
|
+
this.slowPhaseAnnounced = true;
|
|
97
|
+
this.logger.error(`TelegramReceiver failed ${MAX_RESTARTS} fast restarts; switching to slow retry every ${SLOW_RESTART_DELAY_MS}ms — check the bot token`);
|
|
86
98
|
}
|
|
87
|
-
this.
|
|
88
|
-
this.logger.warn(`Restarting TelegramReceiver in ${AUTO_RESTART_DELAY_MS}ms`, {
|
|
99
|
+
this.logger.warn(`Restarting TelegramReceiver in ${delay}ms`, {
|
|
89
100
|
attempt: this.restartCount,
|
|
101
|
+
slowPhase,
|
|
90
102
|
});
|
|
91
103
|
this.restartTimer = setTimeout(() => {
|
|
92
104
|
this.restartTimer = null;
|
|
93
105
|
if (!this.stopping)
|
|
94
106
|
this.spawnProcess();
|
|
95
|
-
},
|
|
107
|
+
}, delay);
|
|
96
108
|
}
|
|
97
109
|
stop() {
|
|
98
110
|
this.stopping = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iDAAoD;AACpD,2CAA6B;AAE7B,sCAAyC;AAEzC,MAAM,qBAAqB,GAAG,IAAK,CAAC;AACpC,MAAM,YAAY,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { randomBytes } from 'crypto';
|
|
9
|
-
import type { DiscordAccessConfig, DiscordMessageContext, DiscordAccess, DiscordGateResult } from './types';
|
|
9
|
+
import type { DiscordAccessConfig, DiscordMessageContext, DiscordAccess, DiscordGateResult, DiscordPending } from './types';
|
|
10
10
|
export type { DiscordAccessConfig, DiscordAccess } from './types';
|
|
11
11
|
|
|
12
12
|
export type AccessResult = { allowed: boolean; reason?: string };
|
|
@@ -15,10 +15,17 @@ export type AccessResult = { allowed: boolean; reason?: string };
|
|
|
15
15
|
// File-based access (new — pairing flow)
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
|
+
// Default for a brand-new agent (no access.json yet): closed base + pairing on,
|
|
19
|
+
// so the owner can DM the bot and capture their own id via a code. This matches
|
|
20
|
+
// the pre-split behavior where a missing file defaulted to dmPolicy:'pairing'.
|
|
18
21
|
export function defaultAccess(): DiscordAccess {
|
|
19
22
|
return {
|
|
20
|
-
dmPolicy: '
|
|
23
|
+
dmPolicy: 'allowlist',
|
|
24
|
+
pairing: true,
|
|
21
25
|
allowFrom: [],
|
|
26
|
+
// Secure new-agent default: guilds closed + answer only when @mentioned.
|
|
27
|
+
groupPolicy: 'allowlist',
|
|
28
|
+
requireMention: true,
|
|
22
29
|
guildAllowlist: [],
|
|
23
30
|
channelAllowlist: [],
|
|
24
31
|
roleAllowlist: [],
|
|
@@ -26,24 +33,83 @@ export function defaultAccess(): DiscordAccess {
|
|
|
26
33
|
};
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a parsed access.json into the current split shape, migrating the
|
|
38
|
+
* legacy fused dmPolicy ('pairing' folded pairing in) to the split model.
|
|
39
|
+
* Mirrors telegram/pure.ts:migrateAccess.
|
|
40
|
+
*
|
|
41
|
+
* SECURITY: a legacy `allowlist` file was deliberately locked down — it must
|
|
42
|
+
* migrate to `pairing:false`, NOT true, or it would start minting codes for
|
|
43
|
+
* strangers. Here an absent `pairing` on an existing file means a pre-split
|
|
44
|
+
* file → pairing off. Only the brand-new/ENOENT path (defaultAccess) is on.
|
|
45
|
+
*/
|
|
46
|
+
export function migrateAccess(parsed: {
|
|
47
|
+
dmPolicy?: string;
|
|
48
|
+
pairing?: boolean;
|
|
49
|
+
allowFrom?: string[];
|
|
50
|
+
groupPolicy?: string;
|
|
51
|
+
requireMention?: boolean;
|
|
52
|
+
guildAllowlist?: string[];
|
|
53
|
+
channelAllowlist?: string[];
|
|
54
|
+
roleAllowlist?: string[];
|
|
55
|
+
pending?: Record<string, DiscordPending>;
|
|
56
|
+
}): DiscordAccess {
|
|
57
|
+
const legacy = parsed.dmPolicy;
|
|
58
|
+
let dmPolicy: DiscordAccess['dmPolicy'];
|
|
59
|
+
let pairing: boolean;
|
|
60
|
+
if (legacy === 'pairing') {
|
|
61
|
+
dmPolicy = 'allowlist';
|
|
62
|
+
pairing = true;
|
|
63
|
+
} else {
|
|
64
|
+
dmPolicy = (legacy as DiscordAccess['dmPolicy']) ?? 'allowlist';
|
|
65
|
+
pairing = parsed.pairing ?? false;
|
|
66
|
+
}
|
|
67
|
+
const guildAllowlist = parsed.guildAllowlist ?? [];
|
|
68
|
+
// Guild tier migration must be behavior-preserving on upgrade. Today an empty
|
|
69
|
+
// guildAllowlist means "deliver to all guilds" — so derive 'open' when empty,
|
|
70
|
+
// 'allowlist' when non-empty; and requireMention defaults to false for
|
|
71
|
+
// existing files (they had no mention gate). New agents get the secure
|
|
72
|
+
// 'allowlist'+requireMention:true from defaultAccess() instead.
|
|
73
|
+
const groupPolicy = (parsed.groupPolicy as DiscordAccess['groupPolicy'])
|
|
74
|
+
?? (guildAllowlist.length > 0 ? 'allowlist' : 'open');
|
|
75
|
+
const requireMention = parsed.requireMention ?? false;
|
|
76
|
+
return {
|
|
77
|
+
dmPolicy,
|
|
78
|
+
pairing,
|
|
79
|
+
allowFrom: parsed.allowFrom ?? [],
|
|
80
|
+
groupPolicy,
|
|
81
|
+
requireMention,
|
|
82
|
+
guildAllowlist,
|
|
83
|
+
channelAllowlist: parsed.channelAllowlist ?? [],
|
|
84
|
+
roleAllowlist: parsed.roleAllowlist ?? [],
|
|
85
|
+
pending: parsed.pending ?? {},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
29
89
|
export function loadAccess(stateDir: string): DiscordAccess {
|
|
30
90
|
const accessFile = path.join(stateDir, 'access.json');
|
|
31
91
|
try {
|
|
32
92
|
const raw = fs.readFileSync(accessFile, 'utf8');
|
|
33
|
-
const parsed = JSON.parse(raw) as Partial<DiscordAccess
|
|
34
|
-
return
|
|
35
|
-
dmPolicy: parsed.dmPolicy ?? 'pairing',
|
|
36
|
-
allowFrom: parsed.allowFrom ?? [],
|
|
37
|
-
guildAllowlist: parsed.guildAllowlist ?? [],
|
|
38
|
-
channelAllowlist: parsed.channelAllowlist ?? [],
|
|
39
|
-
roleAllowlist: parsed.roleAllowlist ?? [],
|
|
40
|
-
pending: parsed.pending ?? {},
|
|
41
|
-
};
|
|
93
|
+
const parsed = JSON.parse(raw) as Partial<DiscordAccess> & { dmPolicy?: string };
|
|
94
|
+
return migrateAccess(parsed);
|
|
42
95
|
} catch (err) {
|
|
43
96
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
97
|
+
// Brand-new agent (no access.json yet). If the deployment configured
|
|
98
|
+
// access via DISCORD_* env vars, honour that (legacy backward-compat,
|
|
99
|
+
// normalized through migrate). Otherwise fall back to defaultAccess() so
|
|
100
|
+
// the owner can DM the bot and self-pair — migrateAccess would yield
|
|
101
|
+
// pairing:false here (its "locked existing file" rule), which wrongly
|
|
102
|
+
// drops the owner's first DM on a fresh agent.
|
|
103
|
+
const hasEnvConfig = Boolean(
|
|
104
|
+
process.env.DISCORD_DM_POLICY ||
|
|
105
|
+
process.env.DISCORD_DM_ALLOWLIST ||
|
|
106
|
+
process.env.DISCORD_GUILD_ALLOWLIST ||
|
|
107
|
+
process.env.DISCORD_CHANNEL_ALLOWLIST ||
|
|
108
|
+
process.env.DISCORD_ROLE_ALLOWLIST,
|
|
109
|
+
);
|
|
110
|
+
if (!hasEnvConfig) return defaultAccess();
|
|
111
|
+
return migrateAccess({
|
|
112
|
+
dmPolicy: process.env.DISCORD_DM_POLICY,
|
|
47
113
|
allowFrom: process.env.DISCORD_DM_ALLOWLIST
|
|
48
114
|
? process.env.DISCORD_DM_ALLOWLIST.split(',').filter(Boolean)
|
|
49
115
|
: [],
|
|
@@ -57,7 +123,7 @@ export function loadAccess(stateDir: string): DiscordAccess {
|
|
|
57
123
|
? process.env.DISCORD_ROLE_ALLOWLIST.split(',').filter(Boolean)
|
|
58
124
|
: [],
|
|
59
125
|
pending: {},
|
|
60
|
-
};
|
|
126
|
+
});
|
|
61
127
|
}
|
|
62
128
|
try {
|
|
63
129
|
fs.renameSync(accessFile, `${accessFile}.corrupt-${Date.now()}`);
|
|
@@ -100,36 +166,77 @@ export function gate(
|
|
|
100
166
|
const { isDM, userId, guildId, channelId } = context;
|
|
101
167
|
|
|
102
168
|
if (!isDM) {
|
|
103
|
-
if (access.
|
|
104
|
-
|
|
169
|
+
if (access.groupPolicy === 'disabled') return { action: 'drop' };
|
|
170
|
+
|
|
171
|
+
if (access.groupPolicy === 'allowlist' && guildId && !access.guildAllowlist.includes(guildId)) {
|
|
172
|
+
// Unknown guild. Pairing off ⇒ silent drop. On ⇒ mint a code keyed on the
|
|
173
|
+
// guild id and post it here; a member relays it to the admin (mirrors LINE).
|
|
174
|
+
if (!access.pairing) return { action: 'drop' };
|
|
175
|
+
for (const [code, p] of Object.entries(access.pending)) {
|
|
176
|
+
if (p.kind === 'guild' && p.guildId === guildId) {
|
|
177
|
+
if (p.replies >= 2) return { action: 'drop' };
|
|
178
|
+
p.replies++;
|
|
179
|
+
saveAccessFn(access);
|
|
180
|
+
return { action: 'pair', code, isResend: true, isGuild: true };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (countPending(access, 'guild') >= 5) return { action: 'drop' };
|
|
184
|
+
const code = generateCode();
|
|
185
|
+
access.pending[code] = {
|
|
186
|
+
senderId: userId,
|
|
187
|
+
channelId,
|
|
188
|
+
guildId,
|
|
189
|
+
kind: 'guild',
|
|
190
|
+
createdAt: ts,
|
|
191
|
+
expiresAt: ts + 60 * 60 * 1000,
|
|
192
|
+
replies: 1,
|
|
193
|
+
};
|
|
194
|
+
saveAccessFn(access);
|
|
195
|
+
return { action: 'pair', code, isResend: false, isGuild: true };
|
|
105
196
|
}
|
|
197
|
+
|
|
198
|
+
// Allowlisted (or open policy) → keep the channel filter, then mention gate.
|
|
106
199
|
if (access.channelAllowlist.length > 0) {
|
|
107
200
|
if (!access.channelAllowlist.includes(channelId)) return { action: 'drop' };
|
|
108
201
|
}
|
|
202
|
+
if (access.requireMention !== false && !context.mentionsBot) return { action: 'drop' };
|
|
109
203
|
return { action: 'deliver' };
|
|
110
204
|
}
|
|
111
205
|
|
|
112
206
|
// DM message
|
|
113
207
|
if (access.dmPolicy === 'disabled') return { action: 'drop' };
|
|
114
208
|
|
|
209
|
+
if (access.dmPolicy === 'open') {
|
|
210
|
+
// Open base: anyone may DM; capture their id so future messages are known.
|
|
211
|
+
if (!access.allowFrom.includes(userId)) {
|
|
212
|
+
access.allowFrom.push(userId);
|
|
213
|
+
saveAccessFn(access);
|
|
214
|
+
}
|
|
215
|
+
return { action: 'deliver' };
|
|
216
|
+
}
|
|
217
|
+
|
|
115
218
|
if (access.allowFrom.includes(userId)) return { action: 'deliver' };
|
|
116
|
-
|
|
219
|
+
// Base policy is 'allowlist' ('disabled'/'open' handled above). Pairing is the
|
|
220
|
+
// orthogonal toggle: off ⇒ pure allowlist (drop strangers, no code).
|
|
221
|
+
if (!access.pairing) return { action: 'drop' };
|
|
117
222
|
|
|
118
223
|
// pairing mode
|
|
119
224
|
for (const [code, p] of Object.entries(access.pending)) {
|
|
120
|
-
if (p.senderId === userId) {
|
|
225
|
+
if ((p.kind ?? 'dm') === 'dm' && p.senderId === userId) {
|
|
121
226
|
if (p.replies >= 2) return { action: 'drop' };
|
|
122
227
|
p.replies++;
|
|
123
228
|
saveAccessFn(access);
|
|
124
229
|
return { action: 'pair', code, isResend: true };
|
|
125
230
|
}
|
|
126
231
|
}
|
|
127
|
-
|
|
232
|
+
// Cap pending per-kind so guild knocks and DM knocks don't starve each other.
|
|
233
|
+
if (countPending(access, 'dm') >= 5) return { action: 'drop' };
|
|
128
234
|
|
|
129
235
|
const code = generateCode();
|
|
130
236
|
access.pending[code] = {
|
|
131
237
|
senderId: userId,
|
|
132
238
|
channelId,
|
|
239
|
+
kind: 'dm',
|
|
133
240
|
createdAt: ts,
|
|
134
241
|
expiresAt: ts + 60 * 60 * 1000,
|
|
135
242
|
replies: 1,
|
|
@@ -138,6 +245,15 @@ export function gate(
|
|
|
138
245
|
return { action: 'pair', code, isResend: false };
|
|
139
246
|
}
|
|
140
247
|
|
|
248
|
+
/** Count pending entries of a given kind (absent kind ⇒ 'dm'). */
|
|
249
|
+
function countPending(access: DiscordAccess, kind: 'dm' | 'guild'): number {
|
|
250
|
+
let n = 0;
|
|
251
|
+
for (const p of Object.values(access.pending)) {
|
|
252
|
+
if ((p.kind ?? 'dm') === kind) n++;
|
|
253
|
+
}
|
|
254
|
+
return n;
|
|
255
|
+
}
|
|
256
|
+
|
|
141
257
|
// ---------------------------------------------------------------------------
|
|
142
258
|
// Env-var based access (backward compat)
|
|
143
259
|
// ---------------------------------------------------------------------------
|
|
@@ -22,7 +22,9 @@ export async function createDiscordClient(token: string): Promise<any> {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
await new Promise<void>((resolve, reject) => {
|
|
25
|
-
|
|
25
|
+
// 'clientReady' is the renamed 'ready' event (discord.js v14 emits both;
|
|
26
|
+
// 'ready' is deprecated and becomes clientReady-only in v15).
|
|
27
|
+
client.once('clientReady', () => resolve());
|
|
26
28
|
client.once('error', reject);
|
|
27
29
|
client.login(token).catch(reject);
|
|
28
30
|
});
|
|
@@ -23,6 +23,12 @@ import { sendMessage, buildChoiceComponents } from './outbound';
|
|
|
23
23
|
import { loadAccess, saveAccess, gate } from './access';
|
|
24
24
|
import { maybeCreateThread } from './threading';
|
|
25
25
|
import { createMessageHandler } from './inbound';
|
|
26
|
+
// Cross-process message dedup (shared with Telegram): when more than one
|
|
27
|
+
// receiver instance is connected on the same bot token — Discord's gateway
|
|
28
|
+
// delivers every event to all sessions, unlike Telegram's single-consumer
|
|
29
|
+
// getUpdates — the first instance to claim (channelId, messageId) processes it;
|
|
30
|
+
// the others see the O_EXCL marker and drop, so a stranger's DM is handled once.
|
|
31
|
+
import { initDedupDir, isDuplicate, pruneDedup } from '../telegram/dedup';
|
|
26
32
|
import type { DiscordMessageContext } from './types';
|
|
27
33
|
|
|
28
34
|
const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
|
|
@@ -185,6 +191,14 @@ export class DiscordModule implements ChannelModule {
|
|
|
185
191
|
const loadAccessFn = () => loadAccess(stateDir);
|
|
186
192
|
const saveAccessFn = (a: ReturnType<typeof loadAccess>) => saveAccess(stateDir, a);
|
|
187
193
|
|
|
194
|
+
// Dedup dir shared by every receiver instance on this bot token (same
|
|
195
|
+
// DISCORD_STATE_DIR). Init once here, prune stale markers on a timer —
|
|
196
|
+
// mirrors the Telegram receiver's setup.
|
|
197
|
+
const dedupDir = path.join(stateDir, 'dedup');
|
|
198
|
+
initDedupDir(dedupDir);
|
|
199
|
+
pruneDedup(dedupDir);
|
|
200
|
+
setInterval(() => pruneDedup(dedupDir), 60_000).unref();
|
|
201
|
+
|
|
188
202
|
// Permissive config — gate() already handled access, inbound.ts skips re-check
|
|
189
203
|
const permissiveConfig = {
|
|
190
204
|
botToken: this.getToken()!,
|
|
@@ -208,14 +222,24 @@ export class DiscordModule implements ChannelModule {
|
|
|
208
222
|
|
|
209
223
|
const msgHandler = createMessageHandler(agentId, handler, permissiveConfig, permissiveAccessConfig);
|
|
210
224
|
|
|
211
|
-
|
|
225
|
+
const handleDiscordMessage = async (msg: any): Promise<void> => {
|
|
212
226
|
if (msg.author?.bot) return;
|
|
213
227
|
if (msg.system) return;
|
|
214
228
|
|
|
229
|
+
// Drop if another receiver instance already claimed this message. Must run
|
|
230
|
+
// before gate() so a duplicate never mints a pairing code or replies twice.
|
|
231
|
+
if (isDuplicate(dedupDir, msg.channelId, msg.id)) return;
|
|
232
|
+
|
|
215
233
|
this.lastMessageAt = Date.now();
|
|
216
234
|
|
|
217
235
|
const isDM = !msg.guild;
|
|
218
236
|
const isThread = msg.channel?.isThread?.() ?? false;
|
|
237
|
+
// Did this message @mention the bot (or reply to one of its messages)?
|
|
238
|
+
// Computed here so gate() stays discord.js-free. Works regardless of the
|
|
239
|
+
// MessageContent intent — mention entities are always delivered.
|
|
240
|
+
const botUser = this.client.user;
|
|
241
|
+
const mentionsBot = Boolean(botUser && msg.mentions?.has?.(botUser))
|
|
242
|
+
|| (!!botUser && msg.mentions?.repliedUser?.id === botUser.id);
|
|
219
243
|
|
|
220
244
|
const context: DiscordMessageContext = {
|
|
221
245
|
guildId: msg.guildId ?? null,
|
|
@@ -226,6 +250,7 @@ export class DiscordModule implements ChannelModule {
|
|
|
226
250
|
messageId: msg.id,
|
|
227
251
|
isDM,
|
|
228
252
|
isThread,
|
|
253
|
+
mentionsBot,
|
|
229
254
|
};
|
|
230
255
|
|
|
231
256
|
const access = loadAccessFn();
|
|
@@ -235,9 +260,18 @@ export class DiscordModule implements ChannelModule {
|
|
|
235
260
|
|
|
236
261
|
if (result.action === 'pair') {
|
|
237
262
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
263
|
+
if (result.isGuild) {
|
|
264
|
+
// LINE-style guild knock: post the code in the channel so a member
|
|
265
|
+
// can relay it to the admin, who approves it from the web UI. The
|
|
266
|
+
// user runs no command — a guild has no single owner to run one.
|
|
267
|
+
await msg.channel.send(
|
|
268
|
+
`This bot is private in this server.\n\nPairing code: ${result.code}\n\nShare this code with an admin to enable me here.`,
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
await msg.channel.send(
|
|
272
|
+
`Pairing required — run in Claude Code:\n\n/discord:access pair ${result.code}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
241
275
|
} catch {}
|
|
242
276
|
return;
|
|
243
277
|
}
|
|
@@ -259,6 +293,39 @@ export class DiscordModule implements ChannelModule {
|
|
|
259
293
|
if (autoThread) {
|
|
260
294
|
await maybeCreateThread(msg, autoThread, autoArchive).catch(() => {});
|
|
261
295
|
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
this.client.on('messageCreate', handleDiscordMessage);
|
|
299
|
+
|
|
300
|
+
// discord.js's own MESSAGE_CREATE handling resolves msg.channel from its
|
|
301
|
+
// in-memory cache; DM channels are never included in the initial
|
|
302
|
+
// GUILD_CREATE/READY payload, so the *first* DM after every process
|
|
303
|
+
// restart hits an uncached channel. MessageCreateAction's partial-channel
|
|
304
|
+
// fallback constructs the stub without a `type` field (it only forwards
|
|
305
|
+
// {id, author, guild_id}), so ChannelManager#_add can't pick a Channel
|
|
306
|
+
// subclass, logs "Failed to find guild, or unknown type for channel ...
|
|
307
|
+
// undefined", and silently drops the event — messageCreate never fires,
|
|
308
|
+
// no error, no pairing code. Every subsequent DM in that channel fails
|
|
309
|
+
// the same way since the failed resolution never populates the cache.
|
|
310
|
+
// Work around it: on the raw MESSAGE_CREATE dispatch, if it's a DM whose
|
|
311
|
+
// channel isn't cached yet, fetch the channel (and the message) via REST
|
|
312
|
+
// — a real API response includes `type`, so this caches it properly —
|
|
313
|
+
// then hand the fully-formed Message object to the same handler used by
|
|
314
|
+
// the normal path. isDuplicate() above ensures no double-processing if
|
|
315
|
+
// discord.js's own path ever does resolve it first.
|
|
316
|
+
this.client.on('raw', async (packet: { t?: string; d?: any }) => {
|
|
317
|
+
if (packet.t !== 'MESSAGE_CREATE') return;
|
|
318
|
+
const d = packet.d;
|
|
319
|
+
if (!d || d.guild_id || d.author?.bot) return;
|
|
320
|
+
if (this.client.channels.cache.has(d.channel_id)) return;
|
|
321
|
+
try {
|
|
322
|
+
const channel = await this.client.channels.fetch(d.channel_id);
|
|
323
|
+
if (!channel) return;
|
|
324
|
+
const msg = await channel.messages.fetch(d.id);
|
|
325
|
+
await handleDiscordMessage(msg);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
process.stderr.write(`discord: raw DM fallback failed: ${err}\n`);
|
|
328
|
+
}
|
|
262
329
|
});
|
|
263
330
|
|
|
264
331
|
// Interactive-menu button clicks: custom_id `choice:N`. A click is routed
|
|
@@ -295,6 +362,8 @@ export class DiscordModule implements ChannelModule {
|
|
|
295
362
|
messageId: interaction.message?.id ?? '',
|
|
296
363
|
isDM,
|
|
297
364
|
isThread,
|
|
365
|
+
// A button click is an explicit interaction — never mention-gated.
|
|
366
|
+
mentionsBot: true,
|
|
298
367
|
};
|
|
299
368
|
const access = loadAccessFn();
|
|
300
369
|
const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));
|
|
@@ -331,6 +400,8 @@ export class DiscordModule implements ChannelModule {
|
|
|
331
400
|
messageId: interaction.message?.id ?? '',
|
|
332
401
|
isDM,
|
|
333
402
|
isThread,
|
|
403
|
+
// A button click is an explicit interaction — never mention-gated.
|
|
404
|
+
mentionsBot: true,
|
|
334
405
|
};
|
|
335
406
|
const access = loadAccessFn();
|
|
336
407
|
const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));
|