@open-mercato/channel-discord 0.6.8-develop.7090.1.4dbca7b350
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/.turbo/turbo-build.log +2 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js +86 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js +39 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js +97 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js +65 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js +110 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js +103 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js +93 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js +52 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js +46 -0
- package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js +16 -0
- package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/meta.js +10 -0
- package/dist/modules/channel_discord/__integration__/meta.js.map +7 -0
- package/dist/modules/channel_discord/acl.js +15 -0
- package/dist/modules/channel_discord/acl.js.map +7 -0
- package/dist/modules/channel_discord/ai-agents.js +97 -0
- package/dist/modules/channel_discord/ai-agents.js.map +7 -0
- package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js +73 -0
- package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js.map +7 -0
- package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js +222 -0
- package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js.map +7 -0
- package/dist/modules/channel_discord/api/interactions/route.js +173 -0
- package/dist/modules/channel_discord/api/interactions/route.js.map +7 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js +196 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js.map +7 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js +14 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js.map +7 -0
- package/dist/modules/channel_discord/cli.js +142 -0
- package/dist/modules/channel_discord/cli.js.map +7 -0
- package/dist/modules/channel_discord/commands/ai-reply-proposal.js +116 -0
- package/dist/modules/channel_discord/commands/ai-reply-proposal.js.map +7 -0
- package/dist/modules/channel_discord/commands/update-ai-auto-reply.js +104 -0
- package/dist/modules/channel_discord/commands/update-ai-auto-reply.js.map +7 -0
- package/dist/modules/channel_discord/di.js +23 -0
- package/dist/modules/channel_discord/di.js.map +7 -0
- package/dist/modules/channel_discord/index.js +9 -0
- package/dist/modules/channel_discord/index.js.map +7 -0
- package/dist/modules/channel_discord/integration.js +96 -0
- package/dist/modules/channel_discord/integration.js.map +7 -0
- package/dist/modules/channel_discord/lib/adapter.js +248 -0
- package/dist/modules/channel_discord/lib/adapter.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-agent-directory.js +45 -0
- package/dist/modules/channel_discord/lib/ai-agent-directory.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-features.js +9 -0
- package/dist/modules/channel_discord/lib/ai-features.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-proposal-contract.js +9 -0
- package/dist/modules/channel_discord/lib/ai-proposal-contract.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-proposal.js +109 -0
- package/dist/modules/channel_discord/lib/ai-proposal.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-reply.js +96 -0
- package/dist/modules/channel_discord/lib/ai-reply.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-service-principal.js +70 -0
- package/dist/modules/channel_discord/lib/ai-service-principal.js.map +7 -0
- package/dist/modules/channel_discord/lib/capabilities.js +42 -0
- package/dist/modules/channel_discord/lib/capabilities.js.map +7 -0
- package/dist/modules/channel_discord/lib/channel-access.js +62 -0
- package/dist/modules/channel_discord/lib/channel-access.js.map +7 -0
- package/dist/modules/channel_discord/lib/channel-state-store.js +103 -0
- package/dist/modules/channel_discord/lib/channel-state-store.js.map +7 -0
- package/dist/modules/channel_discord/lib/convert-outbound.js +38 -0
- package/dist/modules/channel_discord/lib/convert-outbound.js.map +7 -0
- package/dist/modules/channel_discord/lib/credentials.js +61 -0
- package/dist/modules/channel_discord/lib/credentials.js.map +7 -0
- package/dist/modules/channel_discord/lib/discord-gateway-client.js +289 -0
- package/dist/modules/channel_discord/lib/discord-gateway-client.js.map +7 -0
- package/dist/modules/channel_discord/lib/discord-rest.js +203 -0
- package/dist/modules/channel_discord/lib/discord-rest.js.map +7 -0
- package/dist/modules/channel_discord/lib/failure-reason.js +30 -0
- package/dist/modules/channel_discord/lib/failure-reason.js.map +7 -0
- package/dist/modules/channel_discord/lib/gateway-bridge.js +42 -0
- package/dist/modules/channel_discord/lib/gateway-bridge.js.map +7 -0
- package/dist/modules/channel_discord/lib/health.js +37 -0
- package/dist/modules/channel_discord/lib/health.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-dispatch.js +164 -0
- package/dist/modules/channel_discord/lib/interactions-dispatch.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-handler.js +115 -0
- package/dist/modules/channel_discord/lib/interactions-handler.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-queue.js +17 -0
- package/dist/modules/channel_discord/lib/interactions-queue.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-verify.js +84 -0
- package/dist/modules/channel_discord/lib/interactions-verify.js.map +7 -0
- package/dist/modules/channel_discord/lib/normalize-inbound.js +59 -0
- package/dist/modules/channel_discord/lib/normalize-inbound.js.map +7 -0
- package/dist/modules/channel_discord/lib/preset.js +63 -0
- package/dist/modules/channel_discord/lib/preset.js.map +7 -0
- package/dist/modules/channel_discord/lib/queue-strategy.js +12 -0
- package/dist/modules/channel_discord/lib/queue-strategy.js.map +7 -0
- package/dist/modules/channel_discord/lib/slash-commands.js +42 -0
- package/dist/modules/channel_discord/lib/slash-commands.js.map +7 -0
- package/dist/modules/channel_discord/message-types.js +64 -0
- package/dist/modules/channel_discord/message-types.js.map +7 -0
- package/dist/modules/channel_discord/setup.js +62 -0
- package/dist/modules/channel_discord/setup.js.map +7 -0
- package/dist/modules/channel_discord/subscribers/ai-auto-reply.js +185 -0
- package/dist/modules/channel_discord/subscribers/ai-auto-reply.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js +79 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js +20 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js +220 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.js +17 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection-table.js +26 -0
- package/dist/modules/channel_discord/widgets/injection-table.js.map +7 -0
- package/dist/modules/channel_discord/workers/discord-gateway.js +270 -0
- package/dist/modules/channel_discord/workers/discord-gateway.js.map +7 -0
- package/dist/modules/channel_discord/workers/discord-interactions.js +113 -0
- package/dist/modules/channel_discord/workers/discord-interactions.js.map +7 -0
- package/jest.config.cjs +38 -0
- package/package.json +104 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.ts +109 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.ts +58 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.ts +175 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.ts +83 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.ts +156 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.ts +144 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.ts +119 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.ts +75 -0
- package/src/modules/channel_discord/__integration__/helpers/discordSignature.ts +77 -0
- package/src/modules/channel_discord/__integration__/helpers/eventsQueue.ts +26 -0
- package/src/modules/channel_discord/__integration__/meta.ts +6 -0
- package/src/modules/channel_discord/__tests__/ai-auto-reply.policy.integration.test.ts +286 -0
- package/src/modules/channel_discord/acl.ts +19 -0
- package/src/modules/channel_discord/ai-agents.ts +157 -0
- package/src/modules/channel_discord/api/ai-auto-reply/channels/__tests__/route.test.ts +175 -0
- package/src/modules/channel_discord/api/ai-auto-reply/channels/route.ts +99 -0
- package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/__tests__/route.test.ts +311 -0
- package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.ts +299 -0
- package/src/modules/channel_discord/api/interactions/route.ts +242 -0
- package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.ts +18 -0
- package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.tsx +288 -0
- package/src/modules/channel_discord/cli.ts +234 -0
- package/src/modules/channel_discord/commands/__tests__/ai-reply-proposal.test.ts +155 -0
- package/src/modules/channel_discord/commands/__tests__/update-ai-auto-reply.test.ts +185 -0
- package/src/modules/channel_discord/commands/ai-reply-proposal.ts +162 -0
- package/src/modules/channel_discord/commands/update-ai-auto-reply.ts +157 -0
- package/src/modules/channel_discord/di.ts +26 -0
- package/src/modules/channel_discord/i18n/de.json +57 -0
- package/src/modules/channel_discord/i18n/en.json +57 -0
- package/src/modules/channel_discord/i18n/es.json +57 -0
- package/src/modules/channel_discord/i18n/ko.json +57 -0
- package/src/modules/channel_discord/i18n/pl.json +57 -0
- package/src/modules/channel_discord/index.ts +6 -0
- package/src/modules/channel_discord/integration.ts +98 -0
- package/src/modules/channel_discord/lib/__tests__/adapter.test.ts +206 -0
- package/src/modules/channel_discord/lib/__tests__/ai-agent-directory.test.ts +61 -0
- package/src/modules/channel_discord/lib/__tests__/ai-reply.test.ts +115 -0
- package/src/modules/channel_discord/lib/__tests__/capabilities.test.ts +170 -0
- package/src/modules/channel_discord/lib/__tests__/channel-quarantine.test.ts +89 -0
- package/src/modules/channel_discord/lib/__tests__/channel-state-store.auto-reply-marker.test.ts +145 -0
- package/src/modules/channel_discord/lib/__tests__/channel-state-store.test.ts +78 -0
- package/src/modules/channel_discord/lib/__tests__/convert-outbound.test.ts +39 -0
- package/src/modules/channel_discord/lib/__tests__/discord-gateway-client.test.ts +243 -0
- package/src/modules/channel_discord/lib/__tests__/failure-reason.test.ts +68 -0
- package/src/modules/channel_discord/lib/__tests__/gateway-bridge.test.ts +62 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-dispatch.test.ts +223 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-handler.test.ts +469 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-verify.test.ts +126 -0
- package/src/modules/channel_discord/lib/__tests__/normalize-inbound.identity.test.ts +64 -0
- package/src/modules/channel_discord/lib/__tests__/normalize-inbound.test.ts +67 -0
- package/src/modules/channel_discord/lib/__tests__/preset.test.ts +110 -0
- package/src/modules/channel_discord/lib/__tests__/queue-strategy.test.ts +33 -0
- package/src/modules/channel_discord/lib/adapter.ts +359 -0
- package/src/modules/channel_discord/lib/ai-agent-directory.ts +124 -0
- package/src/modules/channel_discord/lib/ai-features.ts +23 -0
- package/src/modules/channel_discord/lib/ai-proposal-contract.ts +24 -0
- package/src/modules/channel_discord/lib/ai-proposal.ts +179 -0
- package/src/modules/channel_discord/lib/ai-reply.ts +179 -0
- package/src/modules/channel_discord/lib/ai-service-principal.ts +143 -0
- package/src/modules/channel_discord/lib/capabilities.ts +104 -0
- package/src/modules/channel_discord/lib/channel-access.ts +110 -0
- package/src/modules/channel_discord/lib/channel-state-store.ts +217 -0
- package/src/modules/channel_discord/lib/convert-outbound.ts +88 -0
- package/src/modules/channel_discord/lib/credentials.ts +88 -0
- package/src/modules/channel_discord/lib/discord-gateway-client.ts +432 -0
- package/src/modules/channel_discord/lib/discord-rest.ts +393 -0
- package/src/modules/channel_discord/lib/failure-reason.ts +49 -0
- package/src/modules/channel_discord/lib/gateway-bridge.ts +79 -0
- package/src/modules/channel_discord/lib/health.ts +55 -0
- package/src/modules/channel_discord/lib/interactions-dispatch.ts +288 -0
- package/src/modules/channel_discord/lib/interactions-handler.ts +267 -0
- package/src/modules/channel_discord/lib/interactions-queue.ts +52 -0
- package/src/modules/channel_discord/lib/interactions-verify.ts +174 -0
- package/src/modules/channel_discord/lib/normalize-inbound.ts +89 -0
- package/src/modules/channel_discord/lib/preset.ts +109 -0
- package/src/modules/channel_discord/lib/queue-strategy.ts +29 -0
- package/src/modules/channel_discord/lib/slash-commands.ts +57 -0
- package/src/modules/channel_discord/message-types.ts +75 -0
- package/src/modules/channel_discord/setup.ts +78 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.armed.test.ts +149 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.failure-marker.test.ts +133 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.test.ts +273 -0
- package/src/modules/channel_discord/subscribers/ai-auto-reply.ts +325 -0
- package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.tsx +143 -0
- package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.ts +19 -0
- package/src/modules/channel_discord/widgets/injection/connect/widget.client.tsx +237 -0
- package/src/modules/channel_discord/widgets/injection/connect/widget.ts +16 -0
- package/src/modules/channel_discord/widgets/injection-table.ts +24 -0
- package/src/modules/channel_discord/workers/__tests__/discord-gateway.single-identify.test.ts +88 -0
- package/src/modules/channel_discord/workers/__tests__/discord-gateway.test.ts +179 -0
- package/src/modules/channel_discord/workers/__tests__/discord-interactions.test.ts +152 -0
- package/src/modules/channel_discord/workers/discord-gateway.ts +458 -0
- package/src/modules/channel_discord/workers/discord-interactions.ts +162 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
6
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
7
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle
|
|
15
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
16
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
17
|
+
import { Label } from "@open-mercato/ui/primitives/label";
|
|
18
|
+
import { PasswordInput } from "@open-mercato/ui/primitives/password-input";
|
|
19
|
+
const INITIAL_FORM = {
|
|
20
|
+
displayName: "",
|
|
21
|
+
botToken: "",
|
|
22
|
+
applicationId: "",
|
|
23
|
+
publicKey: "",
|
|
24
|
+
guildId: "",
|
|
25
|
+
defaultChannelId: ""
|
|
26
|
+
};
|
|
27
|
+
function ConnectDiscordWidget({
|
|
28
|
+
context
|
|
29
|
+
}) {
|
|
30
|
+
const t = useT();
|
|
31
|
+
const widgetContext = context;
|
|
32
|
+
const [open, setOpen] = React.useState(false);
|
|
33
|
+
const [pending, setPending] = React.useState(false);
|
|
34
|
+
const [form, setForm] = React.useState(INITIAL_FORM);
|
|
35
|
+
const [fieldErrors, setFieldErrors] = React.useState({});
|
|
36
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
37
|
+
contextId: "channel-discord-connect",
|
|
38
|
+
blockedMessage: t("channel_discord.connect.blocked", "Connection blocked by validation")
|
|
39
|
+
});
|
|
40
|
+
const mutationContext = React.useMemo(
|
|
41
|
+
() => ({ providerKey: "discord", retryLastMutation }),
|
|
42
|
+
[retryLastMutation]
|
|
43
|
+
);
|
|
44
|
+
const update = React.useCallback(
|
|
45
|
+
(key, value) => {
|
|
46
|
+
setForm((current) => ({ ...current, [key]: value }));
|
|
47
|
+
setFieldErrors((current) => {
|
|
48
|
+
if (!current[key]) return current;
|
|
49
|
+
const next = { ...current };
|
|
50
|
+
delete next[key];
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
[]
|
|
55
|
+
);
|
|
56
|
+
const submit = React.useCallback(async () => {
|
|
57
|
+
if (pending) return;
|
|
58
|
+
setPending(true);
|
|
59
|
+
setFieldErrors({});
|
|
60
|
+
try {
|
|
61
|
+
const response = await runMutation({
|
|
62
|
+
context: mutationContext,
|
|
63
|
+
mutationPayload: { providerKey: "discord", displayName: form.displayName },
|
|
64
|
+
operation: () => apiCall("/api/communication_channels/channels/connect/credentials", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
providerKey: "discord",
|
|
69
|
+
displayName: form.displayName.trim() || "Discord",
|
|
70
|
+
credentials: {
|
|
71
|
+
botToken: form.botToken.trim(),
|
|
72
|
+
applicationId: form.applicationId.trim(),
|
|
73
|
+
publicKey: form.publicKey.trim(),
|
|
74
|
+
guildId: form.guildId.trim() || void 0,
|
|
75
|
+
defaultChannelId: form.defaultChannelId.trim() || void 0
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
const body = response.result;
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
setFieldErrors(body?.fieldErrors ?? {});
|
|
83
|
+
flash(body?.error ?? t("channel_discord.connect.failed", "Could not connect Discord bot."), "error");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
flash(t("channel_discord.connect.connected", "Discord channel connected."), "success");
|
|
87
|
+
setOpen(false);
|
|
88
|
+
setForm(INITIAL_FORM);
|
|
89
|
+
widgetContext?.reload?.();
|
|
90
|
+
} finally {
|
|
91
|
+
setPending(false);
|
|
92
|
+
}
|
|
93
|
+
}, [form, mutationContext, pending, runMutation, t, widgetContext]);
|
|
94
|
+
const onDialogKeyDown = React.useCallback(
|
|
95
|
+
(event) => {
|
|
96
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
void submit();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[submit]
|
|
102
|
+
);
|
|
103
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
104
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(true), children: t("channel_discord.connect.button", "Connect Discord") }),
|
|
105
|
+
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { onKeyDown: onDialogKeyDown, children: [
|
|
106
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("channel_discord.connect.title", "Connect Discord bot") }) }),
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 py-2", children: [
|
|
108
|
+
/* @__PURE__ */ jsx(
|
|
109
|
+
Field,
|
|
110
|
+
{
|
|
111
|
+
label: t("channel_discord.connect.fields.displayName", "Display name"),
|
|
112
|
+
error: fieldErrors.displayName,
|
|
113
|
+
children: /* @__PURE__ */ jsx(
|
|
114
|
+
Input,
|
|
115
|
+
{
|
|
116
|
+
value: form.displayName,
|
|
117
|
+
onChange: (event) => update("displayName", event.target.value),
|
|
118
|
+
"aria-invalid": Boolean(fieldErrors.displayName)
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
Field,
|
|
125
|
+
{
|
|
126
|
+
label: t("channel_discord.connect.fields.botToken", "Bot token"),
|
|
127
|
+
error: fieldErrors.botToken,
|
|
128
|
+
children: /* @__PURE__ */ jsx(
|
|
129
|
+
PasswordInput,
|
|
130
|
+
{
|
|
131
|
+
value: form.botToken,
|
|
132
|
+
onChange: (event) => update("botToken", event.target.value),
|
|
133
|
+
"aria-invalid": Boolean(fieldErrors.botToken)
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
Field,
|
|
141
|
+
{
|
|
142
|
+
label: t("channel_discord.connect.fields.applicationId", "Application ID"),
|
|
143
|
+
error: fieldErrors.applicationId,
|
|
144
|
+
children: /* @__PURE__ */ jsx(
|
|
145
|
+
Input,
|
|
146
|
+
{
|
|
147
|
+
value: form.applicationId,
|
|
148
|
+
onChange: (event) => update("applicationId", event.target.value),
|
|
149
|
+
"aria-invalid": Boolean(fieldErrors.applicationId)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
Field,
|
|
156
|
+
{
|
|
157
|
+
label: t("channel_discord.connect.fields.publicKey", "Public key"),
|
|
158
|
+
error: fieldErrors.publicKey,
|
|
159
|
+
children: /* @__PURE__ */ jsx(
|
|
160
|
+
Input,
|
|
161
|
+
{
|
|
162
|
+
value: form.publicKey,
|
|
163
|
+
onChange: (event) => update("publicKey", event.target.value),
|
|
164
|
+
"aria-invalid": Boolean(fieldErrors.publicKey)
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
] }),
|
|
170
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
171
|
+
/* @__PURE__ */ jsx(
|
|
172
|
+
Field,
|
|
173
|
+
{
|
|
174
|
+
label: t("channel_discord.connect.fields.guildId", "Guild (server) ID"),
|
|
175
|
+
error: fieldErrors.guildId,
|
|
176
|
+
children: /* @__PURE__ */ jsx(
|
|
177
|
+
Input,
|
|
178
|
+
{
|
|
179
|
+
value: form.guildId,
|
|
180
|
+
onChange: (event) => update("guildId", event.target.value),
|
|
181
|
+
"aria-invalid": Boolean(fieldErrors.guildId)
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
/* @__PURE__ */ jsx(
|
|
187
|
+
Field,
|
|
188
|
+
{
|
|
189
|
+
label: t("channel_discord.connect.fields.defaultChannelId", "Default channel ID"),
|
|
190
|
+
error: fieldErrors.defaultChannelId,
|
|
191
|
+
children: /* @__PURE__ */ jsx(
|
|
192
|
+
Input,
|
|
193
|
+
{
|
|
194
|
+
value: form.defaultChannelId,
|
|
195
|
+
onChange: (event) => update("defaultChannelId", event.target.value),
|
|
196
|
+
"aria-invalid": Boolean(fieldErrors.defaultChannelId)
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
] })
|
|
202
|
+
] }),
|
|
203
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
204
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(false), disabled: pending, children: t("channel_discord.connect.cancel", "Cancel") }),
|
|
205
|
+
/* @__PURE__ */ jsx(Button, { type: "button", onClick: () => void submit(), disabled: pending, children: pending ? t("channel_discord.connect.connecting", "Connecting...") : t("channel_discord.connect.save", "Connect") })
|
|
206
|
+
] })
|
|
207
|
+
] }) })
|
|
208
|
+
] });
|
|
209
|
+
}
|
|
210
|
+
function Field(props) {
|
|
211
|
+
return /* @__PURE__ */ jsxs("label", { className: "grid gap-1.5", children: [
|
|
212
|
+
/* @__PURE__ */ jsx(Label, { asChild: true, children: /* @__PURE__ */ jsx("span", { children: props.label }) }),
|
|
213
|
+
props.children,
|
|
214
|
+
props.error ? /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive", children: props.error }) : null
|
|
215
|
+
] });
|
|
216
|
+
}
|
|
217
|
+
export {
|
|
218
|
+
ConnectDiscordWidget as default
|
|
219
|
+
};
|
|
220
|
+
//# sourceMappingURL=widget.client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/channel_discord/widgets/injection/connect/widget.client.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\n\ntype WidgetContext = Record<string, unknown> & {\n reload?: () => void\n}\n\ntype ConnectResponse = {\n channelId?: string\n error?: string\n fieldErrors?: Record<string, string>\n}\n\ntype FormState = {\n displayName: string\n botToken: string\n applicationId: string\n publicKey: string\n guildId: string\n defaultChannelId: string\n}\n\nconst INITIAL_FORM: FormState = {\n displayName: '',\n botToken: '',\n applicationId: '',\n publicKey: '',\n guildId: '',\n defaultChannelId: '',\n}\n\nexport default function ConnectDiscordWidget({\n context,\n}: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>) {\n const t = useT()\n const widgetContext = context as WidgetContext | undefined\n const [open, setOpen] = React.useState(false)\n const [pending, setPending] = React.useState(false)\n const [form, setForm] = React.useState<FormState>(INITIAL_FORM)\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const { runMutation, retryLastMutation } = useGuardedMutation({\n contextId: 'channel-discord-connect',\n blockedMessage: t('channel_discord.connect.blocked', 'Connection blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({ providerKey: 'discord', retryLastMutation }),\n [retryLastMutation],\n )\n\n const update = React.useCallback(\n <K extends keyof FormState>(key: K, value: FormState[K]) => {\n setForm((current) => ({ ...current, [key]: value }))\n setFieldErrors((current) => {\n if (!current[key]) return current\n const next = { ...current }\n delete next[key]\n return next\n })\n },\n [],\n )\n\n const submit = React.useCallback(async () => {\n if (pending) return\n setPending(true)\n setFieldErrors({})\n try {\n const response = await runMutation({\n context: mutationContext,\n mutationPayload: { providerKey: 'discord', displayName: form.displayName },\n operation: () =>\n apiCall<ConnectResponse>('/api/communication_channels/channels/connect/credentials', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n providerKey: 'discord',\n displayName: form.displayName.trim() || 'Discord',\n credentials: {\n botToken: form.botToken.trim(),\n applicationId: form.applicationId.trim(),\n publicKey: form.publicKey.trim(),\n guildId: form.guildId.trim() || undefined,\n defaultChannelId: form.defaultChannelId.trim() || undefined,\n },\n }),\n }),\n })\n const body = response.result as ConnectResponse | undefined\n if (!response.ok) {\n setFieldErrors(body?.fieldErrors ?? {})\n flash(body?.error ?? t('channel_discord.connect.failed', 'Could not connect Discord bot.'), 'error')\n return\n }\n flash(t('channel_discord.connect.connected', 'Discord channel connected.'), 'success')\n setOpen(false)\n setForm(INITIAL_FORM)\n widgetContext?.reload?.()\n } finally {\n setPending(false)\n }\n }, [form, mutationContext, pending, runMutation, t, widgetContext])\n\n const onDialogKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submit()\n }\n },\n [submit],\n )\n\n return (\n <>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(true)}>\n {t('channel_discord.connect.button', 'Connect Discord')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent onKeyDown={onDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>{t('channel_discord.connect.title', 'Connect Discord bot')}</DialogTitle>\n </DialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <Field\n label={t('channel_discord.connect.fields.displayName', 'Display name')}\n error={fieldErrors.displayName}\n >\n <Input\n value={form.displayName}\n onChange={(event) => update('displayName', event.target.value)}\n aria-invalid={Boolean(fieldErrors.displayName)}\n />\n </Field>\n\n <Field\n label={t('channel_discord.connect.fields.botToken', 'Bot token')}\n error={fieldErrors.botToken}\n >\n <PasswordInput\n value={form.botToken}\n onChange={(event) => update('botToken', event.target.value)}\n aria-invalid={Boolean(fieldErrors.botToken)}\n />\n </Field>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('channel_discord.connect.fields.applicationId', 'Application ID')}\n error={fieldErrors.applicationId}\n >\n <Input\n value={form.applicationId}\n onChange={(event) => update('applicationId', event.target.value)}\n aria-invalid={Boolean(fieldErrors.applicationId)}\n />\n </Field>\n <Field\n label={t('channel_discord.connect.fields.publicKey', 'Public key')}\n error={fieldErrors.publicKey}\n >\n <Input\n value={form.publicKey}\n onChange={(event) => update('publicKey', event.target.value)}\n aria-invalid={Boolean(fieldErrors.publicKey)}\n />\n </Field>\n </div>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('channel_discord.connect.fields.guildId', 'Guild (server) ID')}\n error={fieldErrors.guildId}\n >\n <Input\n value={form.guildId}\n onChange={(event) => update('guildId', event.target.value)}\n aria-invalid={Boolean(fieldErrors.guildId)}\n />\n </Field>\n <Field\n label={t('channel_discord.connect.fields.defaultChannelId', 'Default channel ID')}\n error={fieldErrors.defaultChannelId}\n >\n <Input\n value={form.defaultChannelId}\n onChange={(event) => update('defaultChannelId', event.target.value)}\n aria-invalid={Boolean(fieldErrors.defaultChannelId)}\n />\n </Field>\n </div>\n </div>\n\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(false)} disabled={pending}>\n {t('channel_discord.connect.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void submit()} disabled={pending}>\n {pending\n ? t('channel_discord.connect.connecting', 'Connecting...')\n : t('channel_discord.connect.save', 'Connect')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\nfunction Field(props: { label: string; error?: string; children: React.ReactNode }) {\n return (\n <label className=\"grid gap-1.5\">\n <Label asChild>\n <span>{props.label}</span>\n </Label>\n {props.children}\n {props.error ? <span className=\"text-xs text-destructive\">{props.error}</span> : null}\n </label>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ConnectDiscordWidget from "./widget.client.js";
|
|
2
|
+
const widget = {
|
|
3
|
+
metadata: {
|
|
4
|
+
id: "channel_discord.injection.connect",
|
|
5
|
+
title: "Connect Discord",
|
|
6
|
+
description: "Connects a Discord bot as a two-way communication channel.",
|
|
7
|
+
features: ["communication_channels.connect_user_channel"],
|
|
8
|
+
priority: 100,
|
|
9
|
+
enabled: true
|
|
10
|
+
},
|
|
11
|
+
Widget: ConnectDiscordWidget
|
|
12
|
+
};
|
|
13
|
+
var widget_default = widget;
|
|
14
|
+
export {
|
|
15
|
+
widget_default as default
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/channel_discord/widgets/injection/connect/widget.ts"],
|
|
4
|
+
"sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport ConnectDiscordWidget from './widget.client'\n\nconst widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {\n metadata: {\n id: 'channel_discord.injection.connect',\n title: 'Connect Discord',\n description: 'Connects a Discord bot as a two-way communication channel.',\n features: ['communication_channels.connect_user_channel'],\n priority: 100,\n enabled: true,\n },\n Widget: ConnectDiscordWidget,\n}\n\nexport default widget\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,0BAA0B;AAEjC,MAAM,SAAkF;AAAA,EACtF,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,6CAA6C;AAAA,IACxD,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA,QAAQ;AACV;AAEA,IAAO,iBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { channelDiscordDetailWidgetSpotId } from "../integration.js";
|
|
2
|
+
const injectionTable = {
|
|
3
|
+
"profile:communication-channels:connect": [
|
|
4
|
+
{
|
|
5
|
+
widgetId: "channel_discord.injection.connect",
|
|
6
|
+
priority: 120
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
// The Discord integration's own detail page — the only spot in the product
|
|
10
|
+
// that is Discord-scoped by construction, which is what a per-channel AI
|
|
11
|
+
// setting needs (see the widget's file header).
|
|
12
|
+
[channelDiscordDetailWidgetSpotId]: [
|
|
13
|
+
{
|
|
14
|
+
widgetId: "channel_discord.injection.ai-auto-reply",
|
|
15
|
+
priority: 40,
|
|
16
|
+
kind: "group",
|
|
17
|
+
groupLabel: "channel_discord.aiAutoReply.widget.title"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
var injection_table_default = injectionTable;
|
|
22
|
+
export {
|
|
23
|
+
injection_table_default as default,
|
|
24
|
+
injectionTable
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=injection-table.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_discord/widgets/injection-table.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\nimport { channelDiscordDetailWidgetSpotId } from '../integration'\n\nexport const injectionTable: ModuleInjectionTable = {\n 'profile:communication-channels:connect': [\n {\n widgetId: 'channel_discord.injection.connect',\n priority: 120,\n },\n ],\n // The Discord integration's own detail page \u2014 the only spot in the product\n // that is Discord-scoped by construction, which is what a per-channel AI\n // setting needs (see the widget's file header).\n [channelDiscordDetailWidgetSpotId]: [\n {\n widgetId: 'channel_discord.injection.ai-auto-reply',\n priority: 40,\n kind: 'group',\n groupLabel: 'channel_discord.aiAutoReply.widget.title',\n },\n ],\n}\n\nexport default injectionTable\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,wCAAwC;AAE1C,MAAM,iBAAuC;AAAA,EAClD,0CAA0C;AAAA,IACxC;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAIA,CAAC,gCAAgC,GAAG;AAAA,IAClC;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,MACV,MAAM;AAAA,MACN,YAAY;AAAA,IACd;AAAA,EACF;AACF;AAEA,IAAO,0BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
4
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
|
+
import { CommunicationChannel } from "@open-mercato/core/modules/communication_channels/data/entities";
|
|
6
|
+
import {
|
|
7
|
+
COMMUNICATION_CHANNELS_QUEUES,
|
|
8
|
+
getCommunicationChannelsQueue
|
|
9
|
+
} from "@open-mercato/core/modules/communication_channels/lib/queue";
|
|
10
|
+
import { emitCommunicationChannelsEvent } from "@open-mercato/core/modules/communication_channels/events";
|
|
11
|
+
import { parseDiscordCredentialsOrThrow, discordChannelStateSchema } from "../lib/credentials.js";
|
|
12
|
+
import {
|
|
13
|
+
getDiscordGatewayClient
|
|
14
|
+
} from "../lib/discord-gateway-client.js";
|
|
15
|
+
import {
|
|
16
|
+
persistDiscordChannelState,
|
|
17
|
+
quarantineDiscordChannel
|
|
18
|
+
} from "../lib/channel-state-store.js";
|
|
19
|
+
import { buildInboundMessageJob, buildReactionJob } from "../lib/gateway-bridge.js";
|
|
20
|
+
const logger = createLogger("channel_discord").child({ component: "gateway-worker" });
|
|
21
|
+
const CHANNEL_DISCORD_GATEWAY_QUEUE = "channel_discord_gateway";
|
|
22
|
+
const metadata = {
|
|
23
|
+
queue: CHANNEL_DISCORD_GATEWAY_QUEUE,
|
|
24
|
+
id: "channel_discord:gateway",
|
|
25
|
+
concurrency: 1
|
|
26
|
+
};
|
|
27
|
+
function botTokenFingerprint(botToken) {
|
|
28
|
+
return createHash("sha256").update(botToken).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
function findChannelWithSameBot(channelId, fingerprint, connections = activeConnections) {
|
|
31
|
+
for (const [otherId, entry] of connections) {
|
|
32
|
+
if (otherId === channelId) continue;
|
|
33
|
+
if (entry.botTokenFingerprint !== fingerprint) continue;
|
|
34
|
+
if (!isConnectionLive(entry)) continue;
|
|
35
|
+
return otherId;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const activeConnections = /* @__PURE__ */ new Map();
|
|
40
|
+
const pendingStateWrites = /* @__PURE__ */ new Map();
|
|
41
|
+
function isConnectionLive(entry) {
|
|
42
|
+
if (!entry) return false;
|
|
43
|
+
try {
|
|
44
|
+
return entry.handle.isActive();
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function buildGatewayChannelFilter(scope) {
|
|
50
|
+
const filter = { providerKey: "discord", isActive: true, deletedAt: null };
|
|
51
|
+
if (scope.tenantId) filter.tenantId = scope.tenantId;
|
|
52
|
+
if (scope.organizationId != null) filter.organizationId = scope.organizationId;
|
|
53
|
+
return filter;
|
|
54
|
+
}
|
|
55
|
+
function reconcileGatewayConnections(activeChannelIds, connections = activeConnections, scope = {}) {
|
|
56
|
+
const removed = [];
|
|
57
|
+
for (const [channelId, entry] of connections) {
|
|
58
|
+
if (activeChannelIds.has(channelId)) continue;
|
|
59
|
+
if (scope.tenantId && entry.tenantId !== scope.tenantId) continue;
|
|
60
|
+
if (scope.organizationId != null && entry.organizationId !== scope.organizationId) continue;
|
|
61
|
+
try {
|
|
62
|
+
entry.handle.close();
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
connections.delete(channelId);
|
|
66
|
+
removed.push(channelId);
|
|
67
|
+
}
|
|
68
|
+
return removed;
|
|
69
|
+
}
|
|
70
|
+
async function handle(job, ctx) {
|
|
71
|
+
if (parseBooleanWithDefault(process.env.OM_CHANNEL_DISCORD_GATEWAY_DISABLED, false)) {
|
|
72
|
+
logger.info("gateway disabled via OM_CHANNEL_DISCORD_GATEWAY_DISABLED \u2014 skipping connect");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const em = ctx.resolve("em").fork();
|
|
76
|
+
let credentialsService = null;
|
|
77
|
+
try {
|
|
78
|
+
credentialsService = ctx.resolve("integrationCredentialsService");
|
|
79
|
+
} catch {
|
|
80
|
+
credentialsService = null;
|
|
81
|
+
}
|
|
82
|
+
if (!credentialsService) {
|
|
83
|
+
logger.warn("integrationCredentialsService unavailable \u2014 cannot resolve bot tokens");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const scope = {
|
|
87
|
+
tenantId: job.payload?.tenantId,
|
|
88
|
+
organizationId: job.payload?.organizationId ?? null
|
|
89
|
+
};
|
|
90
|
+
const channels = await findWithDecryption(
|
|
91
|
+
em,
|
|
92
|
+
CommunicationChannel,
|
|
93
|
+
buildGatewayChannelFilter(scope)
|
|
94
|
+
);
|
|
95
|
+
let started = 0;
|
|
96
|
+
let kept = 0;
|
|
97
|
+
let quarantined = 0;
|
|
98
|
+
for (const channel of channels) {
|
|
99
|
+
if (isConnectionLive(activeConnections.get(channel.id))) {
|
|
100
|
+
kept += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (channel.status === "requires_reauth") {
|
|
104
|
+
quarantined += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
await startChannelConnection(channel, credentialsService, em);
|
|
108
|
+
started += 1;
|
|
109
|
+
}
|
|
110
|
+
const activeIds = new Set(channels.map((channel) => channel.id));
|
|
111
|
+
const removed = reconcileGatewayConnections(activeIds, activeConnections, scope);
|
|
112
|
+
if (removed.length > 0) {
|
|
113
|
+
logger.info("reconciled away stale discord gateway connections", { channelIds: removed });
|
|
114
|
+
}
|
|
115
|
+
logger.debug("discord gateway reconciliation finished", { started, kept, quarantined, removed: removed.length });
|
|
116
|
+
}
|
|
117
|
+
async function startChannelConnection(channel, credentialsService, em) {
|
|
118
|
+
const scope = {
|
|
119
|
+
channelId: channel.id,
|
|
120
|
+
channelType: channel.channelType,
|
|
121
|
+
tenantId: channel.tenantId,
|
|
122
|
+
organizationId: channel.organizationId ?? null
|
|
123
|
+
};
|
|
124
|
+
let credentials = null;
|
|
125
|
+
try {
|
|
126
|
+
credentials = await credentialsService.resolve("channel_discord", {
|
|
127
|
+
tenantId: channel.tenantId,
|
|
128
|
+
organizationId: channel.organizationId ?? channel.tenantId,
|
|
129
|
+
userId: channel.userId ?? null
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.warn("failed to resolve discord credentials for channel", { channelId: channel.id, err });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!credentials) return;
|
|
136
|
+
let botToken;
|
|
137
|
+
try {
|
|
138
|
+
botToken = parseDiscordCredentialsOrThrow(credentials).botToken;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.warn("invalid discord credentials for channel", { channelId: channel.id, err });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const fingerprint = botTokenFingerprint(botToken);
|
|
144
|
+
const duplicateOf = findChannelWithSameBot(channel.id, fingerprint);
|
|
145
|
+
if (duplicateOf) {
|
|
146
|
+
logger.warn("skipping discord gateway connect \u2014 another channel already serves this bot", {
|
|
147
|
+
channelId: channel.id,
|
|
148
|
+
servedBy: duplicateOf
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const channelState = discordChannelStateSchema.parse(channel.channelState ?? {});
|
|
153
|
+
const resumeState = {
|
|
154
|
+
sessionId: channelState.sessionId,
|
|
155
|
+
sequence: channelState.sequence ?? null,
|
|
156
|
+
resumeGatewayUrl: channelState.resumeGatewayUrl
|
|
157
|
+
};
|
|
158
|
+
const stale = activeConnections.get(channel.id);
|
|
159
|
+
if (stale) {
|
|
160
|
+
try {
|
|
161
|
+
stale.handle.close();
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
activeConnections.delete(channel.id);
|
|
165
|
+
}
|
|
166
|
+
const stateScope = {
|
|
167
|
+
tenantId: channel.tenantId,
|
|
168
|
+
organizationId: channel.organizationId ?? null
|
|
169
|
+
};
|
|
170
|
+
const inboundQueue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.inbound);
|
|
171
|
+
const reactionsQueue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.reactions);
|
|
172
|
+
let botUserId = channelState.botUserId;
|
|
173
|
+
const handle2 = getDiscordGatewayClient().connect({
|
|
174
|
+
botToken,
|
|
175
|
+
resumeState,
|
|
176
|
+
onMessage: async (message) => {
|
|
177
|
+
logger.debug("discord gateway received message", { channelId: channel.id, messageId: message.id });
|
|
178
|
+
const jobPayload = buildInboundMessageJob({ message, channel: scope, botUserId });
|
|
179
|
+
if (!jobPayload) {
|
|
180
|
+
logger.debug("discord inbound message dropped \u2014 authored by this bot", {
|
|
181
|
+
channelId: channel.id,
|
|
182
|
+
messageId: message.id
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await inboundQueue.enqueue(jobPayload);
|
|
187
|
+
logger.debug("discord inbound message enqueued", {
|
|
188
|
+
channelId: channel.id,
|
|
189
|
+
messageId: message.id,
|
|
190
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.inbound
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
onReaction: async (reaction, action) => {
|
|
194
|
+
const reactionJob = await buildReactionJob({ reaction, action, channel: scope, botUserId });
|
|
195
|
+
if (!reactionJob) return;
|
|
196
|
+
await reactionsQueue.enqueue(reactionJob);
|
|
197
|
+
},
|
|
198
|
+
onReady: async ({ botUserId: readyBotUserId, resumeState: freshResumeState }) => {
|
|
199
|
+
botUserId = readyBotUserId || botUserId;
|
|
200
|
+
await persistChannelState(em, channel.id, stateScope, { ...freshResumeState, botUserId });
|
|
201
|
+
logger.info("discord gateway ready", { channelId: channel.id });
|
|
202
|
+
},
|
|
203
|
+
onRequiresReauth: async ({ code }) => {
|
|
204
|
+
logger.warn("discord gateway fatal close \u2014 quarantining channel", { channelId: channel.id, code });
|
|
205
|
+
await quarantineChannel(em, channel.id, stateScope, `gateway_close_${code}`);
|
|
206
|
+
await emitCommunicationChannelsEvent(
|
|
207
|
+
"communication_channels.channel.requires_reauth",
|
|
208
|
+
{
|
|
209
|
+
channelId: channel.id,
|
|
210
|
+
providerKey: "discord",
|
|
211
|
+
channelType: channel.channelType,
|
|
212
|
+
reason: `gateway_close_${code}`,
|
|
213
|
+
tenantId: channel.tenantId,
|
|
214
|
+
organizationId: channel.organizationId ?? null
|
|
215
|
+
},
|
|
216
|
+
{ persistent: true }
|
|
217
|
+
);
|
|
218
|
+
activeConnections.delete(channel.id);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
activeConnections.set(channel.id, {
|
|
222
|
+
handle: handle2,
|
|
223
|
+
tenantId: channel.tenantId,
|
|
224
|
+
organizationId: channel.organizationId ?? null,
|
|
225
|
+
botTokenFingerprint: fingerprint
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async function persistChannelState(em, channelId, scope, patch) {
|
|
229
|
+
const previous = pendingStateWrites.get(channelId) ?? Promise.resolve();
|
|
230
|
+
const next = previous.catch(() => void 0).then(async () => {
|
|
231
|
+
try {
|
|
232
|
+
const result = await persistDiscordChannelState({ em, channelId, scope, patch });
|
|
233
|
+
if (result === "not_found") {
|
|
234
|
+
logger.warn("discord channel not found in scope while persisting resume state", { channelId });
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logger.warn("failed to persist discord gateway resume state", { channelId, err });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
pendingStateWrites.set(channelId, next);
|
|
241
|
+
await next;
|
|
242
|
+
if (pendingStateWrites.get(channelId) === next) pendingStateWrites.delete(channelId);
|
|
243
|
+
}
|
|
244
|
+
async function quarantineChannel(em, channelId, scope, reason) {
|
|
245
|
+
const previous = pendingStateWrites.get(channelId) ?? Promise.resolve();
|
|
246
|
+
const next = previous.catch(() => void 0).then(async () => {
|
|
247
|
+
try {
|
|
248
|
+
const result = await quarantineDiscordChannel({ em, channelId, scope, reason });
|
|
249
|
+
if (result === "not_found") {
|
|
250
|
+
logger.warn("discord channel not found in scope while quarantining", { channelId });
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger.warn("failed to quarantine discord channel after fatal close", { channelId, reason, err });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
pendingStateWrites.set(channelId, next);
|
|
257
|
+
await next;
|
|
258
|
+
if (pendingStateWrites.get(channelId) === next) pendingStateWrites.delete(channelId);
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
CHANNEL_DISCORD_GATEWAY_QUEUE,
|
|
262
|
+
botTokenFingerprint,
|
|
263
|
+
buildGatewayChannelFilter,
|
|
264
|
+
handle as default,
|
|
265
|
+
findChannelWithSameBot,
|
|
266
|
+
isConnectionLive,
|
|
267
|
+
metadata,
|
|
268
|
+
reconcileGatewayConnections
|
|
269
|
+
};
|
|
270
|
+
//# sourceMappingURL=discord-gateway.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_discord/workers/discord-gateway.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { CommunicationChannel } from '@open-mercato/core/modules/communication_channels/data/entities'\nimport {\n COMMUNICATION_CHANNELS_QUEUES,\n getCommunicationChannelsQueue,\n} from '@open-mercato/core/modules/communication_channels/lib/queue'\nimport { emitCommunicationChannelsEvent } from '@open-mercato/core/modules/communication_channels/events'\nimport { parseDiscordCredentialsOrThrow, discordChannelStateSchema } from '../lib/credentials'\nimport {\n getDiscordGatewayClient,\n type DiscordGatewayHandle,\n type GatewayResumeState,\n} from '../lib/discord-gateway-client'\nimport {\n persistDiscordChannelState,\n quarantineDiscordChannel,\n type DiscordChannelScope,\n type DiscordChannelStatePatch,\n} from '../lib/channel-state-store'\nimport { buildInboundMessageJob, buildReactionJob, type GatewayChannelScope } from '../lib/gateway-bridge'\n\nconst logger = createLogger('channel_discord').child({ component: 'gateway-worker' })\n\n/**\n * Long-running Discord Gateway bridge worker (SPEC 2026-06-19 \u00A7 Gateway worker).\n *\n * This is a *provider-owned* long-running worker \u2014 a novel pattern relative to\n * the email providers (which are poll/push-driven by the hub). A worker file\n * under `workers/` is auto-discovered, so shipping one from a provider package is\n * allowed by the framework. `concurrency: 1` enforces the single-identify-per-bot\n * discipline Discord requires.\n *\n * The job opens one Gateway WebSocket per active `discord` channel and bridges\n * `MESSAGE_CREATE` / reaction events into the hub's existing queues (inbound +\n * reactions) \u2014 the same jobs the webhook route enqueues, so the hub stays\n * unchanged. Events the bot authored are dropped (feedback-loop guard); the hub\n * dedups the rest.\n *\n * Set `OM_CHANNEL_DISCORD_GATEWAY_DISABLED=1` to skip opening sockets (CI /\n * send-only deployments).\n */\n/** Queue that the gateway bridge consumes. Shared with `cli.ts` (start-gateway). */\nexport const CHANNEL_DISCORD_GATEWAY_QUEUE = 'channel_discord_gateway'\n\nexport const metadata: WorkerMeta = {\n queue: CHANNEL_DISCORD_GATEWAY_QUEUE,\n id: 'channel_discord:gateway',\n concurrency: 1,\n}\n\ntype HandlerContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype GatewayJobPayload = {\n /** Optional tenant filter; when absent, all active discord channels connect. */\n tenantId?: string\n /**\n * Optional organization filter, narrowing within `tenantId`. Both the channel\n * query and the reconciliation honour it, so a job scoped to one organization\n * never opens, and never tears down, a socket belonging to another.\n */\n organizationId?: string | null\n}\n\nexport interface GatewayConnectionEntry {\n handle: DiscordGatewayHandle\n tenantId: string\n /** Owning organization, or `null` for a tenant-wide channel. Scopes teardown. */\n organizationId: string | null\n /** SHA-256 of the bot token, never the token itself \u2014 see `botTokenFingerprint`. */\n botTokenFingerprint?: string\n}\n\n/**\n * Stable, non-reversible identity for a bot token, used to detect that two\n * channel rows are really the same Discord bot.\n *\n * The hub derives a channel's `externalIdentifier` by sniffing the credential\n * bag for email-shaped keys, which Discord has none of, so every reconnect\n * inserts a fresh row instead of healing the existing one. Until that is fixed\n * hub-side, two rows for one bot would each open a socket and IDENTIFY\n * independently, defeating the single-identify discipline `concurrency: 1`\n * exists to enforce. Hashing keeps the token out of the registry and the logs.\n */\nexport function botTokenFingerprint(botToken: string): string {\n return createHash('sha256').update(botToken).digest('hex')\n}\n\n/**\n * Whether another channel already holds a live session for the same bot.\n * Pure over its arguments so the guard is unit-testable without sockets.\n */\nexport function findChannelWithSameBot(\n channelId: string,\n fingerprint: string,\n connections: Map<string, GatewayConnectionEntry> = activeConnections,\n): string | null {\n for (const [otherId, entry] of connections) {\n if (otherId === channelId) continue\n if (entry.botTokenFingerprint !== fingerprint) continue\n if (!isConnectionLive(entry)) continue\n return otherId\n }\n return null\n}\n\n// Module-level registry so a re-run replaces an existing connection instead of\n// opening a second socket for the same channel (single-identify discipline).\n// Keyed by channel id; carries tenantId so a per-tenant reconciliation never\n// tears down another tenant's sockets.\nconst activeConnections = new Map<string, GatewayConnectionEntry>()\n\n// Serializes resume-state writes per channel. The socket callbacks that produce\n// them (READY on every (re)connect) fire independently of the job loop, so\n// without a chain two callbacks for the same channel could interleave their\n// read-modify-write and lose the fresher one.\nconst pendingStateWrites = new Map<string, Promise<unknown>>()\n\n/**\n * Whether a registry entry still holds a running session. A session that is\n * merely reconnecting with backoff after a resumable close reports `true` \u2014 it\n * heals itself and MUST NOT be torn down and replaced by the refresh job.\n */\nexport function isConnectionLive(entry: GatewayConnectionEntry | undefined): boolean {\n if (!entry) return false\n try {\n return entry.handle.isActive()\n } catch {\n return false\n }\n}\n\n/** The scope a reconciliation run is allowed to tear connections down within. */\nexport interface GatewayReconcileScope {\n tenantId?: string\n organizationId?: string | null\n}\n\n/**\n * The channel query for one job payload. Every scope key the payload carries\n * narrows the query: a job scoped to an organization must connect that\n * organization's channels and no others, and the same scope is then handed to\n * the reconciliation so it tears down exactly the set it queried.\n */\nexport function buildGatewayChannelFilter(scope: GatewayReconcileScope): Record<string, unknown> {\n const filter: Record<string, unknown> = { providerKey: 'discord', isActive: true, deletedAt: null }\n if (scope.tenantId) filter.tenantId = scope.tenantId\n if (scope.organizationId != null) filter.organizationId = scope.organizationId\n return filter\n}\n\n/**\n * Close + drop any live connection whose channel is no longer in the active set\n * (deactivated / soft-deleted / re-scoped). Without this the socket + heartbeat\n * timer would leak forever after a channel is disconnected. When `scope` names a\n * tenant and/or an organization, only connections inside that scope are eligible\n * for teardown \u2014 a scoped refresh reconciles exactly the set it queried and\n * never touches another tenant's or organization's sockets. Returns the ids\n * reconciled away. Pure over its arguments so it is unit-testable.\n */\nexport function reconcileGatewayConnections(\n activeChannelIds: Set<string>,\n connections: Map<string, GatewayConnectionEntry> = activeConnections,\n scope: GatewayReconcileScope = {},\n): string[] {\n const removed: string[] = []\n for (const [channelId, entry] of connections) {\n if (activeChannelIds.has(channelId)) continue\n if (scope.tenantId && entry.tenantId !== scope.tenantId) continue\n if (scope.organizationId != null && entry.organizationId !== scope.organizationId) continue\n try {\n entry.handle.close()\n } catch {\n /* best-effort close */\n }\n connections.delete(channelId)\n removed.push(channelId)\n }\n return removed\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n}\n\nexport default async function handle(job: QueuedJob<GatewayJobPayload>, ctx: HandlerContext): Promise<void> {\n if (parseBooleanWithDefault(process.env.OM_CHANNEL_DISCORD_GATEWAY_DISABLED, false)) {\n logger.info('gateway disabled via OM_CHANNEL_DISCORD_GATEWAY_DISABLED \u2014 skipping connect')\n return\n }\n\n const em = (ctx.resolve('em') as EntityManager).fork()\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n if (!credentialsService) {\n logger.warn('integrationCredentialsService unavailable \u2014 cannot resolve bot tokens')\n return\n }\n\n // A scoped job narrows the query by every scope key it carries \u2014 a payload\n // that names an organization must not silently connect the whole tenant.\n const scope: GatewayReconcileScope = {\n tenantId: job.payload?.tenantId,\n organizationId: job.payload?.organizationId ?? null,\n }\n\n const channels = (await findWithDecryption(\n em,\n CommunicationChannel,\n buildGatewayChannelFilter(scope),\n )) as CommunicationChannel[]\n\n // The refresh job is a *reconciler*, not a re-connector: a channel whose\n // session is still running is left alone. Restarting it (the previous\n // behaviour) tore down healthy sockets on every tick of the CLI's 60s refresh\n // and forced a fresh IDENTIFY each time, which burns Discord's session-start\n // budget and drops the dispatches in flight.\n let started = 0\n let kept = 0\n let quarantined = 0\n for (const channel of channels) {\n if (isConnectionLive(activeConnections.get(channel.id))) {\n kept += 1\n continue\n }\n // A channel Discord fatally rejected (bad token / disallowed intents) stays\n // parked until an operator reconnects it. Retrying cannot fix either cause,\n // and re-IDENTIFYing every tick burns the bot's daily session-start budget.\n if (channel.status === 'requires_reauth') {\n quarantined += 1\n continue\n }\n await startChannelConnection(channel, credentialsService, em)\n started += 1\n }\n\n // Full reconciliation: close sockets for channels that dropped out of the\n // active set since the last run (deactivated / soft-deleted). A scoped run\n // reconciles within exactly the scope it queried, so a per-tenant or\n // per-organization refresh never tears down sockets it did not consider.\n const activeIds = new Set(channels.map((channel) => channel.id))\n const removed = reconcileGatewayConnections(activeIds, activeConnections, scope)\n if (removed.length > 0) {\n logger.info('reconciled away stale discord gateway connections', { channelIds: removed })\n }\n logger.debug('discord gateway reconciliation finished', { started, kept, quarantined, removed: removed.length })\n}\n\nasync function startChannelConnection(\n channel: CommunicationChannel,\n credentialsService: CredentialsServiceLike,\n em: EntityManager,\n): Promise<void> {\n const scope: GatewayChannelScope = {\n channelId: channel.id,\n channelType: channel.channelType,\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? null,\n }\n\n let credentials: Record<string, unknown> | null = null\n try {\n credentials = await credentialsService.resolve('channel_discord', {\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? channel.tenantId,\n userId: channel.userId ?? null,\n })\n } catch (err) {\n logger.warn('failed to resolve discord credentials for channel', { channelId: channel.id, err })\n return\n }\n if (!credentials) return\n\n let botToken: string\n try {\n botToken = parseDiscordCredentialsOrThrow(credentials).botToken\n } catch (err) {\n logger.warn('invalid discord credentials for channel', { channelId: channel.id, err })\n return\n }\n\n const fingerprint = botTokenFingerprint(botToken)\n const duplicateOf = findChannelWithSameBot(channel.id, fingerprint)\n if (duplicateOf) {\n logger.warn('skipping discord gateway connect \u2014 another channel already serves this bot', {\n channelId: channel.id,\n servedBy: duplicateOf,\n })\n return\n }\n\n const channelState = discordChannelStateSchema.parse(channel.channelState ?? {})\n const resumeState: GatewayResumeState = {\n sessionId: channelState.sessionId,\n sequence: channelState.sequence ?? null,\n resumeGatewayUrl: channelState.resumeGatewayUrl,\n }\n\n // Only reached for a channel with no live session (see `handle`): drop the\n // dead handle, if any, before opening its replacement.\n const stale = activeConnections.get(channel.id)\n if (stale) {\n try {\n stale.handle.close()\n } catch {\n /* best-effort close of an already-dead session */\n }\n activeConnections.delete(channel.id)\n }\n\n const stateScope: DiscordChannelScope = {\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? null,\n }\n\n const inboundQueue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.inbound)\n const reactionsQueue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.reactions)\n let botUserId: string | undefined = channelState.botUserId\n\n const handle = getDiscordGatewayClient().connect({\n botToken,\n resumeState,\n onMessage: async (message) => {\n // Log receive / drop / enqueue separately. Without these three lines a lost\n // inbound message is indistinguishable from a message correctly dropped by\n // the bot-self guard, and both are indistinguishable from a dead socket \u2014\n // diagnosing it required an external WebSocket probe during QA.\n logger.debug('discord gateway received message', { channelId: channel.id, messageId: message.id })\n const jobPayload = buildInboundMessageJob({ message, channel: scope, botUserId })\n if (!jobPayload) {\n logger.debug('discord inbound message dropped \u2014 authored by this bot', {\n channelId: channel.id,\n messageId: message.id,\n })\n return\n }\n await inboundQueue.enqueue(jobPayload as unknown as Record<string, unknown>)\n logger.debug('discord inbound message enqueued', {\n channelId: channel.id,\n messageId: message.id,\n queue: COMMUNICATION_CHANNELS_QUEUES.inbound,\n })\n },\n onReaction: async (reaction, action) => {\n const reactionJob = await buildReactionJob({ reaction, action, channel: scope, botUserId })\n if (!reactionJob) return\n await reactionsQueue.enqueue(reactionJob as unknown as Record<string, unknown>)\n },\n onReady: async ({ botUserId: readyBotUserId, resumeState: freshResumeState }) => {\n botUserId = readyBotUserId || botUserId\n await persistChannelState(em, channel.id, stateScope, { ...freshResumeState, botUserId })\n logger.info('discord gateway ready', { channelId: channel.id })\n },\n onRequiresReauth: async ({ code }) => {\n logger.warn('discord gateway fatal close \u2014 quarantining channel', { channelId: channel.id, code })\n // Persist the status BEFORE dropping the handle. Emitting the event alone\n // is not enough: nothing subscribes to it to change channel state, so the\n // next reconciliation tick would see \"no live session\" and IDENTIFY again,\n // forever, against a token or intent configuration that cannot self-heal.\n await quarantineChannel(em, channel.id, stateScope, `gateway_close_${code}`)\n await emitCommunicationChannelsEvent(\n 'communication_channels.channel.requires_reauth',\n {\n channelId: channel.id,\n providerKey: 'discord',\n channelType: channel.channelType,\n reason: `gateway_close_${code}`,\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? null,\n },\n { persistent: true },\n )\n activeConnections.delete(channel.id)\n },\n })\n activeConnections.set(channel.id, {\n handle,\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? null,\n botTokenFingerprint: fingerprint,\n })\n}\n\n/**\n * Persist gateway resume state for one channel, tenant-scoped and serialized.\n *\n * The write is queued behind any write already in flight for the same channel so\n * two socket callbacks cannot interleave their read-modify-write, and it runs\n * through `persistDiscordChannelState`, which filters by `tenant_id` /\n * `organization_id` and only merges the gateway-owned keys.\n */\nasync function persistChannelState(\n em: EntityManager,\n channelId: string,\n scope: DiscordChannelScope,\n patch: DiscordChannelStatePatch,\n): Promise<void> {\n const previous = pendingStateWrites.get(channelId) ?? Promise.resolve()\n const next = previous\n .catch(() => undefined)\n .then(async () => {\n try {\n const result = await persistDiscordChannelState({ em, channelId, scope, patch })\n if (result === 'not_found') {\n logger.warn('discord channel not found in scope while persisting resume state', { channelId })\n }\n } catch (err) {\n // Resume-state persistence is best-effort \u2014 a failure just means the next\n // connect re-identifies fresh instead of resuming.\n logger.warn('failed to persist discord gateway resume state', { channelId, err })\n }\n })\n pendingStateWrites.set(channelId, next)\n await next\n if (pendingStateWrites.get(channelId) === next) pendingStateWrites.delete(channelId)\n}\n\n/**\n * Park a fatally-rejected channel, serialized behind the same per-channel write\n * chain as the resume-state writes so a quarantine and a late `READY` callback\n * cannot interleave their read-modify-write on the same row.\n */\nasync function quarantineChannel(\n em: EntityManager,\n channelId: string,\n scope: DiscordChannelScope,\n reason: string,\n): Promise<void> {\n const previous = pendingStateWrites.get(channelId) ?? Promise.resolve()\n const next = previous\n .catch(() => undefined)\n .then(async () => {\n try {\n const result = await quarantineDiscordChannel({ em, channelId, scope, reason })\n if (result === 'not_found') {\n logger.warn('discord channel not found in scope while quarantining', { channelId })\n }\n } catch (err) {\n logger.warn('failed to quarantine discord channel after fatal close', { channelId, reason, err })\n }\n })\n pendingStateWrites.set(channelId, next)\n await next\n if (pendingStateWrites.get(channelId) === next) pendingStateWrites.delete(channelId)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["handle"]
|
|
7
|
+
}
|