@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
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { type ChannelDoctorAdapter } from "autobot/plugin-sdk/channel-contract";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
import { collectProviderDangerousNameMatchingScopes } from "autobot/plugin-sdk/runtime-doctor";
|
|
4
|
+
import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
|
|
5
|
+
import { inspectDiscordAccount } from "./account-inspect.js";
|
|
6
|
+
import { resolveDefaultDiscordAccountId } from "./accounts.js";
|
|
7
|
+
import { normalizeCompatibilityConfig as normalizeDiscordCompatibilityConfig } from "./doctor-contract.js";
|
|
8
|
+
import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js";
|
|
9
|
+
import { isDiscordMutableAllowEntry } from "./security-doctor.js";
|
|
10
|
+
|
|
11
|
+
type DiscordNumericIdHit = { path: string; entry: number; safe: boolean };
|
|
12
|
+
|
|
13
|
+
type DiscordIdListRef = {
|
|
14
|
+
pathLabel: string;
|
|
15
|
+
holder: Record<string, unknown>;
|
|
16
|
+
key: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
|
20
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
21
|
+
? (value as Record<string, unknown>)
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sanitizeForLog(value: string): string {
|
|
26
|
+
return value.replace(/\p{Cc}+/gu, " ").trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectDiscordAccountScopes(
|
|
30
|
+
cfg: AutoBotConfig,
|
|
31
|
+
): Array<{ prefix: string; account: Record<string, unknown> }> {
|
|
32
|
+
const scopes: Array<{ prefix: string; account: Record<string, unknown> }> = [];
|
|
33
|
+
const discord = asObjectRecord(cfg.channels?.discord);
|
|
34
|
+
if (!discord) {
|
|
35
|
+
return scopes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
scopes.push({ prefix: "channels.discord", account: discord });
|
|
39
|
+
const accounts = asObjectRecord(discord.accounts);
|
|
40
|
+
if (!accounts) {
|
|
41
|
+
return scopes;
|
|
42
|
+
}
|
|
43
|
+
for (const key of Object.keys(accounts)) {
|
|
44
|
+
const account = asObjectRecord(accounts[key]);
|
|
45
|
+
if (account) {
|
|
46
|
+
scopes.push({ prefix: `channels.discord.accounts.${key}`, account });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return scopes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectDiscordIdLists(
|
|
53
|
+
prefix: string,
|
|
54
|
+
account: Record<string, unknown>,
|
|
55
|
+
): DiscordIdListRef[] {
|
|
56
|
+
const refs: DiscordIdListRef[] = [
|
|
57
|
+
{ pathLabel: `${prefix}.allowFrom`, holder: account, key: "allowFrom" },
|
|
58
|
+
];
|
|
59
|
+
const dm = asObjectRecord(account.dm);
|
|
60
|
+
if (dm) {
|
|
61
|
+
refs.push({ pathLabel: `${prefix}.dm.allowFrom`, holder: dm, key: "allowFrom" });
|
|
62
|
+
refs.push({ pathLabel: `${prefix}.dm.groupChannels`, holder: dm, key: "groupChannels" });
|
|
63
|
+
}
|
|
64
|
+
const execApprovals = asObjectRecord(account.execApprovals);
|
|
65
|
+
if (execApprovals) {
|
|
66
|
+
refs.push({
|
|
67
|
+
pathLabel: `${prefix}.execApprovals.approvers`,
|
|
68
|
+
holder: execApprovals,
|
|
69
|
+
key: "approvers",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const guilds = asObjectRecord(account.guilds);
|
|
73
|
+
if (!guilds) {
|
|
74
|
+
return refs;
|
|
75
|
+
}
|
|
76
|
+
for (const guildId of Object.keys(guilds)) {
|
|
77
|
+
const guild = asObjectRecord(guilds[guildId]);
|
|
78
|
+
if (!guild) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
refs.push({ pathLabel: `${prefix}.guilds.${guildId}.users`, holder: guild, key: "users" });
|
|
82
|
+
refs.push({ pathLabel: `${prefix}.guilds.${guildId}.roles`, holder: guild, key: "roles" });
|
|
83
|
+
const channels = asObjectRecord(guild.channels);
|
|
84
|
+
if (!channels) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const channelId of Object.keys(channels)) {
|
|
88
|
+
const channel = asObjectRecord(channels[channelId]);
|
|
89
|
+
if (!channel) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
refs.push({
|
|
93
|
+
pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.users`,
|
|
94
|
+
holder: channel,
|
|
95
|
+
key: "users",
|
|
96
|
+
});
|
|
97
|
+
refs.push({
|
|
98
|
+
pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.roles`,
|
|
99
|
+
holder: channel,
|
|
100
|
+
key: "roles",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return refs;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function scanDiscordNumericIdEntries(cfg: AutoBotConfig): DiscordNumericIdHit[] {
|
|
108
|
+
const hits: DiscordNumericIdHit[] = [];
|
|
109
|
+
const scanList = (pathLabel: string, list: unknown) => {
|
|
110
|
+
if (!Array.isArray(list)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const [index, entry] of list.entries()) {
|
|
114
|
+
if (typeof entry !== "number") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
hits.push({
|
|
118
|
+
path: `${pathLabel}[${index}]`,
|
|
119
|
+
entry,
|
|
120
|
+
safe: Number.isSafeInteger(entry) && entry >= 0,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
for (const scope of collectDiscordAccountScopes(cfg)) {
|
|
126
|
+
for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) {
|
|
127
|
+
scanList(ref.pathLabel, ref.holder[ref.key]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return hits;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function collectDiscordNumericIdWarnings(params: {
|
|
134
|
+
hits: DiscordNumericIdHit[];
|
|
135
|
+
doctorFixCommand: string;
|
|
136
|
+
}): string[] {
|
|
137
|
+
if (params.hits.length === 0) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const hitsByListPath = new Map<string, DiscordNumericIdHit[]>();
|
|
141
|
+
for (const hit of params.hits) {
|
|
142
|
+
const listPath = hit.path.replace(/\[\d+\]$/, "");
|
|
143
|
+
const existing = hitsByListPath.get(listPath);
|
|
144
|
+
if (existing) {
|
|
145
|
+
existing.push(hit);
|
|
146
|
+
} else {
|
|
147
|
+
hitsByListPath.set(listPath, [hit]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const repairableHits: DiscordNumericIdHit[] = [];
|
|
152
|
+
const blockedHits: DiscordNumericIdHit[] = [];
|
|
153
|
+
for (const hits of hitsByListPath.values()) {
|
|
154
|
+
if (hits.some((hit) => !hit.safe)) {
|
|
155
|
+
blockedHits.push(...hits);
|
|
156
|
+
} else {
|
|
157
|
+
repairableHits.push(...hits);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const lines: string[] = [];
|
|
162
|
+
if (repairableHits.length > 0) {
|
|
163
|
+
const sample = repairableHits[0];
|
|
164
|
+
lines.push(
|
|
165
|
+
`- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${sanitizeForLog(sample.path)}=${sanitizeForLog(String(sample.entry))}).`,
|
|
166
|
+
`- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (blockedHits.length > 0) {
|
|
170
|
+
const sample = blockedHits[0];
|
|
171
|
+
lines.push(
|
|
172
|
+
`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${sanitizeForLog(sample.path)}).`,
|
|
173
|
+
`- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function maybeRepairDiscordNumericIds(
|
|
180
|
+
cfg: AutoBotConfig,
|
|
181
|
+
doctorFixCommand: string,
|
|
182
|
+
): { config: AutoBotConfig; changes: string[]; warnings?: string[] } {
|
|
183
|
+
const hits = scanDiscordNumericIdEntries(cfg);
|
|
184
|
+
if (hits.length === 0) {
|
|
185
|
+
return { config: cfg, changes: [] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const next = structuredClone(cfg);
|
|
189
|
+
const changes: string[] = [];
|
|
190
|
+
|
|
191
|
+
const repairList = (pathLabel: string, holder: Record<string, unknown>, key: string) => {
|
|
192
|
+
const raw = holder[key];
|
|
193
|
+
if (!Array.isArray(raw)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const hasUnsafe = raw.some(
|
|
197
|
+
(entry) => typeof entry === "number" && (!Number.isSafeInteger(entry) || entry < 0),
|
|
198
|
+
);
|
|
199
|
+
if (hasUnsafe) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let converted = 0;
|
|
203
|
+
holder[key] = raw.map((entry) => {
|
|
204
|
+
if (typeof entry === "number") {
|
|
205
|
+
converted += 1;
|
|
206
|
+
return String(entry);
|
|
207
|
+
}
|
|
208
|
+
return entry;
|
|
209
|
+
});
|
|
210
|
+
if (converted > 0) {
|
|
211
|
+
changes.push(
|
|
212
|
+
`- ${sanitizeForLog(pathLabel)}: converted ${converted} numeric ${converted === 1 ? "ID" : "IDs"} to strings`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const scope of collectDiscordAccountScopes(next)) {
|
|
218
|
+
for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) {
|
|
219
|
+
repairList(ref.pathLabel, ref.holder, ref.key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (changes.length === 0) {
|
|
224
|
+
return {
|
|
225
|
+
config: cfg,
|
|
226
|
+
changes: [],
|
|
227
|
+
warnings: collectDiscordNumericIdWarnings({ hits, doctorFixCommand }),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
config: next,
|
|
232
|
+
changes,
|
|
233
|
+
warnings: collectDiscordNumericIdWarnings({
|
|
234
|
+
hits: hits.filter((hit) => !hit.safe),
|
|
235
|
+
doctorFixCommand,
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function collectDiscordMissingEnvTokenWarnings(params: {
|
|
241
|
+
cfg: AutoBotConfig;
|
|
242
|
+
env?: NodeJS.ProcessEnv;
|
|
243
|
+
}): string[] {
|
|
244
|
+
if (resolveDefaultDiscordAccountId(params.cfg) !== "default") {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
const account = inspectDiscordAccount({
|
|
248
|
+
cfg: params.cfg,
|
|
249
|
+
accountId: "default",
|
|
250
|
+
envToken: params.env?.DISCORD_BOT_TOKEN ?? "",
|
|
251
|
+
});
|
|
252
|
+
if (!account.enabled || account.tokenStatus !== "missing" || account.tokenSource !== "none") {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return [
|
|
256
|
+
"- channels.discord: default account has no available bot token, and DISCORD_BOT_TOKEN is absent in this doctor environment. After migration, verify DISCORD_BOT_TOKEN is present in the state-dir .env or configure channels.discord.token / channels.discord.accounts.default.token as a SecretRef.",
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function collectDiscordMutableAllowlistWarnings(cfg: AutoBotConfig): string[] {
|
|
261
|
+
const hits: Array<{ path: string; entry: string }> = [];
|
|
262
|
+
const addHits = (pathLabel: string, list: unknown) => {
|
|
263
|
+
if (!Array.isArray(list)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
for (const entry of list) {
|
|
267
|
+
const text = normalizeOptionalString(String(entry)) ?? "";
|
|
268
|
+
if (!text || text === "*" || !isDiscordMutableAllowEntry(text)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
hits.push({ path: pathLabel, entry: text });
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "discord")) {
|
|
276
|
+
if (scope.dangerousNameMatchingEnabled) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
addHits(`${scope.prefix}.allowFrom`, scope.account.allowFrom);
|
|
280
|
+
const dm = asObjectRecord(scope.account.dm);
|
|
281
|
+
if (dm) {
|
|
282
|
+
addHits(`${scope.prefix}.dm.allowFrom`, dm.allowFrom);
|
|
283
|
+
}
|
|
284
|
+
const guilds = asObjectRecord(scope.account.guilds);
|
|
285
|
+
if (!guilds) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
for (const [guildId, guildRaw] of Object.entries(guilds)) {
|
|
289
|
+
const guild = asObjectRecord(guildRaw);
|
|
290
|
+
if (!guild) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
addHits(`${scope.prefix}.guilds.${guildId}.users`, guild.users);
|
|
294
|
+
const channels = asObjectRecord(guild.channels);
|
|
295
|
+
if (!channels) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
for (const [channelId, channelRaw] of Object.entries(channels)) {
|
|
299
|
+
const channel = asObjectRecord(channelRaw);
|
|
300
|
+
if (channel) {
|
|
301
|
+
addHits(`${scope.prefix}.guilds.${guildId}.channels.${channelId}.users`, channel.users);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (hits.length === 0) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
const exampleLines = hits
|
|
311
|
+
.slice(0, 8)
|
|
312
|
+
.map((hit) => `- ${sanitizeForLog(hit.path)}: ${sanitizeForLog(hit.entry)}`);
|
|
313
|
+
const remaining =
|
|
314
|
+
hits.length > 8 ? `- +${hits.length - 8} more mutable allowlist entries.` : null;
|
|
315
|
+
return [
|
|
316
|
+
`- Found ${hits.length} mutable allowlist ${hits.length === 1 ? "entry" : "entries"} across discord while name matching is disabled by default.`,
|
|
317
|
+
...exampleLines,
|
|
318
|
+
...(remaining ? [remaining] : []),
|
|
319
|
+
`- Option A (break-glass): enable channels.discord.dangerousNameMatching=true for the affected scope.`,
|
|
320
|
+
`- Option B (recommended): resolve names to stable Discord IDs and rewrite the allowlist entries.`,
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const discordDoctor: ChannelDoctorAdapter = {
|
|
325
|
+
dmAllowFromMode: "topOnly",
|
|
326
|
+
groupModel: "route",
|
|
327
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
328
|
+
warnOnEmptyGroupSenderAllowlist: false,
|
|
329
|
+
legacyConfigRules: DISCORD_LEGACY_CONFIG_RULES,
|
|
330
|
+
normalizeCompatibilityConfig: normalizeDiscordCompatibilityConfig,
|
|
331
|
+
collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [
|
|
332
|
+
...collectDiscordMissingEnvTokenWarnings({ cfg, env }),
|
|
333
|
+
...collectDiscordNumericIdWarnings({
|
|
334
|
+
hits: scanDiscordNumericIdEntries(cfg),
|
|
335
|
+
doctorFixCommand,
|
|
336
|
+
}),
|
|
337
|
+
],
|
|
338
|
+
collectMutableAllowlistWarnings: ({ cfg }) => collectDiscordMutableAllowlistWarnings(cfg),
|
|
339
|
+
repairConfig: ({ cfg, doctorFixCommand }) => maybeRepairDiscordNumericIds(cfg, doctorFixCommand),
|
|
340
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveChannelStreamingPreviewChunk } from "autobot/plugin-sdk/channel-streaming";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
import { resolveTextChunkLimit } from "autobot/plugin-sdk/reply-chunking";
|
|
4
|
+
import { resolveAccountEntry } from "autobot/plugin-sdk/routing";
|
|
5
|
+
import { normalizeAccountId } from "autobot/plugin-sdk/routing";
|
|
6
|
+
import { DISCORD_TEXT_CHUNK_LIMIT } from "./outbound-adapter.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200;
|
|
9
|
+
const DEFAULT_DISCORD_DRAFT_STREAM_MAX = 800;
|
|
10
|
+
|
|
11
|
+
export function resolveDiscordDraftStreamingChunking(
|
|
12
|
+
cfg: AutoBotConfig,
|
|
13
|
+
accountId?: string | null,
|
|
14
|
+
): {
|
|
15
|
+
minChars: number;
|
|
16
|
+
maxChars: number;
|
|
17
|
+
breakPreference: "paragraph" | "newline" | "sentence";
|
|
18
|
+
} {
|
|
19
|
+
const textLimit = resolveTextChunkLimit(cfg, "discord", accountId, {
|
|
20
|
+
fallbackLimit: DISCORD_TEXT_CHUNK_LIMIT,
|
|
21
|
+
});
|
|
22
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
23
|
+
const accountCfg = resolveAccountEntry(cfg?.channels?.discord?.accounts, normalizedAccountId);
|
|
24
|
+
const draftCfg =
|
|
25
|
+
resolveChannelStreamingPreviewChunk(accountCfg) ??
|
|
26
|
+
resolveChannelStreamingPreviewChunk(cfg?.channels?.discord);
|
|
27
|
+
|
|
28
|
+
const maxRequested = Math.max(
|
|
29
|
+
1,
|
|
30
|
+
Math.floor(draftCfg?.maxChars ?? DEFAULT_DISCORD_DRAFT_STREAM_MAX),
|
|
31
|
+
);
|
|
32
|
+
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
|
33
|
+
const minRequested = Math.max(
|
|
34
|
+
1,
|
|
35
|
+
Math.floor(draftCfg?.minChars ?? DEFAULT_DISCORD_DRAFT_STREAM_MIN),
|
|
36
|
+
);
|
|
37
|
+
const minChars = Math.min(minRequested, maxChars);
|
|
38
|
+
const breakPreference =
|
|
39
|
+
draftCfg?.breakPreference === "newline" || draftCfg?.breakPreference === "sentence"
|
|
40
|
+
? draftCfg.breakPreference
|
|
41
|
+
: "paragraph";
|
|
42
|
+
return { minChars, maxChars, breakPreference };
|
|
43
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createFinalizableDraftLifecycle } from "autobot/plugin-sdk/channel-lifecycle";
|
|
2
|
+
import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
|
|
3
|
+
import {
|
|
4
|
+
createChannelMessage,
|
|
5
|
+
deleteChannelMessage,
|
|
6
|
+
editChannelMessage,
|
|
7
|
+
type RequestClient,
|
|
8
|
+
} from "./internal/discord.js";
|
|
9
|
+
import { resolveDiscordMessageFlags } from "./send.shared.js";
|
|
10
|
+
|
|
11
|
+
/** Discord messages cap at 2000 characters. */
|
|
12
|
+
const DISCORD_STREAM_MAX_CHARS = 2000;
|
|
13
|
+
const DEFAULT_THROTTLE_MS = 1200;
|
|
14
|
+
const DISCORD_PREVIEW_ALLOWED_MENTIONS = { parse: [] };
|
|
15
|
+
|
|
16
|
+
type DiscordDraftStream = {
|
|
17
|
+
update: (text: string) => void;
|
|
18
|
+
flush: () => Promise<void>;
|
|
19
|
+
messageId: () => string | undefined;
|
|
20
|
+
clear: () => Promise<void>;
|
|
21
|
+
discardPending: () => Promise<void>;
|
|
22
|
+
seal: () => Promise<void>;
|
|
23
|
+
stop: () => Promise<void>;
|
|
24
|
+
/** Reset internal state so the next update creates a new message instead of editing. */
|
|
25
|
+
forceNewMessage: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function createDiscordDraftStream(params: {
|
|
29
|
+
rest: RequestClient;
|
|
30
|
+
channelId: string;
|
|
31
|
+
maxChars?: number;
|
|
32
|
+
replyToMessageId?: string | (() => string | undefined);
|
|
33
|
+
throttleMs?: number;
|
|
34
|
+
/** Minimum chars before sending first message (debounce for push notifications) */
|
|
35
|
+
minInitialChars?: number;
|
|
36
|
+
suppressEmbeds?: boolean;
|
|
37
|
+
log?: (message: string) => void;
|
|
38
|
+
warn?: (message: string) => void;
|
|
39
|
+
}): DiscordDraftStream {
|
|
40
|
+
const maxChars = Math.min(params.maxChars ?? DISCORD_STREAM_MAX_CHARS, DISCORD_STREAM_MAX_CHARS);
|
|
41
|
+
const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
|
|
42
|
+
const minInitialChars = params.minInitialChars;
|
|
43
|
+
const channelId = params.channelId;
|
|
44
|
+
const rest = params.rest;
|
|
45
|
+
const flags = resolveDiscordMessageFlags({ suppressEmbeds: params.suppressEmbeds });
|
|
46
|
+
const resolveReplyToMessageId = () =>
|
|
47
|
+
typeof params.replyToMessageId === "function"
|
|
48
|
+
? params.replyToMessageId()
|
|
49
|
+
: params.replyToMessageId;
|
|
50
|
+
|
|
51
|
+
const streamState = { stopped: false, final: false };
|
|
52
|
+
let streamMessageId: string | undefined;
|
|
53
|
+
let lastSentText = "";
|
|
54
|
+
|
|
55
|
+
const sendOrEditStreamMessage = async (text: string): Promise<boolean> => {
|
|
56
|
+
// Allow final flush even if stopped (e.g., after clear()).
|
|
57
|
+
if (streamState.stopped && !streamState.final) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const trimmed = text.trimEnd();
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (trimmed.length > maxChars) {
|
|
65
|
+
// Discord messages cap at 2000 chars.
|
|
66
|
+
// Stop streaming once we exceed the cap to avoid repeated API failures.
|
|
67
|
+
streamState.stopped = true;
|
|
68
|
+
params.warn?.(`discord stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (trimmed === lastSentText) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Debounce first preview send for better push notification quality.
|
|
76
|
+
if (streamMessageId === undefined && minInitialChars != null && !streamState.final) {
|
|
77
|
+
if (trimmed.length < minInitialChars) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lastSentText = trimmed;
|
|
83
|
+
try {
|
|
84
|
+
if (streamMessageId !== undefined) {
|
|
85
|
+
// Edit existing message
|
|
86
|
+
await editChannelMessage(rest, channelId, streamMessageId, {
|
|
87
|
+
body: {
|
|
88
|
+
content: trimmed,
|
|
89
|
+
allowed_mentions: DISCORD_PREVIEW_ALLOWED_MENTIONS,
|
|
90
|
+
...(flags ? { flags } : {}),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// Send new message
|
|
96
|
+
const replyToMessageId = resolveReplyToMessageId()?.trim();
|
|
97
|
+
const messageReference = replyToMessageId
|
|
98
|
+
? { message_id: replyToMessageId, fail_if_not_exists: false }
|
|
99
|
+
: undefined;
|
|
100
|
+
const sent = await createChannelMessage<{ id?: string }>(rest, channelId, {
|
|
101
|
+
body: {
|
|
102
|
+
content: trimmed,
|
|
103
|
+
allowed_mentions: DISCORD_PREVIEW_ALLOWED_MENTIONS,
|
|
104
|
+
...(flags ? { flags } : {}),
|
|
105
|
+
...(messageReference ? { message_reference: messageReference } : {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const sentMessageId = sent?.id;
|
|
109
|
+
if (typeof sentMessageId !== "string" || !sentMessageId) {
|
|
110
|
+
streamState.stopped = true;
|
|
111
|
+
params.warn?.("discord stream preview stopped (missing message id from send)");
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
streamMessageId = sentMessageId;
|
|
115
|
+
return true;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
streamState.stopped = true;
|
|
118
|
+
params.warn?.(`discord stream preview failed: ${formatErrorMessage(err)}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const readMessageId = () => streamMessageId;
|
|
124
|
+
const clearMessageId = () => {
|
|
125
|
+
streamMessageId = undefined;
|
|
126
|
+
};
|
|
127
|
+
const isValidStreamMessageId = (value: unknown): value is string => typeof value === "string";
|
|
128
|
+
const deleteStreamMessage = async (messageId: string) => {
|
|
129
|
+
await deleteChannelMessage(rest, channelId, messageId);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const { loop, update, stop, clear, discardPending, seal } = createFinalizableDraftLifecycle({
|
|
133
|
+
throttleMs,
|
|
134
|
+
state: streamState,
|
|
135
|
+
sendOrEditStreamMessage,
|
|
136
|
+
readMessageId,
|
|
137
|
+
clearMessageId,
|
|
138
|
+
isValidMessageId: isValidStreamMessageId,
|
|
139
|
+
deleteMessage: deleteStreamMessage,
|
|
140
|
+
warn: params.warn,
|
|
141
|
+
warnPrefix: "discord stream preview cleanup failed",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const forceNewMessage = () => {
|
|
145
|
+
streamMessageId = undefined;
|
|
146
|
+
lastSentText = "";
|
|
147
|
+
loop.resetPending();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
params.log?.(`discord stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
update,
|
|
154
|
+
flush: loop.flush,
|
|
155
|
+
messageId: () => streamMessageId,
|
|
156
|
+
clear,
|
|
157
|
+
discardPending,
|
|
158
|
+
seal,
|
|
159
|
+
stop,
|
|
160
|
+
forceNewMessage,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const DISCORD_RESPONSE_BODY_SUMMARY_MAX_CHARS = 240;
|
|
2
|
+
|
|
3
|
+
export function summarizeDiscordResponseBody(
|
|
4
|
+
body: string,
|
|
5
|
+
opts: { emptyText?: string } = {},
|
|
6
|
+
): string | undefined {
|
|
7
|
+
const summary = body
|
|
8
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
9
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
|
|
10
|
+
.replace(/<[^>]+>/g, " ")
|
|
11
|
+
.replace(/ /gi, " ")
|
|
12
|
+
.replace(/&/gi, "&")
|
|
13
|
+
.replace(/</gi, "<")
|
|
14
|
+
.replace(/>/gi, ">")
|
|
15
|
+
.replace(/\s+/g, " ")
|
|
16
|
+
.trim();
|
|
17
|
+
if (!summary) {
|
|
18
|
+
return opts.emptyText;
|
|
19
|
+
}
|
|
20
|
+
return summary.slice(0, DISCORD_RESPONSE_BODY_SUMMARY_MAX_CHARS);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isDiscordHtmlResponseBody(body: string, contentType?: string | null): boolean {
|
|
24
|
+
return (
|
|
25
|
+
/\bhtml\b/i.test(contentType ?? "") ||
|
|
26
|
+
/^\s*<!doctype\s+html\b/i.test(body) ||
|
|
27
|
+
/^\s*<html\b/i.test(body)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isDiscordRateLimitResponseBody(body: string): boolean {
|
|
32
|
+
const normalized = body.toLowerCase();
|
|
33
|
+
return (
|
|
34
|
+
normalized.includes("error 1015") ||
|
|
35
|
+
normalized.includes("cloudflare") ||
|
|
36
|
+
normalized.includes("rate limit")
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ChannelOutboundPayloadHint } from "autobot/plugin-sdk/channel-contract";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
import type { DiscordExecApprovalConfig } from "autobot/plugin-sdk/config-contracts";
|
|
4
|
+
import type { ReplyPayload } from "autobot/plugin-sdk/reply-dispatch-runtime";
|
|
5
|
+
import { resolveDiscordAccount } from "./accounts.js";
|
|
6
|
+
import {
|
|
7
|
+
getExecApprovalReplyMetadata,
|
|
8
|
+
isChannelExecApprovalClientEnabledFromConfig,
|
|
9
|
+
matchesApprovalRequestFilters,
|
|
10
|
+
resolveApprovalApprovers,
|
|
11
|
+
} from "./approval-runtime.js";
|
|
12
|
+
import { parseDiscordTarget } from "./target-parsing.js";
|
|
13
|
+
|
|
14
|
+
function normalizeDiscordApproverId(value: string): string | undefined {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (/^\d+$/.test(trimmed)) {
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const target = parseDiscordTarget(trimmed);
|
|
24
|
+
return target?.kind === "user" ? target.id : undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveDiscordOwnerApprovers(cfg: AutoBotConfig): string[] {
|
|
31
|
+
const ownerAllowFrom = cfg.commands?.ownerAllowFrom;
|
|
32
|
+
if (!Array.isArray(ownerAllowFrom) || ownerAllowFrom.length === 0) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return resolveApprovalApprovers({
|
|
36
|
+
explicit: ownerAllowFrom,
|
|
37
|
+
normalizeApprover: (value) => normalizeDiscordApproverId(String(value)),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getDiscordExecApprovalApprovers(params: {
|
|
42
|
+
cfg: AutoBotConfig;
|
|
43
|
+
accountId?: string | null;
|
|
44
|
+
configOverride?: DiscordExecApprovalConfig | null;
|
|
45
|
+
}): string[] {
|
|
46
|
+
return resolveApprovalApprovers({
|
|
47
|
+
explicit:
|
|
48
|
+
params.configOverride?.approvers ??
|
|
49
|
+
resolveDiscordAccount(params).config.execApprovals?.approvers ??
|
|
50
|
+
resolveDiscordOwnerApprovers(params.cfg),
|
|
51
|
+
normalizeApprover: (value) => normalizeDiscordApproverId(String(value)),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isDiscordExecApprovalClientEnabled(params: {
|
|
56
|
+
cfg: AutoBotConfig;
|
|
57
|
+
accountId?: string | null;
|
|
58
|
+
configOverride?: DiscordExecApprovalConfig | null;
|
|
59
|
+
}): boolean {
|
|
60
|
+
const config = params.configOverride ?? resolveDiscordAccount(params).config.execApprovals;
|
|
61
|
+
return isChannelExecApprovalClientEnabledFromConfig({
|
|
62
|
+
enabled: config?.enabled,
|
|
63
|
+
approverCount: getDiscordExecApprovalApprovers({
|
|
64
|
+
cfg: params.cfg,
|
|
65
|
+
accountId: params.accountId,
|
|
66
|
+
configOverride: params.configOverride,
|
|
67
|
+
}).length,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isDiscordExecApprovalApprover(params: {
|
|
72
|
+
cfg: AutoBotConfig;
|
|
73
|
+
accountId?: string | null;
|
|
74
|
+
senderId?: string | null;
|
|
75
|
+
configOverride?: DiscordExecApprovalConfig | null;
|
|
76
|
+
}): boolean {
|
|
77
|
+
const senderId = params.senderId?.trim();
|
|
78
|
+
if (!senderId) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return getDiscordExecApprovalApprovers({
|
|
82
|
+
cfg: params.cfg,
|
|
83
|
+
accountId: params.accountId,
|
|
84
|
+
configOverride: params.configOverride,
|
|
85
|
+
}).includes(senderId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function shouldSuppressLocalDiscordExecApprovalPrompt(params: {
|
|
89
|
+
cfg: AutoBotConfig;
|
|
90
|
+
accountId?: string | null;
|
|
91
|
+
payload: ReplyPayload;
|
|
92
|
+
hint?: ChannelOutboundPayloadHint;
|
|
93
|
+
}): boolean {
|
|
94
|
+
const metadata = getExecApprovalReplyMetadata(params.payload);
|
|
95
|
+
const config = resolveDiscordAccount(params).config.execApprovals;
|
|
96
|
+
return (
|
|
97
|
+
params.hint?.kind === "approval-pending" &&
|
|
98
|
+
params.hint.nativeRouteActive === true &&
|
|
99
|
+
isDiscordExecApprovalClientEnabled(params) &&
|
|
100
|
+
metadata !== null &&
|
|
101
|
+
matchesApprovalRequestFilters({
|
|
102
|
+
request: {
|
|
103
|
+
agentId: metadata.agentId,
|
|
104
|
+
sessionKey: metadata.sessionKey,
|
|
105
|
+
},
|
|
106
|
+
agentFilter: config?.agentFilter,
|
|
107
|
+
sessionFilter: config?.sessionFilter,
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|