@open-everywhere/provider-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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +47 -0
- package/agent/access/discord-grant-click.ts +237 -0
- package/agent/approvals/discord-admin.ts +227 -0
- package/agent/approvals/discord-card.ts +248 -0
- package/agent/approvals/discord-flow.ts +469 -0
- package/agent/approvals/discord-scope-label.ts +27 -0
- package/agent/channels/discord-approval-card.ts +373 -0
- package/agent/channels/discord-backscroll.ts +593 -0
- package/agent/channels/discord-bot-name.ts +113 -0
- package/agent/channels/discord-channel-info.ts +83 -0
- package/agent/channels/discord-directory.ts +179 -0
- package/agent/channels/discord-dispatch-target.ts +103 -0
- package/agent/channels/discord-events.ts +175 -0
- package/agent/channels/discord-front.ts +355 -0
- package/agent/channels/discord-gateway.ts +3256 -0
- package/agent/channels/discord-identity.ts +269 -0
- package/agent/channels/discord-interaction-defer.ts +123 -0
- package/agent/channels/discord-message-ops.ts +127 -0
- package/agent/channels/discord-origin-codec.ts +105 -0
- package/agent/channels/discord-post-arm.ts +230 -0
- package/agent/channels/discord-post.ts +463 -0
- package/agent/channels/discord-read.ts +140 -0
- package/agent/channels/discord-rest.ts +85 -0
- package/agent/channels/discord.ts +244 -0
- package/agent/commands/discord-command-sync.ts +285 -0
- package/agent/commands/discord-commands.ts +291 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/identity/discord-link-consent.ts +124 -0
- package/agent/ops.ts +14 -0
- package/agent/provider-events.ts +86 -0
- package/agent/register.ts +147 -0
- package/agent/surface-parity.ts +95 -0
- package/dist/agent/access/discord-grant-click.d.ts +25 -0
- package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
- package/dist/agent/access/discord-grant-click.js +210 -0
- package/dist/agent/access/discord-grant-click.js.map +1 -0
- package/dist/agent/approvals/discord-admin.d.ts +63 -0
- package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
- package/dist/agent/approvals/discord-admin.js +219 -0
- package/dist/agent/approvals/discord-admin.js.map +1 -0
- package/dist/agent/approvals/discord-card.d.ts +102 -0
- package/dist/agent/approvals/discord-card.d.ts.map +1 -0
- package/dist/agent/approvals/discord-card.js +217 -0
- package/dist/agent/approvals/discord-card.js.map +1 -0
- package/dist/agent/approvals/discord-flow.d.ts +109 -0
- package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
- package/dist/agent/approvals/discord-flow.js +375 -0
- package/dist/agent/approvals/discord-flow.js.map +1 -0
- package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
- package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
- package/dist/agent/approvals/discord-scope-label.js +27 -0
- package/dist/agent/approvals/discord-scope-label.js.map +1 -0
- package/dist/agent/channels/discord-approval-card.d.ts +81 -0
- package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/discord-approval-card.js +303 -0
- package/dist/agent/channels/discord-approval-card.js.map +1 -0
- package/dist/agent/channels/discord-backscroll.d.ts +209 -0
- package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/discord-backscroll.js +505 -0
- package/dist/agent/channels/discord-backscroll.js.map +1 -0
- package/dist/agent/channels/discord-bot-name.d.ts +19 -0
- package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
- package/dist/agent/channels/discord-bot-name.js +98 -0
- package/dist/agent/channels/discord-bot-name.js.map +1 -0
- package/dist/agent/channels/discord-channel-info.d.ts +22 -0
- package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
- package/dist/agent/channels/discord-channel-info.js +65 -0
- package/dist/agent/channels/discord-channel-info.js.map +1 -0
- package/dist/agent/channels/discord-directory.d.ts +11 -0
- package/dist/agent/channels/discord-directory.d.ts.map +1 -0
- package/dist/agent/channels/discord-directory.js +170 -0
- package/dist/agent/channels/discord-directory.js.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.js +99 -0
- package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
- package/dist/agent/channels/discord-events.d.ts +58 -0
- package/dist/agent/channels/discord-events.d.ts.map +1 -0
- package/dist/agent/channels/discord-events.js +150 -0
- package/dist/agent/channels/discord-events.js.map +1 -0
- package/dist/agent/channels/discord-front.d.ts +115 -0
- package/dist/agent/channels/discord-front.d.ts.map +1 -0
- package/dist/agent/channels/discord-front.js +279 -0
- package/dist/agent/channels/discord-front.js.map +1 -0
- package/dist/agent/channels/discord-gateway.d.ts +783 -0
- package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
- package/dist/agent/channels/discord-gateway.js +2591 -0
- package/dist/agent/channels/discord-gateway.js.map +1 -0
- package/dist/agent/channels/discord-identity.d.ts +110 -0
- package/dist/agent/channels/discord-identity.d.ts.map +1 -0
- package/dist/agent/channels/discord-identity.js +201 -0
- package/dist/agent/channels/discord-identity.js.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.js +102 -0
- package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
- package/dist/agent/channels/discord-message-ops.d.ts +3 -0
- package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/discord-message-ops.js +123 -0
- package/dist/agent/channels/discord-message-ops.js.map +1 -0
- package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
- package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/discord-origin-codec.js +98 -0
- package/dist/agent/channels/discord-origin-codec.js.map +1 -0
- package/dist/agent/channels/discord-post-arm.d.ts +3 -0
- package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/discord-post-arm.js +195 -0
- package/dist/agent/channels/discord-post-arm.js.map +1 -0
- package/dist/agent/channels/discord-post.d.ts +120 -0
- package/dist/agent/channels/discord-post.d.ts.map +1 -0
- package/dist/agent/channels/discord-post.js +426 -0
- package/dist/agent/channels/discord-post.js.map +1 -0
- package/dist/agent/channels/discord-read.d.ts +40 -0
- package/dist/agent/channels/discord-read.d.ts.map +1 -0
- package/dist/agent/channels/discord-read.js +128 -0
- package/dist/agent/channels/discord-read.js.map +1 -0
- package/dist/agent/channels/discord-rest.d.ts +33 -0
- package/dist/agent/channels/discord-rest.d.ts.map +1 -0
- package/dist/agent/channels/discord-rest.js +75 -0
- package/dist/agent/channels/discord-rest.js.map +1 -0
- package/dist/agent/channels/discord.d.ts +3 -0
- package/dist/agent/channels/discord.d.ts.map +1 -0
- package/dist/agent/channels/discord.js +230 -0
- package/dist/agent/channels/discord.js.map +1 -0
- package/dist/agent/commands/discord-command-sync.d.ts +66 -0
- package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
- package/dist/agent/commands/discord-command-sync.js +224 -0
- package/dist/agent/commands/discord-command-sync.js.map +1 -0
- package/dist/agent/commands/discord-commands.d.ts +45 -0
- package/dist/agent/commands/discord-commands.d.ts.map +1 -0
- package/dist/agent/commands/discord-commands.js +266 -0
- package/dist/agent/commands/discord-commands.js.map +1 -0
- package/dist/agent/contributions.d.ts +17 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +31 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/identity/discord-link-consent.d.ts +13 -0
- package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
- package/dist/agent/identity/discord-link-consent.js +111 -0
- package/dist/agent/identity/discord-link-consent.js.map +1 -0
- package/dist/agent/ops.d.ts +2 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +13 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +78 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +125 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +90 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +185 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// discord-post-arm — Discord's POST arm for the post-arms registry (OPE-114 stage 1c). Absorbs,
|
|
2
|
+
// VERBATIM, Discord's arms out of the three reply/delivery spines: reply-post.ts (resolveReplyTarget's
|
|
3
|
+
// discord case + dispatchReply's POST-messages branch + the discord splitForDiscord chunking used by
|
|
4
|
+
// postTerminalProse), unified-reply.ts (liveDiscordPost, incl. the auto-thread restore + engage), and
|
|
5
|
+
// unified-redrive.ts (the discord describe/container cases). The write seam is discord-post.ts (the
|
|
6
|
+
// same one the reply tools use — deliberately NOT the gateway module).
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
DISCORD_MESSAGE_MAX,
|
|
10
|
+
discordAddReaction,
|
|
11
|
+
discordEditMessage,
|
|
12
|
+
discordExistingThreadForMessage,
|
|
13
|
+
discordOpenDmChannel,
|
|
14
|
+
discordPostMessage,
|
|
15
|
+
discordPostMessageWithButtons,
|
|
16
|
+
discordThreadForMessage,
|
|
17
|
+
splitForDiscord,
|
|
18
|
+
} from "./discord-post.js";
|
|
19
|
+
import {
|
|
20
|
+
bindFrom,
|
|
21
|
+
markThreadEngaged,
|
|
22
|
+
deploymentOrg,
|
|
23
|
+
recordOwnPost,
|
|
24
|
+
type PostArm,
|
|
25
|
+
type TargetResolveBag,
|
|
26
|
+
type ReplyTarget,
|
|
27
|
+
type ReactTarget,
|
|
28
|
+
type UnifiedWakeOrigin,
|
|
29
|
+
type UnifiedReplyDeps,
|
|
30
|
+
type EditLocus,
|
|
31
|
+
type DirectMessageRequest,
|
|
32
|
+
type DirectMessageDelivery,
|
|
33
|
+
type DirectMessageAction,
|
|
34
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
35
|
+
|
|
36
|
+
type DiscordTarget = Extract<ReplyTarget, { platform: "discord" }>;
|
|
37
|
+
type DiscordOrigin = Extract<UnifiedWakeOrigin, { platform: "discord" }>;
|
|
38
|
+
type DiscordReactTarget = Extract<ReactTarget, { platform: "discord" }>;
|
|
39
|
+
|
|
40
|
+
/** UNIFIED: POST /channels/{id}/messages under the bot token. The target channelId is the frozen wake
|
|
41
|
+
* origin's (host-side, never model-authored); a thread IS its own channel id so a threaded trigger
|
|
42
|
+
* replies into its thread. The FIRST chunk anchors to the triggering message via message_reference
|
|
43
|
+
* (fail-soft when the trigger was deleted); Discord caps a message at 2000 chars, so a long reply
|
|
44
|
+
* splits and posts in order. Errors re-wrapped token-free (defensive parity with the other arms —
|
|
45
|
+
* discordPostMessage's own errors are already clean, but never risk echoing a transport body).
|
|
46
|
+
* Auto-thread restore (origin.autoThreadName ⇔ an addressed mention in a base channel): open the
|
|
47
|
+
* thread off the mention and answer there — the native path's UX the divert skips.
|
|
48
|
+
* discordThreadForMessage is idempotent (finds an existing thread) and fail-open (null ⇒ today's
|
|
49
|
+
* in-channel reply, anchored to the mention via message_reference). */
|
|
50
|
+
async function liveDiscordPost(origin: DiscordOrigin, text: string): Promise<string> {
|
|
51
|
+
let targetChannelId = origin.channelId;
|
|
52
|
+
let anchorToMessage = origin.messageId;
|
|
53
|
+
// The auto-thread branch is GUILD-ONLY, and now says so structurally. `!("dm" in origin)` narrows
|
|
54
|
+
// to the guild variant so `origin.guildId` type-checks below (OPE-315) — no behavioural change:
|
|
55
|
+
// the DM variant cannot carry `autoThreadName` in the first place (the type forbids it and the
|
|
56
|
+
// validator refuses it, D4), precisely because the engagement row this branch writes is keyed on
|
|
57
|
+
// the guild. The narrowing makes that unrepresentable rather than merely unreached.
|
|
58
|
+
if (!("dm" in origin) && origin.autoThreadName && origin.messageId) {
|
|
59
|
+
const threadId = await discordThreadForMessage(origin.channelId, origin.messageId, origin.autoThreadName);
|
|
60
|
+
if (threadId) {
|
|
61
|
+
targetChannelId = threadId;
|
|
62
|
+
anchorToMessage = undefined; // the thread already hangs off the mention — no reference needed
|
|
63
|
+
// ENGAGE the thread we just opened — the native path engages at dispatch, but this thread
|
|
64
|
+
// only exists NOW, so the divert path must enroll it here or non-mention follow-ups inside
|
|
65
|
+
// it are dropped at the wake gate (live-found 2026-07-19: J-DISC's continuity leg — the
|
|
66
|
+
// reply landed, then "expand that..." produced zero audit rows). The partition mirrors what
|
|
67
|
+
// the gate resolves for a message ARRIVING in the thread (workspace=guild, surface=thread id,
|
|
68
|
+
// key=thread channel id — engagementKey). markThreadEngaged is best-effort/never throws, and
|
|
69
|
+
// a later "stop" disengages normally: nothing re-engages until OE next replies here.
|
|
70
|
+
const workspaceKey = `discord/${origin.guildId}`;
|
|
71
|
+
const surfaceKey = `${workspaceKey}/${threadId}`;
|
|
72
|
+
await markThreadEngaged(
|
|
73
|
+
{ org: deploymentOrg(), platform: "discord", workspace: origin.guildId, surface: threadId, workspaceKey, surfaceKey, key: surfaceKey },
|
|
74
|
+
threadId,
|
|
75
|
+
new Date().toISOString(),
|
|
76
|
+
{ participating: true },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let first: string | undefined;
|
|
81
|
+
try {
|
|
82
|
+
for (const chunk of splitForDiscord(text)) {
|
|
83
|
+
if (!chunk.trim()) continue; // never post an invisible empty message
|
|
84
|
+
const id = await discordPostMessage(targetChannelId, chunk, first === undefined && anchorToMessage ? { replyToMessageId: anchorToMessage } : undefined);
|
|
85
|
+
first ??= id;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(`unified reply couldn't post to Discord — create-message failed on channel ${targetChannelId}`);
|
|
89
|
+
}
|
|
90
|
+
if (first === undefined) {
|
|
91
|
+
throw new Error(`unified reply couldn't post to Discord — nothing to post (empty reply) for channel ${origin.channelId}`);
|
|
92
|
+
}
|
|
93
|
+
// 🔴 OPE-793 — THE ONE PLACE AN ARM POSTS SOMEWHERE ITS SPINE CANNOT NAME. When the auto-thread
|
|
94
|
+
// branch above fires, this reply lands in a thread that did not exist when the wake origin was
|
|
95
|
+
// frozen, so the spine's own ledger write records `origin.channelId` — the BASE channel — and an
|
|
96
|
+
// `edit_reply` addressed at the thread would be refused as not-ours. Recording the real channel
|
|
97
|
+
// here closes that, and only that: the id is one this call just posted, at the locus it posted to.
|
|
98
|
+
// Best-effort by construction (recordOwnPost never throws); a lost row costs an edit, never a post.
|
|
99
|
+
if (targetChannelId !== origin.channelId) {
|
|
100
|
+
await recordOwnPost(deploymentOrg(), { platform: "discord", channelId: targetChannelId }, first);
|
|
101
|
+
}
|
|
102
|
+
return first;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* UNIFIED, WITH A COMPONENT PLANE (OPE-1720): the same single-window post as {@link liveDiscordPost},
|
|
107
|
+
* carrying a row of tappable actions under it. Discord is the SECOND surface to implement this
|
|
108
|
+
* member, and it may only do so because the tap lands: `discord-front.ts` classifies the `oe_input:`
|
|
109
|
+
* custom_id and `discord-approval-card.ts` settles it.
|
|
110
|
+
*
|
|
111
|
+
* 🔴 NO AUTO-THREAD BRANCH, DELIBERATELY. `liveDiscordPost` may open a thread off the mention and
|
|
112
|
+
* answer there. A governed approval card must not move: the people who can answer it are watching the
|
|
113
|
+
* conversation it was asked in, and burying the buttons in a freshly-created thread is how a card
|
|
114
|
+
* goes unanswered. The anchor still rides — the card hangs under the triggering message through the
|
|
115
|
+
* same `message_reference` a plain reply uses — so it is placed and never moved.
|
|
116
|
+
*
|
|
117
|
+
* 🔴 IT FALLS BACK TO TEXT RATHER THAN SPLITTING. Discord caps a message at 2000 characters and a
|
|
118
|
+
* button row cannot be split across messages: a row attached to the wrong part would sit under a
|
|
119
|
+
* fragment. So an over-cap card posts through the ordinary splitting path and loses the SHORTCUT,
|
|
120
|
+
* never the meaning — `DirectMessageAction`'s own rule, which the question bridge's text already
|
|
121
|
+
* satisfies (it prints the prompt, the options, and the line saying a reply may name one).
|
|
122
|
+
*/
|
|
123
|
+
async function liveDiscordPostWithActions(origin: DiscordOrigin, text: string, actions: readonly DirectMessageAction[]): Promise<string> {
|
|
124
|
+
const buttons = actions.map((a) => ({ customId: a.id, label: a.label, intent: a.intent }));
|
|
125
|
+
if (buttons.length === 0 || text.length > DISCORD_MESSAGE_MAX) return liveDiscordPost(origin, text);
|
|
126
|
+
try {
|
|
127
|
+
return await discordPostMessageWithButtons(origin.channelId, text, buttons, origin.messageId ? { replyToMessageId: origin.messageId } : undefined);
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error(`unified reply couldn't post to Discord — create-message with buttons failed on channel ${origin.channelId}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** EDIT (OPE-793): `PATCH /channels/{id}/messages/{mid}` through the same write seam the post path
|
|
134
|
+
* uses. Ownership was already decided upstream against the own-post ledger; this is transport only. */
|
|
135
|
+
async function editDiscordMessage(locus: EditLocus, messageId: string, text: string): Promise<void> {
|
|
136
|
+
await discordEditMessage(locus.channelId, messageId, text);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** EDIT ADDRESS (OPE-798): the other half of the auto-thread branch in {@link liveDiscordPost}.
|
|
140
|
+
*
|
|
141
|
+
* When that branch fires the reply lands in a thread — and a Discord thread IS its own channel —
|
|
142
|
+
* while the frozen wake origin still names the BASE channel. The arm already records an own-post
|
|
143
|
+
* ledger row on the thread so the fence passes; without this the PATCH still went to the base
|
|
144
|
+
* channel and Discord answered 404 (measured live, deploy #89). Here the edit path resolves the
|
|
145
|
+
* SAME thread the post path used, so the ledger read and the PATCH address one conversation.
|
|
146
|
+
*
|
|
147
|
+
* 🔴 READ-ONLY: `discordExistingThreadForMessage`, never `discordThreadForMessage`. The post path
|
|
148
|
+
* may CREATE the thread; the edit path may not. A create at edit time would open a new empty thread
|
|
149
|
+
* — a visible message in the room, and still a 404 for a message that is not in it.
|
|
150
|
+
*
|
|
151
|
+
* Fail-soft to the base locus on every unknown: a DM origin, a wake with no auto-thread name, a
|
|
152
|
+
* message with no thread, an API failure. That degrades to today's behaviour, which is correct for
|
|
153
|
+
* a reply that genuinely went to the base channel. */
|
|
154
|
+
async function retargetDiscordEditLocus(origin: UnifiedWakeOrigin, locus: EditLocus): Promise<EditLocus> {
|
|
155
|
+
const o = origin as DiscordOrigin;
|
|
156
|
+
if ("dm" in o || !o.autoThreadName || !o.messageId) return locus;
|
|
157
|
+
const threadId = await discordExistingThreadForMessage(o.channelId, o.messageId);
|
|
158
|
+
return threadId ? { ...locus, channelId: threadId } : locus;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* DM (OPE-623): open the 1:1 conversation with a Discord user and post there. The SECOND provider to
|
|
163
|
+
* implement this member — Slack was the only one, and several core behaviours read the registry
|
|
164
|
+
* rather than a platform list precisely so that they tighten the day a second arm appears:
|
|
165
|
+
*
|
|
166
|
+
* • `link_identity` can now offer the DIRECTORY transport on Discord (it needs a directory arm AND
|
|
167
|
+
* a DM arm; channels/discord-directory.ts is the other half).
|
|
168
|
+
* • `claimVenueRefusal` (core identity/link-flow.ts) now REFUSES a `link <code>` claim typed in a
|
|
169
|
+
* Discord channel and points the person at the DM. A code is a bearer secret for its whole TTL,
|
|
170
|
+
* so it must not be posted where a room can read it. That is a deliberate tightening, not a
|
|
171
|
+
* regression: it only arms where a private door exists.
|
|
172
|
+
* • link-set change notices (`notifyLinkSetMembers`) and the governance DM-delivery venue
|
|
173
|
+
* (core access/pane-venue.ts) reach Discord accounts instead of skipping them.
|
|
174
|
+
*
|
|
175
|
+
* The caller names WHO, never WHERE — opening the conversation is this arm's business. `text` goes
|
|
176
|
+
* through the same 2000-char splitter every other Discord post uses, so a long notice arrives whole
|
|
177
|
+
* rather than 400-ing; the FIRST part's id is the returned anchor.
|
|
178
|
+
*/
|
|
179
|
+
async function deliverDiscordDirectMessage(input: DirectMessageRequest): Promise<DirectMessageDelivery> {
|
|
180
|
+
const surfaceId = await discordOpenDmChannel(input.principalId);
|
|
181
|
+
// BUTTONS (OPE-622): Discord has a component plane, so a caller's `actions` render as a row under
|
|
182
|
+
// the text and a tap comes back to the interactions endpoint (channels/discord-front.ts). The
|
|
183
|
+
// button path cannot split an over-cap message, so a long text falls back to the plain splitting
|
|
184
|
+
// post rather than failing — the caller's copy always stands on its own without a button.
|
|
185
|
+
const buttons = (input.actions ?? []).map((a) => ({ customId: a.id, label: a.label, intent: a.intent }));
|
|
186
|
+
if (buttons.length > 0 && input.text.length <= DISCORD_MESSAGE_MAX) {
|
|
187
|
+
return { surfaceId, messageId: await discordPostMessageWithButtons(surfaceId, input.text, buttons) };
|
|
188
|
+
}
|
|
189
|
+
const messageId = await discordPostMessage(surfaceId, input.text);
|
|
190
|
+
return { surfaceId, messageId };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const discordPostArm: PostArm = {
|
|
194
|
+
platform: "discord",
|
|
195
|
+
resolveTarget: (bag: TargetResolveBag): ReplyTarget | null => {
|
|
196
|
+
// On Discord the surface channel already IS the thread — no separate thread ts.
|
|
197
|
+
const channelId = bag.str("channelId");
|
|
198
|
+
return channelId ? { platform: "discord", channelId } : null;
|
|
199
|
+
},
|
|
200
|
+
dispatchReply: (target, text) => discordPostMessage((target as DiscordTarget).channelId, text),
|
|
201
|
+
dispatchUnified: (origin, text, deps: UnifiedReplyDeps) => (deps.postDiscord ?? liveDiscordPost)(origin as DiscordOrigin, text),
|
|
202
|
+
// OPE-1720: Discord declares the unified component plane because the tap route exists on it —
|
|
203
|
+
// `discord-front.ts` classifies the `oe_input:` custom_id and `discord-approval-card.ts` settles
|
|
204
|
+
// it. The injected `postDiscord` override is honoured VERBATIM and drops the buttons: a test that
|
|
205
|
+
// scripted a text-only post must not start receiving a card it never asked for.
|
|
206
|
+
dispatchUnifiedActions: (origin, text, actions, deps: UnifiedReplyDeps) =>
|
|
207
|
+
deps.postDiscord ? deps.postDiscord(origin as DiscordOrigin, text) : liveDiscordPostWithActions(origin as DiscordOrigin, text, actions),
|
|
208
|
+
describeRedriveTarget: (origin) => `discord channel ${(origin as DiscordOrigin).channelId}`,
|
|
209
|
+
redriveContainerId: (origin) => (origin as DiscordOrigin).channelId,
|
|
210
|
+
editMessage: editDiscordMessage,
|
|
211
|
+
retargetEditLocus: retargetDiscordEditLocus,
|
|
212
|
+
deliverDirectMessage: deliverDiscordDirectMessage,
|
|
213
|
+
// REACT (OPE-987): the message-level (channelId, messageId) pair `setReaction` already takes for
|
|
214
|
+
// the host-placed ✋ glyph — closer to Slack's per-message granularity than to GitHub/Linear's
|
|
215
|
+
// conversation-anchor one (post-arms.ts `ReactTarget`'s header explains why the union carries both
|
|
216
|
+
// shapes). `channelId` comes off the binding's post-back target, same as `resolveTarget` above;
|
|
217
|
+
// `messageId` is NOT on that target (Discord threads reuse `channelId` for their own identity, so
|
|
218
|
+
// the triggering message rides its own `anchorMessageId` field — identity/binding.ts, OPE-671) —
|
|
219
|
+
// read via `bindFrom(bag.ctx)` rather than widening the bag's target shape for one extra field.
|
|
220
|
+
resolveReactTarget: (bag: TargetResolveBag): ReactTarget | null => {
|
|
221
|
+
const channelId = bag.str("channelId");
|
|
222
|
+
if (!channelId) return null;
|
|
223
|
+
const messageId = bindFrom(bag.ctx).anchorMessageId;
|
|
224
|
+
return messageId ? { platform: "discord", channelId, messageId } : null;
|
|
225
|
+
},
|
|
226
|
+
react: (target, emoji) => {
|
|
227
|
+
const t = target as DiscordReactTarget;
|
|
228
|
+
return discordAddReaction(t.channelId, t.messageId, emoji);
|
|
229
|
+
},
|
|
230
|
+
};
|