@gakr-gakr/discord 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/account-inspect-api.ts +6 -0
- package/action-runtime-api.ts +1 -0
- package/api.ts +130 -0
- package/autobot.plugin.json +15 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +3 -0
- package/config-api.ts +4 -0
- package/configured-state.ts +6 -0
- package/contract-api.ts +21 -0
- package/directory-contract-api.ts +4 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +24 -0
- package/package.json +79 -0
- package/runtime-api.actions.ts +15 -0
- package/runtime-api.lookup.ts +22 -0
- package/runtime-api.monitor.ts +50 -0
- package/runtime-api.send.ts +79 -0
- package/runtime-api.threads.ts +31 -0
- package/runtime-api.ts +181 -0
- package/runtime-setter-api.ts +3 -0
- package/secret-contract-api.ts +4 -0
- package/security-audit-contract-api.ts +1 -0
- package/security-contract-api.ts +4 -0
- package/session-key-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-inspect.ts +131 -0
- package/src/accounts.ts +205 -0
- package/src/actions/handle-action.guild-admin.ts +421 -0
- package/src/actions/handle-action.ts +402 -0
- package/src/actions/runtime.guild.ts +446 -0
- package/src/actions/runtime.messaging.messages.ts +226 -0
- package/src/actions/runtime.messaging.reactions.ts +67 -0
- package/src/actions/runtime.messaging.runtime.ts +73 -0
- package/src/actions/runtime.messaging.send.ts +336 -0
- package/src/actions/runtime.messaging.shared.ts +97 -0
- package/src/actions/runtime.messaging.ts +37 -0
- package/src/actions/runtime.moderation-shared.ts +48 -0
- package/src/actions/runtime.moderation.ts +116 -0
- package/src/actions/runtime.presence.ts +117 -0
- package/src/actions/runtime.shared.ts +86 -0
- package/src/actions/runtime.ts +87 -0
- package/src/api.ts +219 -0
- package/src/approval-handler.runtime.ts +636 -0
- package/src/approval-native.ts +219 -0
- package/src/approval-runtime.ts +14 -0
- package/src/approval-shared.ts +56 -0
- package/src/audit-core.ts +178 -0
- package/src/audit.ts +32 -0
- package/src/channel-actions.runtime.ts +1 -0
- package/src/channel-actions.ts +254 -0
- package/src/channel-api.ts +29 -0
- package/src/channel.conversation.ts +159 -0
- package/src/channel.loaders.ts +50 -0
- package/src/channel.runtime.ts +1 -0
- package/src/channel.setup.ts +12 -0
- package/src/channel.ts +728 -0
- package/src/chunk.ts +321 -0
- package/src/client.ts +143 -0
- package/src/component-custom-id.ts +72 -0
- package/src/components-registry.ts +356 -0
- package/src/components.builders.ts +410 -0
- package/src/components.modal.ts +124 -0
- package/src/components.parse.ts +407 -0
- package/src/components.ts +54 -0
- package/src/components.types.ts +187 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +354 -0
- package/src/conversation-identity.ts +58 -0
- package/src/delivery-retry.ts +56 -0
- package/src/directory-cache.ts +116 -0
- package/src/directory-config.ts +58 -0
- package/src/directory-live.ts +135 -0
- package/src/doctor-contract.ts +477 -0
- package/src/doctor-shared.ts +5 -0
- package/src/doctor.ts +340 -0
- package/src/draft-chunking.ts +43 -0
- package/src/draft-stream.ts +162 -0
- package/src/error-body.ts +38 -0
- package/src/exec-approvals.ts +110 -0
- package/src/gateway-logging.ts +67 -0
- package/src/group-policy.ts +113 -0
- package/src/guilds.ts +29 -0
- package/src/inbound-event-delivery.ts +135 -0
- package/src/interactive-dispatch.ts +104 -0
- package/src/internal/api.commands.ts +51 -0
- package/src/internal/api.guild.ts +164 -0
- package/src/internal/api.interactions.ts +53 -0
- package/src/internal/api.messages.ts +113 -0
- package/src/internal/api.reactions.ts +38 -0
- package/src/internal/api.ts +61 -0
- package/src/internal/api.users.ts +19 -0
- package/src/internal/api.webhooks.ts +13 -0
- package/src/internal/client.ts +310 -0
- package/src/internal/command-deploy.ts +352 -0
- package/src/internal/commands.ts +188 -0
- package/src/internal/components.base.ts +65 -0
- package/src/internal/components.message.ts +279 -0
- package/src/internal/components.modal.ts +95 -0
- package/src/internal/components.ts +31 -0
- package/src/internal/discord.ts +11 -0
- package/src/internal/embeds.ts +35 -0
- package/src/internal/entity-cache.ts +98 -0
- package/src/internal/event-queue.ts +185 -0
- package/src/internal/gateway-close-codes.ts +25 -0
- package/src/internal/gateway-dispatch.ts +96 -0
- package/src/internal/gateway-identify-limiter.ts +26 -0
- package/src/internal/gateway-lifecycle.ts +75 -0
- package/src/internal/gateway-rate-limit.ts +104 -0
- package/src/internal/gateway.ts +479 -0
- package/src/internal/interaction-dispatch.ts +162 -0
- package/src/internal/interaction-options.ts +98 -0
- package/src/internal/interaction-response.ts +53 -0
- package/src/internal/interactions.ts +378 -0
- package/src/internal/listeners.ts +91 -0
- package/src/internal/modal-fields.ts +95 -0
- package/src/internal/payload.ts +69 -0
- package/src/internal/rest-body.ts +115 -0
- package/src/internal/rest-errors.ts +88 -0
- package/src/internal/rest-routes.ts +50 -0
- package/src/internal/rest-scheduler.ts +557 -0
- package/src/internal/rest.ts +322 -0
- package/src/internal/schemas.ts +36 -0
- package/src/internal/structures.ts +280 -0
- package/src/internal/test-builders.test-support.ts +167 -0
- package/src/internal/voice.ts +49 -0
- package/src/media-detection.ts +28 -0
- package/src/mentions.ts +147 -0
- package/src/monitor/ack-reactions.ts +70 -0
- package/src/monitor/agent-components-auth.ts +7 -0
- package/src/monitor/agent-components-context.ts +154 -0
- package/src/monitor/agent-components-data.ts +224 -0
- package/src/monitor/agent-components-dm-auth.ts +177 -0
- package/src/monitor/agent-components-guild-auth.ts +322 -0
- package/src/monitor/agent-components-helpers.runtime.ts +3 -0
- package/src/monitor/agent-components-helpers.ts +34 -0
- package/src/monitor/agent-components-reply.ts +10 -0
- package/src/monitor/agent-components.deps.runtime.ts +2 -0
- package/src/monitor/agent-components.dispatch.ts +359 -0
- package/src/monitor/agent-components.handlers.ts +303 -0
- package/src/monitor/agent-components.modal.ts +160 -0
- package/src/monitor/agent-components.plugin-interactive.ts +187 -0
- package/src/monitor/agent-components.runtime.ts +14 -0
- package/src/monitor/agent-components.system-controls.ts +215 -0
- package/src/monitor/agent-components.ts +70 -0
- package/src/monitor/agent-components.types.ts +58 -0
- package/src/monitor/agent-components.wildcard-controls.ts +171 -0
- package/src/monitor/allow-list.ts +631 -0
- package/src/monitor/auto-presence.ts +356 -0
- package/src/monitor/channel-access.ts +102 -0
- package/src/monitor/commands.ts +9 -0
- package/src/monitor/dm-command-auth.ts +259 -0
- package/src/monitor/dm-command-decision.ts +49 -0
- package/src/monitor/exec-approvals.ts +161 -0
- package/src/monitor/format.ts +45 -0
- package/src/monitor/gateway-handle.ts +34 -0
- package/src/monitor/gateway-metadata.ts +298 -0
- package/src/monitor/gateway-plugin.ts +302 -0
- package/src/monitor/gateway-registry.ts +37 -0
- package/src/monitor/gateway-supervisor.ts +206 -0
- package/src/monitor/inbound-context.ts +95 -0
- package/src/monitor/inbound-dedupe.ts +79 -0
- package/src/monitor/inbound-job.ts +118 -0
- package/src/monitor/listeners.queue.ts +91 -0
- package/src/monitor/listeners.reactions.ts +594 -0
- package/src/monitor/listeners.ts +150 -0
- package/src/monitor/message-channel-info.ts +96 -0
- package/src/monitor/message-forwarded.ts +114 -0
- package/src/monitor/message-handler.batch-gate.ts +19 -0
- package/src/monitor/message-handler.context.ts +492 -0
- package/src/monitor/message-handler.dm-preflight.ts +119 -0
- package/src/monitor/message-handler.draft-preview.ts +436 -0
- package/src/monitor/message-handler.hydration.ts +198 -0
- package/src/monitor/message-handler.module-test-helpers.ts +31 -0
- package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
- package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
- package/src/monitor/message-handler.preflight-context.ts +54 -0
- package/src/monitor/message-handler.preflight-helpers.ts +164 -0
- package/src/monitor/message-handler.preflight-history.ts +23 -0
- package/src/monitor/message-handler.preflight-logging.ts +36 -0
- package/src/monitor/message-handler.preflight-pluralkit.ts +28 -0
- package/src/monitor/message-handler.preflight-runtime.ts +28 -0
- package/src/monitor/message-handler.preflight-thread.ts +49 -0
- package/src/monitor/message-handler.preflight.ts +822 -0
- package/src/monitor/message-handler.preflight.types.ts +115 -0
- package/src/monitor/message-handler.process.ts +1033 -0
- package/src/monitor/message-handler.routing-preflight.ts +112 -0
- package/src/monitor/message-handler.ts +309 -0
- package/src/monitor/message-media.ts +536 -0
- package/src/monitor/message-run-queue.ts +101 -0
- package/src/monitor/message-text.ts +171 -0
- package/src/monitor/message-utils.ts +34 -0
- package/src/monitor/model-picker-preferences.ts +184 -0
- package/src/monitor/model-picker.state.ts +364 -0
- package/src/monitor/model-picker.test-utils.ts +26 -0
- package/src/monitor/model-picker.ts +38 -0
- package/src/monitor/model-picker.view.ts +722 -0
- package/src/monitor/native-command-agent-reply.ts +125 -0
- package/src/monitor/native-command-arg-ui.ts +233 -0
- package/src/monitor/native-command-auth.ts +309 -0
- package/src/monitor/native-command-bypass.ts +13 -0
- package/src/monitor/native-command-context.ts +109 -0
- package/src/monitor/native-command-dispatch.ts +35 -0
- package/src/monitor/native-command-model-picker-apply.ts +209 -0
- package/src/monitor/native-command-model-picker-interaction.ts +516 -0
- package/src/monitor/native-command-model-picker-ui.ts +357 -0
- package/src/monitor/native-command-reply.ts +185 -0
- package/src/monitor/native-command-route.ts +91 -0
- package/src/monitor/native-command-status.ts +76 -0
- package/src/monitor/native-command-ui.ts +26 -0
- package/src/monitor/native-command-ui.types.ts +20 -0
- package/src/monitor/native-command.args.ts +45 -0
- package/src/monitor/native-command.options.ts +153 -0
- package/src/monitor/native-command.runtime.ts +51 -0
- package/src/monitor/native-command.ts +747 -0
- package/src/monitor/native-command.types.ts +9 -0
- package/src/monitor/native-interaction-channel-context.ts +50 -0
- package/src/monitor/preflight-audio.runtime.ts +9 -0
- package/src/monitor/preflight-audio.ts +130 -0
- package/src/monitor/presence-cache.ts +61 -0
- package/src/monitor/presence.ts +50 -0
- package/src/monitor/provider-session.runtime.ts +12 -0
- package/src/monitor/provider.acp.ts +89 -0
- package/src/monitor/provider.allowlist.ts +398 -0
- package/src/monitor/provider.cleanup.ts +41 -0
- package/src/monitor/provider.commands.ts +129 -0
- package/src/monitor/provider.config-log.ts +45 -0
- package/src/monitor/provider.deploy-errors.ts +362 -0
- package/src/monitor/provider.deploy.ts +221 -0
- package/src/monitor/provider.interactions.ts +160 -0
- package/src/monitor/provider.lifecycle.ts +562 -0
- package/src/monitor/provider.runtime.ts +1 -0
- package/src/monitor/provider.startup-log.ts +32 -0
- package/src/monitor/provider.startup.ts +323 -0
- package/src/monitor/provider.ts +688 -0
- package/src/monitor/reply-context.ts +64 -0
- package/src/monitor/reply-delivery.ts +216 -0
- package/src/monitor/reply-safety.ts +96 -0
- package/src/monitor/rest-fetch.ts +97 -0
- package/src/monitor/route-resolution.ts +140 -0
- package/src/monitor/sender-identity.ts +81 -0
- package/src/monitor/startup-status.ts +10 -0
- package/src/monitor/status.ts +22 -0
- package/src/monitor/system-events.ts +55 -0
- package/src/monitor/thread-bindings.config.ts +35 -0
- package/src/monitor/thread-bindings.discord-api.ts +310 -0
- package/src/monitor/thread-bindings.lifecycle.ts +354 -0
- package/src/monitor/thread-bindings.manager.ts +554 -0
- package/src/monitor/thread-bindings.messages.ts +6 -0
- package/src/monitor/thread-bindings.persona.ts +25 -0
- package/src/monitor/thread-bindings.session-adapter.ts +229 -0
- package/src/monitor/thread-bindings.session-shared.ts +59 -0
- package/src/monitor/thread-bindings.session-updates.ts +35 -0
- package/src/monitor/thread-bindings.state.ts +540 -0
- package/src/monitor/thread-bindings.ts +48 -0
- package/src/monitor/thread-bindings.types.ts +83 -0
- package/src/monitor/thread-channel-context.ts +112 -0
- package/src/monitor/thread-session-close.ts +63 -0
- package/src/monitor/thread-title.ts +181 -0
- package/src/monitor/threading.auto-thread.ts +287 -0
- package/src/monitor/threading.cache.ts +45 -0
- package/src/monitor/threading.starter.ts +288 -0
- package/src/monitor/threading.ts +20 -0
- package/src/monitor/threading.types.ts +102 -0
- package/src/monitor/timeouts.ts +84 -0
- package/src/monitor/typing.ts +17 -0
- package/src/monitor.gateway.ts +75 -0
- package/src/monitor.ts +28 -0
- package/src/network-config.ts +79 -0
- package/src/normalize.ts +86 -0
- package/src/outbound-adapter.ts +327 -0
- package/src/outbound-approval.ts +29 -0
- package/src/outbound-components.ts +86 -0
- package/src/outbound-payload.ts +208 -0
- package/src/outbound-send-context.ts +92 -0
- package/src/outbound-session-route.ts +72 -0
- package/src/pluralkit.ts +58 -0
- package/src/preview-streaming.ts +18 -0
- package/src/probe.runtime.ts +1 -0
- package/src/probe.ts +237 -0
- package/src/proxy-fetch.ts +92 -0
- package/src/proxy-request-client.ts +21 -0
- package/src/recipient-resolution.ts +39 -0
- package/src/resolve-allowlist-common.ts +39 -0
- package/src/resolve-channels.ts +369 -0
- package/src/resolve-users.ts +184 -0
- package/src/retry.ts +98 -0
- package/src/runtime-api.ts +64 -0
- package/src/runtime-config.ts +16 -0
- package/src/runtime.ts +23 -0
- package/src/secret-config-contract.ts +140 -0
- package/src/security-audit.runtime.ts +1 -0
- package/src/security-audit.ts +208 -0
- package/src/security-contract.ts +47 -0
- package/src/security-doctor.ts +20 -0
- package/src/security.ts +60 -0
- package/src/send-target-parsing.ts +14 -0
- package/src/send.channels.ts +139 -0
- package/src/send.components.ts +391 -0
- package/src/send.emojis-stickers.ts +57 -0
- package/src/send.guild.ts +170 -0
- package/src/send.message-request.ts +112 -0
- package/src/send.messages.ts +229 -0
- package/src/send.outbound.ts +459 -0
- package/src/send.permissions.ts +283 -0
- package/src/send.reactions.ts +155 -0
- package/src/send.receipt.ts +69 -0
- package/src/send.shared.ts +469 -0
- package/src/send.ts +82 -0
- package/src/send.types.ts +191 -0
- package/src/send.typing.ts +9 -0
- package/src/send.voice.ts +140 -0
- package/src/send.webhook.ts +137 -0
- package/src/session-contract.ts +3 -0
- package/src/session-key-normalization.ts +47 -0
- package/src/setup-account-state.ts +144 -0
- package/src/setup-adapter.ts +14 -0
- package/src/setup-core.ts +215 -0
- package/src/setup-runtime-helpers.ts +10 -0
- package/src/setup-surface.ts +132 -0
- package/src/shared-interactive.ts +167 -0
- package/src/shared.ts +197 -0
- package/src/status-issues.ts +201 -0
- package/src/subagent-hooks.ts +232 -0
- package/src/target-parsing.ts +70 -0
- package/src/target-resolver.ts +129 -0
- package/src/targets.ts +12 -0
- package/src/token.ts +107 -0
- package/src/ui-colors.ts +27 -0
- package/src/ui.ts +20 -0
- package/src/voice/access.ts +126 -0
- package/src/voice/audio.ts +249 -0
- package/src/voice/capture-state.ts +120 -0
- package/src/voice/command.ts +284 -0
- package/src/voice/config.ts +8 -0
- package/src/voice/ingress.ts +164 -0
- package/src/voice/manager.runtime.ts +14 -0
- package/src/voice/manager.ts +1155 -0
- package/src/voice/prompt.ts +22 -0
- package/src/voice/realtime.ts +1370 -0
- package/src/voice/receive-recovery.ts +159 -0
- package/src/voice/sanitize.ts +29 -0
- package/src/voice/sdk-runtime.ts +14 -0
- package/src/voice/segment.ts +160 -0
- package/src/voice/session.ts +81 -0
- package/src/voice/speaker-context.ts +127 -0
- package/src/voice/tts.ts +151 -0
- package/src/voice-message.ts +474 -0
- package/subagent-hooks-api.ts +27 -0
- package/test-api.ts +4 -0
- package/thread-binding-api.ts +1 -0
- package/timeouts.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { DirectoryConfigParams } from "autobot/plugin-sdk/directory-runtime";
|
|
2
|
+
import { buildMessagingTarget, type MessagingTarget } from "autobot/plugin-sdk/messaging-targets";
|
|
3
|
+
import { resolveDiscordAccount, resolveDiscordAccountAllowFrom } from "./accounts.js";
|
|
4
|
+
import { rememberDiscordDirectoryUser } from "./directory-cache.js";
|
|
5
|
+
import { listDiscordDirectoryPeersLive } from "./directory-live.js";
|
|
6
|
+
import { allowFromContainsDiscordUserId } from "./normalize.js";
|
|
7
|
+
import { parseDiscordSendTarget } from "./send-target-parsing.js";
|
|
8
|
+
import { type DiscordTargetParseOptions } from "./target-parsing.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a Discord username to user ID using the directory lookup.
|
|
12
|
+
* This enables sending DMs by username instead of requiring explicit user IDs.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveDiscordTarget(
|
|
15
|
+
raw: string,
|
|
16
|
+
options: DirectoryConfigParams,
|
|
17
|
+
parseOptions: DiscordTargetParseOptions = {},
|
|
18
|
+
): Promise<MessagingTarget | undefined> {
|
|
19
|
+
const trimmed = raw.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const likelyUsername = isLikelyUsername(trimmed);
|
|
25
|
+
const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
/^\d+$/.test(trimmed) &&
|
|
29
|
+
parseOptions.defaultKind !== "user" &&
|
|
30
|
+
isConfiguredAllowedDiscordDmUser(trimmed, options)
|
|
31
|
+
) {
|
|
32
|
+
return buildMessagingTarget("user", trimmed, trimmed);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse directly if it's already a known format. Use a safe parse so ambiguous
|
|
36
|
+
// numeric targets don't throw when we still want to attempt username lookup.
|
|
37
|
+
const directParse = safeParseDiscordTarget(trimmed, parseOptions);
|
|
38
|
+
if (directParse && directParse.kind !== "channel" && !likelyUsername) {
|
|
39
|
+
return directParse;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!shouldLookup) {
|
|
43
|
+
return directParse ?? parseDiscordSendTarget(trimmed, parseOptions);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const directoryEntries = await listDiscordDirectoryPeersLive({
|
|
48
|
+
...options,
|
|
49
|
+
query: trimmed,
|
|
50
|
+
limit: 1,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const match = directoryEntries[0];
|
|
54
|
+
if (match && match.kind === "user") {
|
|
55
|
+
const userId = match.id.replace(/^user:/, "");
|
|
56
|
+
const resolvedAccountId = resolveDiscordAccount({
|
|
57
|
+
cfg: options.cfg,
|
|
58
|
+
accountId: options.accountId,
|
|
59
|
+
}).accountId;
|
|
60
|
+
rememberDiscordDirectoryUser({
|
|
61
|
+
accountId: resolvedAccountId,
|
|
62
|
+
userId,
|
|
63
|
+
handles: [trimmed, match.name, match.handle],
|
|
64
|
+
});
|
|
65
|
+
return buildMessagingTarget("user", userId, trimmed);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Preserve legacy fallback behavior for channel names and direct ids.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return parseDiscordSendTarget(trimmed, parseOptions);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function parseAndResolveDiscordTarget(
|
|
75
|
+
raw: string,
|
|
76
|
+
options: DirectoryConfigParams,
|
|
77
|
+
parseOptions: DiscordTargetParseOptions = {},
|
|
78
|
+
): Promise<MessagingTarget> {
|
|
79
|
+
const resolved =
|
|
80
|
+
(await resolveDiscordTarget(raw, options, parseOptions)) ??
|
|
81
|
+
parseDiscordSendTarget(raw, parseOptions);
|
|
82
|
+
if (!resolved) {
|
|
83
|
+
throw new Error("Recipient is required for Discord sends");
|
|
84
|
+
}
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function safeParseDiscordTarget(
|
|
89
|
+
input: string,
|
|
90
|
+
options: DiscordTargetParseOptions,
|
|
91
|
+
): MessagingTarget | undefined {
|
|
92
|
+
try {
|
|
93
|
+
return parseDiscordSendTarget(input, options);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isConfiguredAllowedDiscordDmUser(input: string, options: DirectoryConfigParams): boolean {
|
|
100
|
+
const allowFrom =
|
|
101
|
+
resolveDiscordAccountAllowFrom({
|
|
102
|
+
cfg: options.cfg,
|
|
103
|
+
accountId: options.accountId,
|
|
104
|
+
}) ?? [];
|
|
105
|
+
return allowFromContainsDiscordUserId(allowFrom, input);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isExplicitUserLookup(input: string, options: DiscordTargetParseOptions): boolean {
|
|
109
|
+
if (/^<@!?(\d+)>$/.test(input)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (/^(user:|discord:)/.test(input)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (input.startsWith("@")) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (/^\d+$/.test(input)) {
|
|
119
|
+
return options.defaultKind === "user";
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isLikelyUsername(input: string): boolean {
|
|
125
|
+
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseDiscordTarget,
|
|
3
|
+
type DiscordTarget,
|
|
4
|
+
type DiscordTargetKind,
|
|
5
|
+
type DiscordTargetParseOptions,
|
|
6
|
+
resolveDiscordChannelId,
|
|
7
|
+
} from "./target-parsing.js";
|
|
8
|
+
import { resolveDiscordTarget } from "./target-resolver.js";
|
|
9
|
+
|
|
10
|
+
export { parseDiscordTarget, resolveDiscordChannelId };
|
|
11
|
+
export type { DiscordTarget, DiscordTargetKind, DiscordTargetParseOptions };
|
|
12
|
+
export { resolveDiscordTarget };
|
package/src/token.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BaseTokenResolution } from "autobot/plugin-sdk/channel-contract";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "autobot/plugin-sdk/routing";
|
|
4
|
+
import { resolveAccountEntry } from "autobot/plugin-sdk/routing";
|
|
5
|
+
import {
|
|
6
|
+
normalizeResolvedSecretInputString,
|
|
7
|
+
resolveSecretInputString,
|
|
8
|
+
} from "autobot/plugin-sdk/secret-input";
|
|
9
|
+
import { selectDiscordRuntimeConfig } from "./runtime-config.js";
|
|
10
|
+
|
|
11
|
+
type DiscordTokenSource = "env" | "config" | "none";
|
|
12
|
+
export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing";
|
|
13
|
+
|
|
14
|
+
export type DiscordTokenResolution = BaseTokenResolution & {
|
|
15
|
+
source: DiscordTokenSource;
|
|
16
|
+
tokenStatus: DiscordCredentialStatus;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type DiscordTokenValueResolution =
|
|
20
|
+
| { status: "available"; value: string }
|
|
21
|
+
| { status: "configured_unavailable" }
|
|
22
|
+
| { status: "missing" };
|
|
23
|
+
|
|
24
|
+
function stripDiscordBotPrefix(token: string): string {
|
|
25
|
+
return token.replace(/^Bot\s+/i, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeDiscordToken(raw: unknown, path: string): string | undefined {
|
|
29
|
+
const trimmed = normalizeResolvedSecretInputString({ value: raw, path });
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return stripDiscordBotPrefix(trimmed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveDiscordTokenValue(params: {
|
|
37
|
+
cfg: AutoBotConfig;
|
|
38
|
+
value: unknown;
|
|
39
|
+
path: string;
|
|
40
|
+
}): DiscordTokenValueResolution {
|
|
41
|
+
const resolved = resolveSecretInputString({
|
|
42
|
+
value: params.value,
|
|
43
|
+
path: params.path,
|
|
44
|
+
defaults: params.cfg.secrets?.defaults,
|
|
45
|
+
mode: "inspect",
|
|
46
|
+
});
|
|
47
|
+
if (resolved.status === "available") {
|
|
48
|
+
return {
|
|
49
|
+
status: "available",
|
|
50
|
+
value: stripDiscordBotPrefix(resolved.value),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (resolved.status === "configured_unavailable") {
|
|
54
|
+
return { status: "configured_unavailable" };
|
|
55
|
+
}
|
|
56
|
+
return { status: "missing" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveDiscordToken(
|
|
60
|
+
cfg: AutoBotConfig,
|
|
61
|
+
opts: { accountId?: string | null; envToken?: string | null } = {},
|
|
62
|
+
): DiscordTokenResolution {
|
|
63
|
+
const selectedCfg = selectDiscordRuntimeConfig(cfg);
|
|
64
|
+
const accountId = normalizeAccountId(opts.accountId);
|
|
65
|
+
const discordCfg = selectedCfg?.channels?.discord;
|
|
66
|
+
const accountCfg = resolveAccountEntry(discordCfg?.accounts, accountId);
|
|
67
|
+
const hasAccountToken = Boolean(
|
|
68
|
+
accountCfg &&
|
|
69
|
+
Object.prototype.hasOwnProperty.call(accountCfg as Record<string, unknown>, "token"),
|
|
70
|
+
);
|
|
71
|
+
const accountToken = resolveDiscordTokenValue({
|
|
72
|
+
cfg: selectedCfg,
|
|
73
|
+
value: (accountCfg as { token?: unknown } | undefined)?.token,
|
|
74
|
+
path: `channels.discord.accounts.${accountId}.token`,
|
|
75
|
+
});
|
|
76
|
+
if (accountToken.status === "available" && accountToken.value) {
|
|
77
|
+
return { token: accountToken.value, source: "config", tokenStatus: "available" };
|
|
78
|
+
}
|
|
79
|
+
if (accountToken.status === "configured_unavailable") {
|
|
80
|
+
return { token: "", source: "config", tokenStatus: "configured_unavailable" };
|
|
81
|
+
}
|
|
82
|
+
if (hasAccountToken) {
|
|
83
|
+
return { token: "", source: "none", tokenStatus: "missing" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const configToken = resolveDiscordTokenValue({
|
|
87
|
+
cfg: selectedCfg,
|
|
88
|
+
value: discordCfg?.token,
|
|
89
|
+
path: "channels.discord.token",
|
|
90
|
+
});
|
|
91
|
+
if (configToken.status === "available" && configToken.value) {
|
|
92
|
+
return { token: configToken.value, source: "config", tokenStatus: "available" };
|
|
93
|
+
}
|
|
94
|
+
if (configToken.status === "configured_unavailable") {
|
|
95
|
+
return { token: "", source: "config", tokenStatus: "configured_unavailable" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
99
|
+
const envToken = allowEnv
|
|
100
|
+
? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN, "DISCORD_BOT_TOKEN")
|
|
101
|
+
: undefined;
|
|
102
|
+
if (envToken) {
|
|
103
|
+
return { token: envToken, source: "env", tokenStatus: "available" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { token: "", source: "none", tokenStatus: "missing" };
|
|
107
|
+
}
|
package/src/ui-colors.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
2
|
+
import { inspectDiscordAccount } from "./account-inspect.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DISCORD_ACCENT_COLOR = "#5865F2";
|
|
5
|
+
|
|
6
|
+
type ResolveDiscordAccentColorParams = {
|
|
7
|
+
cfg: AutoBotConfig;
|
|
8
|
+
accountId?: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function normalizeDiscordAccentColor(raw?: string | null): string | null {
|
|
12
|
+
const trimmed = (raw ?? "").trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const normalized = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
17
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return normalized.toUpperCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveDiscordAccentColor(params: ResolveDiscordAccentColorParams): string {
|
|
24
|
+
const account = inspectDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
25
|
+
const configured = normalizeDiscordAccentColor(account.config.ui?.components?.accentColor);
|
|
26
|
+
return configured ?? DEFAULT_DISCORD_ACCENT_COLOR;
|
|
27
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
2
|
+
import { Container } from "./internal/discord.js";
|
|
3
|
+
import { normalizeDiscordAccentColor, resolveDiscordAccentColor } from "./ui-colors.js";
|
|
4
|
+
|
|
5
|
+
type DiscordContainerComponents = ConstructorParameters<typeof Container>[0];
|
|
6
|
+
|
|
7
|
+
export class DiscordUiContainer extends Container {
|
|
8
|
+
constructor(params: {
|
|
9
|
+
cfg: AutoBotConfig;
|
|
10
|
+
accountId?: string | null;
|
|
11
|
+
components?: DiscordContainerComponents;
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
spoiler?: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
const accentOverride = normalizeDiscordAccentColor(params.accentColor);
|
|
16
|
+
const accentColor =
|
|
17
|
+
accentOverride ?? resolveDiscordAccentColor({ cfg: params.cfg, accountId: params.accountId });
|
|
18
|
+
super(params.components, { accentColor, spoiler: params.spoiler });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { resolveCommandAuthorizedFromAuthorizers } from "autobot/plugin-sdk/command-auth-native";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
import type { DiscordAccountConfig } from "autobot/plugin-sdk/config-contracts";
|
|
4
|
+
import { resolveOpenProviderRuntimeGroupPolicy } from "autobot/plugin-sdk/runtime-group-policy";
|
|
5
|
+
import type { Guild } from "../internal/discord.js";
|
|
6
|
+
import {
|
|
7
|
+
isDiscordGroupAllowedByPolicy,
|
|
8
|
+
resolveDiscordChannelConfigWithFallback,
|
|
9
|
+
type DiscordChannelConfigResolved,
|
|
10
|
+
resolveDiscordGuildEntry,
|
|
11
|
+
resolveDiscordMemberAccessState,
|
|
12
|
+
resolveDiscordOwnerAccess,
|
|
13
|
+
} from "../monitor/allow-list.js";
|
|
14
|
+
|
|
15
|
+
export async function authorizeDiscordVoiceIngress(params: {
|
|
16
|
+
cfg: AutoBotConfig;
|
|
17
|
+
discordConfig: DiscordAccountConfig;
|
|
18
|
+
accountId?: string;
|
|
19
|
+
groupPolicy?: "open" | "disabled" | "allowlist";
|
|
20
|
+
useAccessGroups?: boolean;
|
|
21
|
+
guild?: Guild<true> | Guild | null;
|
|
22
|
+
guildName?: string;
|
|
23
|
+
guildId: string;
|
|
24
|
+
channelId: string;
|
|
25
|
+
channelName?: string;
|
|
26
|
+
channelSlug: string;
|
|
27
|
+
parentId?: string;
|
|
28
|
+
parentName?: string;
|
|
29
|
+
parentSlug?: string;
|
|
30
|
+
scope?: "channel" | "thread";
|
|
31
|
+
channelLabel?: string;
|
|
32
|
+
memberRoleIds: string[];
|
|
33
|
+
ownerAllowFrom?: string[];
|
|
34
|
+
sender: { id: string; name?: string; tag?: string };
|
|
35
|
+
}): Promise<
|
|
36
|
+
{ ok: true; channelConfig?: DiscordChannelConfigResolved | null } | { ok: false; message: string }
|
|
37
|
+
> {
|
|
38
|
+
const groupPolicy =
|
|
39
|
+
params.groupPolicy ??
|
|
40
|
+
resolveOpenProviderRuntimeGroupPolicy({
|
|
41
|
+
providerConfigPresent: params.cfg.channels?.discord !== undefined,
|
|
42
|
+
groupPolicy: params.discordConfig.groupPolicy,
|
|
43
|
+
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy,
|
|
44
|
+
}).groupPolicy;
|
|
45
|
+
const guild =
|
|
46
|
+
params.guild ??
|
|
47
|
+
({ id: params.guildId, ...(params.guildName ? { name: params.guildName } : {}) } as Guild);
|
|
48
|
+
const guildInfo = resolveDiscordGuildEntry({
|
|
49
|
+
guild,
|
|
50
|
+
guildId: params.guildId,
|
|
51
|
+
guildEntries: params.discordConfig.guilds,
|
|
52
|
+
});
|
|
53
|
+
const channelConfig = params.channelId
|
|
54
|
+
? resolveDiscordChannelConfigWithFallback({
|
|
55
|
+
guildInfo,
|
|
56
|
+
channelId: params.channelId,
|
|
57
|
+
channelName: params.channelName,
|
|
58
|
+
channelSlug: params.channelSlug,
|
|
59
|
+
parentId: params.parentId,
|
|
60
|
+
parentName: params.parentName,
|
|
61
|
+
parentSlug: params.parentSlug,
|
|
62
|
+
scope: params.scope,
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
if (channelConfig?.enabled === false) {
|
|
67
|
+
return { ok: false, message: "This channel is disabled." };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const channelAllowlistConfigured =
|
|
71
|
+
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
|
72
|
+
if (!params.channelId && groupPolicy === "allowlist" && channelAllowlistConfigured) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
message: `${params.channelLabel ?? "This channel"} is not allowlisted for voice commands.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const channelAllowed = channelConfig ? channelConfig.allowed : !channelAllowlistConfigured;
|
|
80
|
+
if (
|
|
81
|
+
!isDiscordGroupAllowedByPolicy({
|
|
82
|
+
groupPolicy,
|
|
83
|
+
guildAllowlisted: Boolean(guildInfo),
|
|
84
|
+
channelAllowlistConfigured,
|
|
85
|
+
channelAllowed,
|
|
86
|
+
}) ||
|
|
87
|
+
channelConfig?.allowed === false
|
|
88
|
+
) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: `${params.channelLabel ?? "This channel"} is not allowlisted for voice commands.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
|
|
96
|
+
channelConfig,
|
|
97
|
+
guildInfo,
|
|
98
|
+
memberRoleIds: params.memberRoleIds,
|
|
99
|
+
sender: params.sender,
|
|
100
|
+
allowNameMatching: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const { ownerAllowList, ownerAllowed } = resolveDiscordOwnerAccess({
|
|
104
|
+
allowFrom:
|
|
105
|
+
params.ownerAllowFrom ?? params.discordConfig.allowFrom ?? params.discordConfig.dm?.allowFrom,
|
|
106
|
+
sender: params.sender,
|
|
107
|
+
allowNameMatching: false,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const useAccessGroups = params.useAccessGroups ?? params.cfg.commands?.useAccessGroups !== false;
|
|
111
|
+
const authorizers = useAccessGroups
|
|
112
|
+
? [
|
|
113
|
+
{ configured: ownerAllowList != null, allowed: ownerAllowed },
|
|
114
|
+
{ configured: hasAccessRestrictions, allowed: memberAllowed },
|
|
115
|
+
]
|
|
116
|
+
: [{ configured: hasAccessRestrictions, allowed: memberAllowed }];
|
|
117
|
+
|
|
118
|
+
const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
|
119
|
+
useAccessGroups,
|
|
120
|
+
authorizers,
|
|
121
|
+
modeWhenAccessGroupsOff: "configured",
|
|
122
|
+
});
|
|
123
|
+
return commandAuthorized
|
|
124
|
+
? { ok: true, channelConfig }
|
|
125
|
+
: { ok: false, message: "You are not authorized to use this command." };
|
|
126
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import type { Readable } from "node:stream";
|
|
4
|
+
import { resamplePcm } from "autobot/plugin-sdk/realtime-voice";
|
|
5
|
+
import { logVerbose, shouldLogVerbose } from "autobot/plugin-sdk/runtime-env";
|
|
6
|
+
import { formatErrorMessage } from "autobot/plugin-sdk/ssrf-runtime";
|
|
7
|
+
import { tempWorkspace, resolvePreferredAutoBotTmpDir } from "autobot/plugin-sdk/temp-path";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
const SAMPLE_RATE = 48_000;
|
|
12
|
+
const CHANNELS = 2;
|
|
13
|
+
const BIT_DEPTH = 16;
|
|
14
|
+
|
|
15
|
+
type OpusDecoder = {
|
|
16
|
+
decode: (buffer: Buffer) => Buffer;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type OpusDecoderFactory = {
|
|
20
|
+
load: () => OpusDecoder;
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type OpusDecoderPreference = "native" | "opusscript";
|
|
25
|
+
|
|
26
|
+
let warnedOpusMissing = false;
|
|
27
|
+
let cachedOpusDecoderFactory: OpusDecoderFactory | null | "unresolved" = "unresolved";
|
|
28
|
+
|
|
29
|
+
function buildWavBuffer(pcm: Buffer): Buffer {
|
|
30
|
+
const blockAlign = (CHANNELS * BIT_DEPTH) / 8;
|
|
31
|
+
const byteRate = SAMPLE_RATE * blockAlign;
|
|
32
|
+
const header = Buffer.alloc(44);
|
|
33
|
+
header.write("RIFF", 0);
|
|
34
|
+
header.writeUInt32LE(36 + pcm.length, 4);
|
|
35
|
+
header.write("WAVE", 8);
|
|
36
|
+
header.write("fmt ", 12);
|
|
37
|
+
header.writeUInt32LE(16, 16);
|
|
38
|
+
header.writeUInt16LE(1, 20);
|
|
39
|
+
header.writeUInt16LE(CHANNELS, 22);
|
|
40
|
+
header.writeUInt32LE(SAMPLE_RATE, 24);
|
|
41
|
+
header.writeUInt32LE(byteRate, 28);
|
|
42
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
43
|
+
header.writeUInt16LE(BIT_DEPTH, 34);
|
|
44
|
+
header.write("data", 36);
|
|
45
|
+
header.writeUInt32LE(pcm.length, 40);
|
|
46
|
+
return Buffer.concat([header, pcm]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveOpusDecoderFactory(params: {
|
|
50
|
+
onWarn: (message: string) => void;
|
|
51
|
+
}): OpusDecoderFactory | null {
|
|
52
|
+
const nativeFactory: OpusDecoderFactory = {
|
|
53
|
+
name: "@discordjs/opus",
|
|
54
|
+
load: () => {
|
|
55
|
+
const DiscordOpus = require("@discordjs/opus") as {
|
|
56
|
+
OpusEncoder: new (
|
|
57
|
+
sampleRate: number,
|
|
58
|
+
channels: number,
|
|
59
|
+
) => {
|
|
60
|
+
decode: (buffer: Buffer) => Buffer;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
return new DiscordOpus.OpusEncoder(SAMPLE_RATE, CHANNELS);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const opusscriptFactory: OpusDecoderFactory = {
|
|
67
|
+
name: "opusscript",
|
|
68
|
+
load: () => {
|
|
69
|
+
const OpusScript = require("opusscript") as {
|
|
70
|
+
new (sampleRate: number, channels: number, application: number): OpusDecoder;
|
|
71
|
+
Application: { AUDIO: number };
|
|
72
|
+
};
|
|
73
|
+
return new OpusScript(SAMPLE_RATE, CHANNELS, OpusScript.Application.AUDIO);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const factories: OpusDecoderFactory[] =
|
|
77
|
+
resolveOpusDecoderPreference() === "native"
|
|
78
|
+
? [nativeFactory, opusscriptFactory]
|
|
79
|
+
: [opusscriptFactory, nativeFactory];
|
|
80
|
+
|
|
81
|
+
const failures: string[] = [];
|
|
82
|
+
for (const factory of factories) {
|
|
83
|
+
try {
|
|
84
|
+
factory.load();
|
|
85
|
+
return factory;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
failures.push(`${factory.name}: ${formatErrorMessage(err)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!warnedOpusMissing) {
|
|
92
|
+
warnedOpusMissing = true;
|
|
93
|
+
params.onWarn(
|
|
94
|
+
`discord voice: no usable opus decoder available (${failures.join("; ")}); cannot decode voice audio`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveOpusDecoderPreference(
|
|
101
|
+
value = process.env.AUTOBOT_DISCORD_OPUS_DECODER,
|
|
102
|
+
): OpusDecoderPreference {
|
|
103
|
+
const normalized = value?.trim().toLowerCase();
|
|
104
|
+
if (normalized === "native" || normalized === "@discordjs/opus") {
|
|
105
|
+
return "native";
|
|
106
|
+
}
|
|
107
|
+
return "opusscript";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getOrCreateOpusDecoderFactory(params: {
|
|
111
|
+
onWarn: (message: string) => void;
|
|
112
|
+
}): OpusDecoderFactory | null {
|
|
113
|
+
if (cachedOpusDecoderFactory !== "unresolved") {
|
|
114
|
+
return cachedOpusDecoderFactory;
|
|
115
|
+
}
|
|
116
|
+
cachedOpusDecoderFactory = resolveOpusDecoderFactory(params);
|
|
117
|
+
return cachedOpusDecoderFactory;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createOpusDecoder(params: {
|
|
121
|
+
onWarn: (message: string) => void;
|
|
122
|
+
}): { decoder: OpusDecoder; name: string } | null {
|
|
123
|
+
const factory = getOrCreateOpusDecoderFactory(params);
|
|
124
|
+
if (!factory) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return { decoder: factory.load(), name: factory.name };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function decodeOpusStream(
|
|
131
|
+
stream: Readable,
|
|
132
|
+
params: { onVerbose: (message: string) => void; onWarn: (message: string) => void },
|
|
133
|
+
): Promise<Buffer> {
|
|
134
|
+
const selected = createOpusDecoder({ onWarn: params.onWarn });
|
|
135
|
+
if (!selected) {
|
|
136
|
+
return Buffer.alloc(0);
|
|
137
|
+
}
|
|
138
|
+
params.onVerbose(`opus decoder: ${selected.name}`);
|
|
139
|
+
const chunks: Buffer[] = [];
|
|
140
|
+
try {
|
|
141
|
+
for await (const chunk of stream) {
|
|
142
|
+
if (!chunk || !(chunk instanceof Buffer) || chunk.length === 0) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const decoded = selected.decoder.decode(chunk);
|
|
146
|
+
if (decoded && decoded.length > 0) {
|
|
147
|
+
chunks.push(Buffer.from(decoded));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (shouldLogVerbose()) {
|
|
152
|
+
logVerbose(`discord voice: opus decode failed: ${formatErrorMessage(err)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return chunks.length > 0 ? Buffer.concat(chunks) : Buffer.alloc(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function decodeOpusStreamChunks(
|
|
159
|
+
stream: Readable,
|
|
160
|
+
params: {
|
|
161
|
+
onChunk: (pcm48kStereo: Buffer) => void;
|
|
162
|
+
onVerbose: (message: string) => void;
|
|
163
|
+
onWarn: (message: string) => void;
|
|
164
|
+
},
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const selected = createOpusDecoder({ onWarn: params.onWarn });
|
|
167
|
+
if (!selected) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
params.onVerbose(`opus decoder: ${selected.name}`);
|
|
171
|
+
try {
|
|
172
|
+
for await (const chunk of stream) {
|
|
173
|
+
if (!chunk || !(chunk instanceof Buffer) || chunk.length === 0) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const decoded = selected.decoder.decode(chunk);
|
|
177
|
+
if (decoded && decoded.length > 0) {
|
|
178
|
+
params.onChunk(Buffer.from(decoded));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (shouldLogVerbose()) {
|
|
183
|
+
logVerbose(`discord voice: opus decode failed: ${formatErrorMessage(err)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function convertDiscordPcm48kStereoToRealtimePcm24kMono(pcm: Buffer): Buffer {
|
|
189
|
+
const frameCount = Math.floor(pcm.length / 4);
|
|
190
|
+
if (frameCount === 0) {
|
|
191
|
+
return Buffer.alloc(0);
|
|
192
|
+
}
|
|
193
|
+
const mono48k = Buffer.alloc(frameCount * 2);
|
|
194
|
+
for (let frame = 0; frame < frameCount; frame += 1) {
|
|
195
|
+
const offset = frame * 4;
|
|
196
|
+
const left = pcm.readInt16LE(offset);
|
|
197
|
+
const right = pcm.readInt16LE(offset + 2);
|
|
198
|
+
mono48k.writeInt16LE(Math.round((left + right) / 2), frame * 2);
|
|
199
|
+
}
|
|
200
|
+
return resamplePcm(mono48k, SAMPLE_RATE, 24_000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function convertRealtimePcm24kMonoToDiscordPcm48kStereo(pcm: Buffer): Buffer {
|
|
204
|
+
const mono48k = resamplePcm(pcm, 24_000, SAMPLE_RATE);
|
|
205
|
+
const sampleCount = Math.floor(mono48k.length / 2);
|
|
206
|
+
if (sampleCount === 0) {
|
|
207
|
+
return Buffer.alloc(0);
|
|
208
|
+
}
|
|
209
|
+
const stereo = Buffer.alloc(sampleCount * 4);
|
|
210
|
+
for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
|
|
211
|
+
const sample = mono48k.readInt16LE(sampleIndex * 2);
|
|
212
|
+
const offset = sampleIndex * 4;
|
|
213
|
+
stereo.writeInt16LE(sample, offset);
|
|
214
|
+
stereo.writeInt16LE(sample, offset + 2);
|
|
215
|
+
}
|
|
216
|
+
return stereo;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function estimateDurationSeconds(pcm: Buffer): number {
|
|
220
|
+
const bytesPerSample = (BIT_DEPTH / 8) * CHANNELS;
|
|
221
|
+
if (bytesPerSample <= 0) {
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
return pcm.length / (bytesPerSample * SAMPLE_RATE);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function writeVoiceWavFile(
|
|
228
|
+
pcm: Buffer,
|
|
229
|
+
): Promise<{ path: string; durationSeconds: number }> {
|
|
230
|
+
const workspace = await tempWorkspace({
|
|
231
|
+
rootDir: resolvePreferredAutoBotTmpDir(),
|
|
232
|
+
prefix: "discord-voice-",
|
|
233
|
+
});
|
|
234
|
+
const wav = buildWavBuffer(pcm);
|
|
235
|
+
const filePath = await workspace.write("segment.wav", wav);
|
|
236
|
+
scheduleTempCleanup(workspace.dir);
|
|
237
|
+
return { path: filePath, durationSeconds: estimateDurationSeconds(pcm) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function scheduleTempCleanup(tempDir: string, delayMs: number = 30 * 60 * 1000): void {
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
fs.rm(tempDir, { recursive: true, force: true }).catch((err) => {
|
|
243
|
+
if (shouldLogVerbose()) {
|
|
244
|
+
logVerbose(`discord voice: temp cleanup failed for ${tempDir}: ${formatErrorMessage(err)}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}, delayMs);
|
|
248
|
+
timer.unref();
|
|
249
|
+
}
|