@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type {
|
|
2
|
+
AttachmentBuilder,
|
|
3
|
+
ChatInputCommandInteraction,
|
|
4
|
+
Message as DiscordMessage,
|
|
5
|
+
} from 'discord.js';
|
|
2
6
|
|
|
3
7
|
import { chunkMessage } from '../../chunk.js';
|
|
4
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DISCORD_MAX_LINES_PER_MESSAGE,
|
|
10
|
+
DISCORD_TEXT_CHUNK_LIMIT,
|
|
11
|
+
} from '../../config.js';
|
|
12
|
+
import {
|
|
13
|
+
getHumanDelayMs,
|
|
14
|
+
type HumanDelayConfig,
|
|
15
|
+
sleep,
|
|
16
|
+
} from './human-delay.js';
|
|
17
|
+
import { type MentionLookup, rewriteUserMentions } from './mentions.js';
|
|
5
18
|
|
|
6
|
-
export type DiscordRetryFn = <T>(
|
|
19
|
+
export type DiscordRetryFn = <T>(
|
|
20
|
+
label: string,
|
|
21
|
+
fn: () => Promise<T>,
|
|
22
|
+
) => Promise<T>;
|
|
7
23
|
|
|
8
24
|
export function buildResponseText(text: string, toolsUsed?: string[]): string {
|
|
9
25
|
let body = text;
|
|
@@ -27,12 +43,19 @@ export function prepareChunkedPayloads(
|
|
|
27
43
|
files?: AttachmentBuilder[],
|
|
28
44
|
mentionLookup?: MentionLookup,
|
|
29
45
|
): { content: string; files?: AttachmentBuilder[] }[] {
|
|
30
|
-
const prepared = mentionLookup
|
|
31
|
-
|
|
46
|
+
const prepared = mentionLookup
|
|
47
|
+
? rewriteUserMentions(text, mentionLookup)
|
|
48
|
+
: text;
|
|
49
|
+
const chunks = chunkMessage(prepared, {
|
|
50
|
+
maxChars: Math.max(200, Math.min(2_000, DISCORD_TEXT_CHUNK_LIMIT)),
|
|
51
|
+
maxLines: Math.max(4, Math.min(200, DISCORD_MAX_LINES_PER_MESSAGE)),
|
|
52
|
+
});
|
|
32
53
|
const safeChunks = chunks.length > 0 ? chunks : ['(no content)'];
|
|
33
54
|
return safeChunks.map((content, i) => ({
|
|
34
55
|
content,
|
|
35
|
-
...(i === safeChunks.length - 1 && files && files.length > 0
|
|
56
|
+
...(i === safeChunks.length - 1 && files && files.length > 0
|
|
57
|
+
? { files }
|
|
58
|
+
: {}),
|
|
36
59
|
}));
|
|
37
60
|
}
|
|
38
61
|
|
|
@@ -42,6 +65,7 @@ export async function sendChunkedReply(params: {
|
|
|
42
65
|
withRetry: DiscordRetryFn;
|
|
43
66
|
files?: AttachmentBuilder[];
|
|
44
67
|
mentionLookup?: MentionLookup;
|
|
68
|
+
humanDelay?: HumanDelayConfig;
|
|
45
69
|
}): Promise<void> {
|
|
46
70
|
const payloads = prepareChunkedPayloads(
|
|
47
71
|
params.text,
|
|
@@ -52,9 +76,20 @@ export async function sendChunkedReply(params: {
|
|
|
52
76
|
if (i === 0) {
|
|
53
77
|
await params.withRetry('reply', () => params.msg.reply(payloads[i]));
|
|
54
78
|
} else {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
const delayMs = getHumanDelayMs(params.humanDelay);
|
|
80
|
+
if (delayMs > 0) {
|
|
81
|
+
await sleep(delayMs);
|
|
82
|
+
}
|
|
83
|
+
await params.withRetry('send', () =>
|
|
84
|
+
(
|
|
85
|
+
params.msg.channel as unknown as {
|
|
86
|
+
send: (next: {
|
|
87
|
+
content: string;
|
|
88
|
+
files?: AttachmentBuilder[];
|
|
89
|
+
}) => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
).send(payloads[i]),
|
|
92
|
+
);
|
|
58
93
|
}
|
|
59
94
|
}
|
|
60
95
|
}
|
|
@@ -65,14 +100,24 @@ export async function sendChunkedDirectReply(params: {
|
|
|
65
100
|
withRetry: DiscordRetryFn;
|
|
66
101
|
files?: AttachmentBuilder[];
|
|
67
102
|
mentionLookup?: MentionLookup;
|
|
103
|
+
humanDelay?: HumanDelayConfig;
|
|
68
104
|
}): Promise<void> {
|
|
69
105
|
const payloads = prepareChunkedPayloads(
|
|
70
106
|
params.text,
|
|
71
107
|
params.files,
|
|
72
108
|
params.mentionLookup,
|
|
73
109
|
);
|
|
74
|
-
const dm = await params.withRetry('dm-open', () =>
|
|
75
|
-
|
|
110
|
+
const dm = await params.withRetry('dm-open', () =>
|
|
111
|
+
params.msg.author.createDM(),
|
|
112
|
+
);
|
|
113
|
+
for (let i = 0; i < payloads.length; i += 1) {
|
|
114
|
+
if (i > 0) {
|
|
115
|
+
const delayMs = getHumanDelayMs(params.humanDelay);
|
|
116
|
+
if (delayMs > 0) {
|
|
117
|
+
await sleep(delayMs);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const payload = payloads[i];
|
|
76
121
|
await params.withRetry('dm-send', () => dm.send(payload));
|
|
77
122
|
}
|
|
78
123
|
}
|
|
@@ -88,12 +133,18 @@ export async function sendChunkedInteractionReply(params: {
|
|
|
88
133
|
const payload = { ...payloads[i], ephemeral: true };
|
|
89
134
|
if (i === 0) {
|
|
90
135
|
if (params.interaction.replied || params.interaction.deferred) {
|
|
91
|
-
await params.withRetry('interaction-followup', () =>
|
|
136
|
+
await params.withRetry('interaction-followup', () =>
|
|
137
|
+
params.interaction.followUp(payload),
|
|
138
|
+
);
|
|
92
139
|
} else {
|
|
93
|
-
await params.withRetry('interaction-reply', () =>
|
|
140
|
+
await params.withRetry('interaction-reply', () =>
|
|
141
|
+
params.interaction.reply(payload),
|
|
142
|
+
);
|
|
94
143
|
}
|
|
95
144
|
continue;
|
|
96
145
|
}
|
|
97
|
-
await params.withRetry('interaction-followup', () =>
|
|
146
|
+
await params.withRetry('interaction-followup', () =>
|
|
147
|
+
params.interaction.followUp(payload),
|
|
148
|
+
);
|
|
98
149
|
}
|
|
99
150
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface HumanDelayConfig {
|
|
2
|
+
mode: 'off' | 'natural' | 'custom';
|
|
3
|
+
minMs?: number;
|
|
4
|
+
maxMs?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ResolvedHumanDelayConfig {
|
|
8
|
+
mode: 'off' | 'natural' | 'custom';
|
|
9
|
+
minMs: number;
|
|
10
|
+
maxMs: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_HUMAN_DELAY_MIN_MS = 800;
|
|
14
|
+
export const DEFAULT_HUMAN_DELAY_MAX_MS = 2_500;
|
|
15
|
+
|
|
16
|
+
export function resolveHumanDelayConfig(
|
|
17
|
+
config?: HumanDelayConfig,
|
|
18
|
+
): ResolvedHumanDelayConfig {
|
|
19
|
+
const mode = config?.mode ?? 'natural';
|
|
20
|
+
const minMs = Math.max(
|
|
21
|
+
0,
|
|
22
|
+
Math.floor(config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS),
|
|
23
|
+
);
|
|
24
|
+
const rawMax = Math.max(
|
|
25
|
+
0,
|
|
26
|
+
Math.floor(config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS),
|
|
27
|
+
);
|
|
28
|
+
const maxMs = Math.max(minMs, rawMax);
|
|
29
|
+
return {
|
|
30
|
+
mode,
|
|
31
|
+
minMs,
|
|
32
|
+
maxMs,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getHumanDelayMs(config?: HumanDelayConfig): number {
|
|
37
|
+
const resolved = resolveHumanDelayConfig(config);
|
|
38
|
+
if (resolved.mode === 'off') return 0;
|
|
39
|
+
if (resolved.maxMs <= resolved.minMs) return resolved.minMs;
|
|
40
|
+
const span = resolved.maxMs - resolved.minMs + 1;
|
|
41
|
+
return resolved.minMs + Math.floor(Math.random() * span);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function sleep(ms: number): Promise<void> {
|
|
45
|
+
const waitMs = Math.max(0, Math.floor(ms));
|
|
46
|
+
if (waitMs <= 0) return;
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
48
|
+
}
|
|
@@ -5,8 +5,79 @@ export interface ParsedCommand {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export type DiscordGuildMessageMode = 'off' | 'mention' | 'free';
|
|
8
|
+
export type DiscordCommandAccessMode = 'public' | 'restricted';
|
|
9
|
+
const KNOWN_SUBCOMMANDS = new Set([
|
|
10
|
+
'bot',
|
|
11
|
+
'rag',
|
|
12
|
+
'model',
|
|
13
|
+
'status',
|
|
14
|
+
'approve',
|
|
15
|
+
'usage',
|
|
16
|
+
'export',
|
|
17
|
+
'sessions',
|
|
18
|
+
'audit',
|
|
19
|
+
'schedule',
|
|
20
|
+
'channel',
|
|
21
|
+
'ralph',
|
|
22
|
+
'clear',
|
|
23
|
+
'help',
|
|
24
|
+
]);
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
const GREETING_ONLY_RE =
|
|
27
|
+
/^(hi|hey|hello|yo|sup|thanks|thank you|thx|ok|okay|got it|roger|cool)[!. ]*$/i;
|
|
28
|
+
const FREE_MODE_ACK_ONLY_RE =
|
|
29
|
+
/^(ok|okay|thanks|thank you|thx|ty|cool|nice|got it|roger|sounds good|sgtm|lol|lmao)[!. ]*$/i;
|
|
30
|
+
const FREE_MODE_QUESTION_START_RE =
|
|
31
|
+
/^(?:hey|hi|hello|yo|hmm|hm|well)?[\s,:-]*(?:can|could|would|will|should|do|does|did|is|are|am|how|what|why|when|where|who|which)\b/i;
|
|
32
|
+
const FREE_MODE_REQUEST_VERB_RE =
|
|
33
|
+
/\b(?:please|pls|help|explain|review|check|debug|fix|summari[sz]e|create|write|show|tell|give|generate|compare|analy[sz]e)\b/i;
|
|
34
|
+
const FREE_MODE_PROBLEM_SIGNAL_RE =
|
|
35
|
+
/\b(?:error|failed|failure|exception|stack trace|broken|issue|not working|doesn't work|cannot|can't|wont|won't)\b/i;
|
|
36
|
+
const URL_ONLY_RE = /^https?:\/\/\S+$/i;
|
|
37
|
+
const FREE_MODE_CHANNEL_ADDRESS_RE =
|
|
38
|
+
/\b(?:all|everyone|anyone|team|folks|guys|channel|chat|alle|zusammen|jemand)\b/i;
|
|
39
|
+
|
|
40
|
+
function escapeRegex(value: string): string {
|
|
41
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeAlias(raw: string): string {
|
|
45
|
+
return raw.trim().replace(/^@+/, '').toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasLooseBotMention(
|
|
49
|
+
content: string,
|
|
50
|
+
botAliases: string[],
|
|
51
|
+
): boolean {
|
|
52
|
+
const normalized = content.trim().toLowerCase();
|
|
53
|
+
if (!normalized) return false;
|
|
54
|
+
|
|
55
|
+
for (const rawAlias of botAliases) {
|
|
56
|
+
const alias = normalizeAlias(rawAlias);
|
|
57
|
+
if (!alias || alias.length < 2) continue;
|
|
58
|
+
if (!/^[\p{L}\p{N}._-]+$/u.test(alias)) continue;
|
|
59
|
+
|
|
60
|
+
const escapedAlias = escapeRegex(alias);
|
|
61
|
+
const mentionRe = new RegExp(
|
|
62
|
+
`(?:^|[^\\p{L}\\p{N}_-])${escapedAlias}(?=$|[^\\p{L}\\p{N}_-])`,
|
|
63
|
+
'iu',
|
|
64
|
+
);
|
|
65
|
+
if (mentionRe.test(normalized)) return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isAddressedToChannel(content: string): boolean {
|
|
72
|
+
const normalized = content.trim();
|
|
73
|
+
if (!normalized) return false;
|
|
74
|
+
return FREE_MODE_CHANNEL_ADDRESS_RE.test(normalized);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function stripBotMentions(
|
|
78
|
+
text: string,
|
|
79
|
+
botMentionRegex: RegExp | null,
|
|
80
|
+
): string {
|
|
10
81
|
if (!botMentionRegex) return text;
|
|
11
82
|
return text.replace(botMentionRegex, '').trim();
|
|
12
83
|
}
|
|
@@ -32,7 +103,22 @@ export function hasPrefixInvocation(
|
|
|
32
103
|
return text.startsWith(prefix);
|
|
33
104
|
}
|
|
34
105
|
|
|
35
|
-
export function
|
|
106
|
+
export function hasSlashCommandInvocation(
|
|
107
|
+
content: string,
|
|
108
|
+
botMentionRegex: RegExp | null,
|
|
109
|
+
): boolean {
|
|
110
|
+
const text = stripBotMentions(content, botMentionRegex).trim();
|
|
111
|
+
if (!text.startsWith('/')) return false;
|
|
112
|
+
const token = text.slice(1).split(/\s+/)[0]?.toLowerCase() || '';
|
|
113
|
+
if (!token) return false;
|
|
114
|
+
return KNOWN_SUBCOMMANDS.has(token);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildSessionIdFromContext(
|
|
118
|
+
guildId: string | null,
|
|
119
|
+
channelId: string,
|
|
120
|
+
userId: string,
|
|
121
|
+
): string {
|
|
36
122
|
return guildId ? `${guildId}:${channelId}` : `dm:${userId}`;
|
|
37
123
|
}
|
|
38
124
|
|
|
@@ -44,17 +130,99 @@ export function parseCommand(
|
|
|
44
130
|
let text = stripBotMentions(content, botMentionRegex);
|
|
45
131
|
if (text.startsWith(prefix)) {
|
|
46
132
|
text = text.slice(prefix.length).trim();
|
|
133
|
+
} else if (text.startsWith('/')) {
|
|
134
|
+
text = text.slice(1).trim();
|
|
47
135
|
}
|
|
48
136
|
|
|
49
137
|
const parts = text.split(/\s+/);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
138
|
+
if (parts.length > 0 && KNOWN_SUBCOMMANDS.has(parts[0].toLowerCase())) {
|
|
139
|
+
return {
|
|
140
|
+
isCommand: true,
|
|
141
|
+
command: parts[0].toLowerCase(),
|
|
142
|
+
args: parts.slice(1),
|
|
143
|
+
};
|
|
53
144
|
}
|
|
54
145
|
|
|
55
146
|
return { isCommand: false, command: '', args: [] };
|
|
56
147
|
}
|
|
57
148
|
|
|
149
|
+
export function shouldSuppressAutoReply(
|
|
150
|
+
content: string,
|
|
151
|
+
suppressPatterns?: string[],
|
|
152
|
+
): boolean {
|
|
153
|
+
const normalized = content.trim().toLowerCase();
|
|
154
|
+
if (!normalized) return false;
|
|
155
|
+
if (GREETING_ONLY_RE.test(normalized)) return true;
|
|
156
|
+
if (!suppressPatterns || suppressPatterns.length === 0) return false;
|
|
157
|
+
return suppressPatterns.some((pattern) => {
|
|
158
|
+
const needle = pattern.trim().toLowerCase();
|
|
159
|
+
if (!needle) return false;
|
|
160
|
+
return normalized.includes(needle);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function shouldSkipFreeReplyBecauseOtherUsersMentioned(params: {
|
|
165
|
+
guildMessageMode: DiscordGuildMessageMode;
|
|
166
|
+
hasBotMention: boolean;
|
|
167
|
+
hasPrefixInvocation: boolean;
|
|
168
|
+
botUserId: string | null;
|
|
169
|
+
mentionedUserIds: string[];
|
|
170
|
+
}): boolean {
|
|
171
|
+
if (params.guildMessageMode !== 'free') return false;
|
|
172
|
+
if (params.hasBotMention) return false;
|
|
173
|
+
if (params.hasPrefixInvocation) return false;
|
|
174
|
+
if (params.mentionedUserIds.length === 0) return false;
|
|
175
|
+
|
|
176
|
+
const botId = params.botUserId?.trim() || '';
|
|
177
|
+
return params.mentionedUserIds.some((userId) => userId !== botId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function shouldReplyInFreeMode(params: {
|
|
181
|
+
guildMessageMode: DiscordGuildMessageMode;
|
|
182
|
+
content: string;
|
|
183
|
+
hasBotMention: boolean;
|
|
184
|
+
hasLooseBotMention?: boolean;
|
|
185
|
+
isAddressedToChannel?: boolean;
|
|
186
|
+
hasPrefixInvocation: boolean;
|
|
187
|
+
isReplyToBot: boolean;
|
|
188
|
+
hasAttachments: boolean;
|
|
189
|
+
}): boolean {
|
|
190
|
+
if (params.guildMessageMode !== 'free') return true;
|
|
191
|
+
if (params.hasPrefixInvocation) return true;
|
|
192
|
+
if (params.hasBotMention) return true;
|
|
193
|
+
if (params.hasLooseBotMention) return true;
|
|
194
|
+
if (params.isReplyToBot) return true;
|
|
195
|
+
if (params.hasAttachments) return true;
|
|
196
|
+
|
|
197
|
+
const normalized = params.content.trim();
|
|
198
|
+
if (!normalized) return false;
|
|
199
|
+
if (FREE_MODE_ACK_ONLY_RE.test(normalized)) return false;
|
|
200
|
+
if (URL_ONLY_RE.test(normalized)) return false;
|
|
201
|
+
if (params.isAddressedToChannel) return true;
|
|
202
|
+
if (normalized.includes('?')) return true;
|
|
203
|
+
if (FREE_MODE_QUESTION_START_RE.test(normalized)) return true;
|
|
204
|
+
if (FREE_MODE_REQUEST_VERB_RE.test(normalized)) return true;
|
|
205
|
+
if (FREE_MODE_PROBLEM_SIGNAL_RE.test(normalized)) return true;
|
|
206
|
+
if (normalized.includes('```') || /`[^`]+`/.test(normalized)) return true;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function isAuthorizedCommandUser(params: {
|
|
211
|
+
mode: DiscordCommandAccessMode;
|
|
212
|
+
userId: string;
|
|
213
|
+
allowedUserIds: string[];
|
|
214
|
+
legacyCommandUserId?: string;
|
|
215
|
+
}): boolean {
|
|
216
|
+
if (params.mode === 'public') return true;
|
|
217
|
+
const allowed = new Set(
|
|
218
|
+
params.allowedUserIds.map((entry) => entry.trim()).filter(Boolean),
|
|
219
|
+
);
|
|
220
|
+
const legacy = (params.legacyCommandUserId || '').trim();
|
|
221
|
+
if (legacy) allowed.add(legacy);
|
|
222
|
+
if (allowed.size === 0) return false;
|
|
223
|
+
return allowed.has(params.userId);
|
|
224
|
+
}
|
|
225
|
+
|
|
58
226
|
export function isTrigger(params: {
|
|
59
227
|
content: string;
|
|
60
228
|
isDm: boolean;
|
|
@@ -64,15 +232,29 @@ export function isTrigger(params: {
|
|
|
64
232
|
prefix: string;
|
|
65
233
|
botMentionRegex: RegExp | null;
|
|
66
234
|
hasBotMention: boolean;
|
|
235
|
+
suppressPatterns?: string[];
|
|
67
236
|
}): boolean {
|
|
237
|
+
const stripped = stripBotMentions(params.content, params.botMentionRegex);
|
|
238
|
+
const hasPrefixed = hasPrefixInvocation(
|
|
239
|
+
params.content,
|
|
240
|
+
params.botMentionRegex,
|
|
241
|
+
params.prefix,
|
|
242
|
+
);
|
|
243
|
+
const hasSlash = hasSlashCommandInvocation(
|
|
244
|
+
params.content,
|
|
245
|
+
params.botMentionRegex,
|
|
246
|
+
);
|
|
247
|
+
|
|
68
248
|
if (params.commandsOnly) {
|
|
69
|
-
return
|
|
249
|
+
return hasPrefixed || hasSlash;
|
|
70
250
|
}
|
|
251
|
+
if (hasPrefixed || hasSlash) return true;
|
|
252
|
+
if (shouldSuppressAutoReply(stripped, params.suppressPatterns)) return false;
|
|
71
253
|
if (params.isDm) return true;
|
|
72
|
-
if (hasPrefixInvocation(params.content, params.botMentionRegex, params.prefix)) return true;
|
|
73
254
|
if (params.guildMessageMode === 'off') return false;
|
|
74
255
|
if (params.guildMessageMode === 'free') return true;
|
|
75
|
-
|
|
256
|
+
// Keep `respondToAllMessages` consumed for compatibility; mode resolution decides guild behavior.
|
|
257
|
+
void params.respondToAllMessages;
|
|
76
258
|
if (params.hasBotMention) return true;
|
|
77
259
|
return false;
|
|
78
260
|
}
|
|
@@ -21,7 +21,11 @@ export function normalizeMentionAlias(raw: string | null | undefined): string {
|
|
|
21
21
|
return lowered;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function addMentionAlias(
|
|
24
|
+
export function addMentionAlias(
|
|
25
|
+
lookup: MentionLookup,
|
|
26
|
+
rawAlias: string | null | undefined,
|
|
27
|
+
userId: string,
|
|
28
|
+
): void {
|
|
25
29
|
const alias = normalizeMentionAlias(rawAlias);
|
|
26
30
|
if (!alias) return;
|
|
27
31
|
let ids = lookup.byAlias.get(alias);
|
|
@@ -36,7 +40,10 @@ export function extractMentionAliasHints(text: string): MentionAliasHint[] {
|
|
|
36
40
|
if (!text) return [];
|
|
37
41
|
|
|
38
42
|
const hints = new Map<string, MentionAliasHint>();
|
|
39
|
-
const collect = (
|
|
43
|
+
const collect = (
|
|
44
|
+
rawAlias: string | null | undefined,
|
|
45
|
+
rawUserId: string | null | undefined,
|
|
46
|
+
): void => {
|
|
40
47
|
const userId = (rawUserId || '').trim();
|
|
41
48
|
if (!/^\d{16,22}$/.test(userId)) return;
|
|
42
49
|
const alias = normalizeMentionAlias(rawAlias);
|
|
@@ -45,32 +52,44 @@ export function extractMentionAliasHints(text: string): MentionAliasHint[] {
|
|
|
45
52
|
if (!hints.has(key)) hints.set(key, { alias, userId });
|
|
46
53
|
};
|
|
47
54
|
|
|
48
|
-
const aliasToId =
|
|
55
|
+
const aliasToId =
|
|
56
|
+
/(^|[\s,;:.!?])@?([\p{L}\p{N}._-]{2,32})\s*(?:ist|is|=|->|=>|means|heißt)\s*(?:<@!?(\d{16,22})>|(\d{16,22}))/giu;
|
|
49
57
|
let match: RegExpExecArray | null;
|
|
50
|
-
while (
|
|
58
|
+
while (true) {
|
|
59
|
+
match = aliasToId.exec(text);
|
|
60
|
+
if (match === null) break;
|
|
51
61
|
collect(match[2], match[3] || match[4]);
|
|
52
62
|
}
|
|
53
63
|
|
|
54
|
-
const idToAlias =
|
|
55
|
-
|
|
64
|
+
const idToAlias =
|
|
65
|
+
/(?:<@!?(\d{16,22})>|(\d{16,22}))\s*(?:ist|is|=|->|=>|means|heißt)\s*@?([\p{L}\p{N}._-]{2,32})/giu;
|
|
66
|
+
while (true) {
|
|
67
|
+
match = idToAlias.exec(text);
|
|
68
|
+
if (match === null) break;
|
|
56
69
|
collect(match[3], match[1] || match[2]);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
return Array.from(hints.values());
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
export function rewriteUserMentions(
|
|
75
|
+
export function rewriteUserMentions(
|
|
76
|
+
text: string,
|
|
77
|
+
lookup: MentionLookup,
|
|
78
|
+
): string {
|
|
63
79
|
if (!text) return text;
|
|
64
80
|
if (!lookup.byAlias.size) return text;
|
|
65
|
-
return text.replace(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
return text.replace(
|
|
82
|
+
/(^|[\s([{:>])@([\p{L}\p{N}._-]{2,32})\b/gu,
|
|
83
|
+
(full, prefix: string, rawAlias: string) => {
|
|
84
|
+
const alias = normalizeMentionAlias(rawAlias);
|
|
85
|
+
if (!alias) return full;
|
|
86
|
+
const ids = lookup.byAlias.get(alias);
|
|
87
|
+
if (!ids || ids.size !== 1) return full;
|
|
88
|
+
const [id] = Array.from(ids);
|
|
89
|
+
if (!id) return full;
|
|
90
|
+
return `${prefix}<@${id}>`;
|
|
91
|
+
},
|
|
92
|
+
);
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
function extractMentionAliases(text: string): string[] {
|
|
@@ -78,7 +97,9 @@ function extractMentionAliases(text: string): string[] {
|
|
|
78
97
|
const aliases = new Set<string>();
|
|
79
98
|
const re = /(^|[\s([{:>])@([\p{L}\p{N}._-]{2,32})\b/gu;
|
|
80
99
|
let match: RegExpExecArray | null;
|
|
81
|
-
while (
|
|
100
|
+
while (true) {
|
|
101
|
+
match = re.exec(text);
|
|
102
|
+
if (match === null) break;
|
|
82
103
|
const alias = normalizeMentionAlias(match[2]);
|
|
83
104
|
if (!alias) continue;
|
|
84
105
|
aliases.add(alias);
|
|
@@ -97,7 +118,10 @@ async function enrichMentionLookupFromGuild(
|
|
|
97
118
|
for (const alias of aliases) {
|
|
98
119
|
if (lookup.byAlias.has(alias)) continue;
|
|
99
120
|
try {
|
|
100
|
-
const members = await msg.guild.members.search({
|
|
121
|
+
const members = await msg.guild.members.search({
|
|
122
|
+
query: alias,
|
|
123
|
+
limit: 5,
|
|
124
|
+
});
|
|
101
125
|
const exactMatches = Array.from(members.values()).filter((member) => {
|
|
102
126
|
const username = normalizeMentionAlias(member.user?.username || '');
|
|
103
127
|
const displayName = normalizeMentionAlias(member.displayName || '');
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ActivityType, type Client, type PresenceStatusData } from 'discord.js';
|
|
2
|
+
|
|
3
|
+
import { logger } from '../../logger.js';
|
|
4
|
+
|
|
5
|
+
export type PresenceHealthState =
|
|
6
|
+
| 'healthy'
|
|
7
|
+
| 'degraded'
|
|
8
|
+
| 'exhausted'
|
|
9
|
+
| 'maintenance';
|
|
10
|
+
export type DiscordPresenceActivityType =
|
|
11
|
+
| 'playing'
|
|
12
|
+
| 'watching'
|
|
13
|
+
| 'listening'
|
|
14
|
+
| 'competing'
|
|
15
|
+
| 'custom';
|
|
16
|
+
|
|
17
|
+
export interface DiscordAutoPresenceConfig {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
intervalMs: number;
|
|
20
|
+
healthyText: string;
|
|
21
|
+
degradedText: string;
|
|
22
|
+
exhaustedText: string;
|
|
23
|
+
activityType: DiscordPresenceActivityType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type PresenceConfigResolver = () => DiscordAutoPresenceConfig;
|
|
27
|
+
type PresenceStateResolver = () => PresenceHealthState;
|
|
28
|
+
|
|
29
|
+
function toDiscordActivityType(
|
|
30
|
+
type: DiscordPresenceActivityType,
|
|
31
|
+
): ActivityType {
|
|
32
|
+
switch (type) {
|
|
33
|
+
case 'playing':
|
|
34
|
+
return ActivityType.Playing;
|
|
35
|
+
case 'listening':
|
|
36
|
+
return ActivityType.Listening;
|
|
37
|
+
case 'competing':
|
|
38
|
+
return ActivityType.Competing;
|
|
39
|
+
case 'custom':
|
|
40
|
+
return ActivityType.Custom;
|
|
41
|
+
case 'watching':
|
|
42
|
+
default:
|
|
43
|
+
return ActivityType.Watching;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function presenceTextForState(
|
|
48
|
+
config: DiscordAutoPresenceConfig,
|
|
49
|
+
state: PresenceHealthState,
|
|
50
|
+
): string {
|
|
51
|
+
if (state === 'degraded') return config.degradedText;
|
|
52
|
+
if (state === 'exhausted') return config.exhaustedText;
|
|
53
|
+
return config.healthyText;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function presenceStatusForState(
|
|
57
|
+
state: PresenceHealthState,
|
|
58
|
+
): PresenceStatusData {
|
|
59
|
+
if (state === 'maintenance') return 'invisible';
|
|
60
|
+
if (state === 'exhausted') return 'dnd';
|
|
61
|
+
if (state === 'degraded') return 'idle';
|
|
62
|
+
return 'online';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class DiscordAutoPresenceController {
|
|
66
|
+
private readonly client: Client;
|
|
67
|
+
private readonly getConfig: PresenceConfigResolver;
|
|
68
|
+
private readonly resolveState: PresenceStateResolver;
|
|
69
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
70
|
+
private running = false;
|
|
71
|
+
private maintenance = false;
|
|
72
|
+
private lastFingerprint = '';
|
|
73
|
+
|
|
74
|
+
constructor(params: {
|
|
75
|
+
client: Client;
|
|
76
|
+
getConfig: PresenceConfigResolver;
|
|
77
|
+
resolveState: PresenceStateResolver;
|
|
78
|
+
}) {
|
|
79
|
+
this.client = params.client;
|
|
80
|
+
this.getConfig = params.getConfig;
|
|
81
|
+
this.resolveState = params.resolveState;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
start(): void {
|
|
85
|
+
this.stop();
|
|
86
|
+
this.running = true;
|
|
87
|
+
const tick = async (): Promise<void> => {
|
|
88
|
+
if (!this.running) return;
|
|
89
|
+
await this.evaluateNow();
|
|
90
|
+
if (!this.running) return;
|
|
91
|
+
const intervalMs = Math.max(
|
|
92
|
+
5_000,
|
|
93
|
+
Math.floor(this.getConfig().intervalMs),
|
|
94
|
+
);
|
|
95
|
+
this.timer = setTimeout(() => {
|
|
96
|
+
void tick();
|
|
97
|
+
}, intervalMs);
|
|
98
|
+
};
|
|
99
|
+
this.timer = setTimeout(() => {
|
|
100
|
+
void tick();
|
|
101
|
+
}, 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stop(): void {
|
|
105
|
+
this.running = false;
|
|
106
|
+
if (!this.timer) return;
|
|
107
|
+
clearTimeout(this.timer);
|
|
108
|
+
this.timer = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async setMaintenance(): Promise<void> {
|
|
112
|
+
this.maintenance = true;
|
|
113
|
+
await this.evaluateNow();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async evaluateNow(): Promise<void> {
|
|
117
|
+
const config = this.getConfig();
|
|
118
|
+
if (!this.client.user) return;
|
|
119
|
+
if (!config.enabled && !this.maintenance) return;
|
|
120
|
+
|
|
121
|
+
const state = this.maintenance ? 'maintenance' : this.resolveState();
|
|
122
|
+
const status = presenceStatusForState(state);
|
|
123
|
+
const activityText =
|
|
124
|
+
state === 'maintenance' ? '' : presenceTextForState(config, state);
|
|
125
|
+
const activityType = toDiscordActivityType(config.activityType);
|
|
126
|
+
const fingerprint = `${state}:${status}:${activityType}:${activityText}`;
|
|
127
|
+
if (fingerprint === this.lastFingerprint) return;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
if (state === 'maintenance') {
|
|
131
|
+
await this.client.user.setPresence({
|
|
132
|
+
status,
|
|
133
|
+
activities: [],
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
await this.client.user.setPresence({
|
|
137
|
+
status,
|
|
138
|
+
activities: activityText
|
|
139
|
+
? [{ name: activityText, type: activityType }]
|
|
140
|
+
: [],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
this.lastFingerprint = fingerprint;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.debug({ error, state }, 'Failed to update Discord presence');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|