@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,102 @@
|
|
|
1
|
+
// THE DEFERRED INTERACTION RESPONSE (OPE-1282) — how a Discord door answers work that outlasts the
|
|
2
|
+
// three-second interaction token.
|
|
3
|
+
//
|
|
4
|
+
// WHAT WAS WRONG. Discord gives an interactions endpoint THREE SECONDS to answer, and it does NOT
|
|
5
|
+
// redeliver an expired token. A door that decides first and answers second — `await the work, then
|
|
6
|
+
// return the reply` — therefore has a window in which the work really lands and the person sees a
|
|
7
|
+
// red "This interaction failed" over it. `access/discord-grant-click.ts` is that shape: an APPROVE
|
|
8
|
+
// spends a 2000 ms borrow-read budget, then a CAS, a grant insert, audit rows and an awaited post
|
|
9
|
+
// into the requesting room. When it overruns, the owner believes their approval failed while a
|
|
10
|
+
// conversation can already spend their credential, and a second tap answers "that request is no
|
|
11
|
+
// longer open" — true, and reading like a second failure. Slack does not have this: it redelivers an
|
|
12
|
+
// interactivity payload it has not seen answered. Discord does not.
|
|
13
|
+
//
|
|
14
|
+
// THE SHAPE THAT CLOSES IT — ack, then decide, then edit:
|
|
15
|
+
// 1. Answer the interaction IMMEDIATELY with {@link deferredEphemeralResponse} (`type 5`,
|
|
16
|
+
// DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, `flags 64`). Discord renders "thinking…", privately,
|
|
17
|
+
// and the token is now good for fifteen minutes instead of three seconds.
|
|
18
|
+
// 2. Do the work off the response path, under the host's `waitUntil`.
|
|
19
|
+
// 3. Replace the placeholder with the real answer through {@link editOriginalInteractionResponse}.
|
|
20
|
+
//
|
|
21
|
+
// 🔴 THE EPHEMERAL FLAG RIDES THE DEFER, NOT THE EDIT, and that is not a style choice. Discord fixes
|
|
22
|
+
// a response's visibility at the FIRST response; the follow-up edit carries no flags and cannot
|
|
23
|
+
// change it. A defer sent without `flags 64` makes the eventual answer PUBLIC — on the grant door
|
|
24
|
+
// that would publish one person's credential decision into the channel. The flag is therefore part
|
|
25
|
+
// of the ack, and a test pins it.
|
|
26
|
+
//
|
|
27
|
+
// 🔴 THE FOLLOW-UP NEEDS NO BOT TOKEN AND NO NEW CONFIGURATION. The interaction webhook route is
|
|
28
|
+
// authenticated by the interaction TOKEN itself, and both halves of the address — `application_id`
|
|
29
|
+
// and `token` — ride the interaction payload Discord already signed. {@link interactionFollowUp}
|
|
30
|
+
// reads them from that verified envelope and returns `undefined` when either is missing, which is
|
|
31
|
+
// what lets a caller fall back to answering synchronously rather than deferring into silence.
|
|
32
|
+
//
|
|
33
|
+
// 🔴 THE INTERACTION TOKEN IS A CREDENTIAL. It authorises anyone holding it to write as the app for
|
|
34
|
+
// fifteen minutes. It never reaches a log line, an error string or an audit row from this module —
|
|
35
|
+
// `editOriginalInteractionResponse` builds the URL and reports only the HTTP status.
|
|
36
|
+
//
|
|
37
|
+
// EVERY WRITE GOES THROUGH `discordFetch` (the OPE-1236 seam), so the follow-up honours a 429's
|
|
38
|
+
// `retry_after` like every other Discord write. `discord-rest-seam-test.ts` §3 reds on a bare
|
|
39
|
+
// `fetch(` in this package, and this module holding its own would be exactly that.
|
|
40
|
+
import { discordFetch } from "./discord-rest.js";
|
|
41
|
+
const API = "https://discord.com/api/v10";
|
|
42
|
+
/** Discord interaction-response wire constants, defined locally for the same reason each door
|
|
43
|
+
* defines its own: this package holds no shared wire-constant module, and a door that cannot read
|
|
44
|
+
* its own numbers is harder to review than one that repeats them. */
|
|
45
|
+
const RESPONSE_DEFERRED_CHANNEL_MESSAGE = 5; // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
|
|
46
|
+
const EPHEMERAL = 64; // MessageFlags.EPHEMERAL — only the clicker sees it
|
|
47
|
+
function isObject(v) {
|
|
48
|
+
return typeof v === "object" && v !== null;
|
|
49
|
+
}
|
|
50
|
+
function str(v) {
|
|
51
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The follow-up address carried by one verified interaction envelope, or `undefined` when either
|
|
55
|
+
* half is absent.
|
|
56
|
+
*
|
|
57
|
+
* 🔴 `undefined` MUST MEAN "DO NOT DEFER". A door that deferred without an address would ack with
|
|
58
|
+
* "thinking…" and then have no way to say anything else — a permanent placeholder, which is worse
|
|
59
|
+
* than the slow answer this whole module exists to replace. Callers fall back to the synchronous
|
|
60
|
+
* shape instead. Unreachable on a real Discord payload (both fields are always present); written as
|
|
61
|
+
* a real branch because the alternative failure is silent and permanent.
|
|
62
|
+
*/
|
|
63
|
+
export function interactionFollowUp(raw) {
|
|
64
|
+
if (!isObject(raw))
|
|
65
|
+
return undefined;
|
|
66
|
+
const applicationId = str(raw["application_id"]);
|
|
67
|
+
const token = str(raw["token"]);
|
|
68
|
+
return applicationId === undefined || token === undefined ? undefined : { applicationId, token };
|
|
69
|
+
}
|
|
70
|
+
/** The immediate ack: "thinking…", visible only to the clicker. See the header for why the flag
|
|
71
|
+
* cannot be moved to the follow-up. */
|
|
72
|
+
export function deferredEphemeralResponse() {
|
|
73
|
+
return { type: RESPONSE_DEFERRED_CHANNEL_MESSAGE, data: { flags: EPHEMERAL } };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Replace the deferred placeholder with the real answer.
|
|
77
|
+
*
|
|
78
|
+
* NEVER THROWS. This runs off the response path, under `waitUntil`, on a decision that has ALREADY
|
|
79
|
+
* been made and written — an unsent follow-up costs the person their explanation, and throwing here
|
|
80
|
+
* would additionally poison whatever else that background promise carries. Returns whether the edit
|
|
81
|
+
* landed, so a caller can log the difference.
|
|
82
|
+
*
|
|
83
|
+
* The URL's `@original` is the deferred message itself; Discord has no other handle on it.
|
|
84
|
+
*/
|
|
85
|
+
export async function editOriginalInteractionResponse(followUp, content, deps = {}) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await discordFetch("PATCH /webhooks/{application.id}/{interaction.token}/messages/@original", `${API}/webhooks/${encodeURIComponent(followUp.applicationId)}/${encodeURIComponent(followUp.token)}/messages/@original`, {
|
|
88
|
+
method: "PATCH",
|
|
89
|
+
// NO `authorization` HEADER, deliberately: the interaction token in the path IS the
|
|
90
|
+
// authorisation, and Discord rejects this route when a bot token is sent alongside it.
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ content }),
|
|
93
|
+
}, deps);
|
|
94
|
+
return res.ok;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A 429 that outlasts the seam's attempts arrives as `RateLimitExhaustedError`; a transport
|
|
98
|
+
// failure arrives as itself. Neither may escape — see the paragraph above.
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=discord-interaction-defer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-interaction-defer.js","sourceRoot":"","sources":["../../../agent/channels/discord-interaction-defer.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,kCAAkC;AAClC,EAAE;AACF,kGAAkG;AAClG,mGAAmG;AACnG,kGAAkG;AAClG,mGAAmG;AACnG,kGAAkG;AAClG,+FAA+F;AAC/F,gGAAgG;AAChG,qGAAqG;AACrG,oEAAoE;AACpE,EAAE;AACF,0DAA0D;AAC1D,4FAA4F;AAC5F,kGAAkG;AAClG,+EAA+E;AAC/E,wEAAwE;AACxE,qGAAqG;AACrG,EAAE;AACF,qGAAqG;AACrG,gGAAgG;AAChG,kGAAkG;AAClG,mGAAmG;AACnG,kCAAkC;AAClC,EAAE;AACF,iGAAiG;AACjG,mGAAmG;AACnG,iGAAiG;AACjG,kGAAkG;AAClG,8FAA8F;AAC9F,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,qFAAqF;AACrF,EAAE;AACF,gGAAgG;AAChG,8FAA8F;AAC9F,mFAAmF;AAEnF,OAAO,EAAE,YAAY,EAAyB,MAAM,mBAAmB,CAAC;AAExE,MAAM,GAAG,GAAG,6BAA6B,CAAC;AAE1C;;sEAEsE;AACtE,MAAM,iCAAiC,GAAG,CAAC,CAAC,CAAC,uCAAuC;AACpF,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC,oDAAoD;AAS1E,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC;AAC7C,CAAC;AAED,SAAS,GAAG,CAAC,CAAU;IACrB,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,aAAa,GAAG,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAChC,OAAO,aAAa,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;AACnG,CAAC;AAED;wCACwC;AACxC,MAAM,UAAU,yBAAyB;IACvC,OAAO,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACnD,QAA6B,EAC7B,OAAe,EACf,IAAI,GAAqB,EAAE;IAE3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,YAAY,CAC5B,yEAAyE,EACzE,GAAG,GAAG,aAAa,kBAAkB,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,qBAAqB,EACxH;YACE,MAAM,EAAE,OAAO;YACf,oFAAoF;YACpF,uFAAuF;YACvF,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;SAClC,EACD,IAAI,CACL,CAAC;QACF,OAAO,GAAG,CAAC,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,4FAA4F;QAC5F,2EAA2E;QAC3E,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-message-ops.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-message-ops.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,iBAAiB,EAAgB,MAAM,4CAA4C,CAAC;AAYlG,eAAO,MAAM,iBAAiB,EAAE,iBAoG/B,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// The Discord SurfaceMessageOps — the bot-token REST transport for the ANCILLARY chat message ops
|
|
2
|
+
// generic core features dispatch through channels/surface-message-ops.ts (OPE-114 stage 4):
|
|
3
|
+
// - postText — a plain create-message (the hosted-artifact URL, the float notice, the approval
|
|
4
|
+
// decision notice). `replyToMessageId` anchors it via message_reference. Rides the
|
|
5
|
+
// reply feature's own write seam (discord-post.ts discordPostMessage — the 2000-char
|
|
6
|
+
// split + reply anchor already live there).
|
|
7
|
+
// - editMessage — PATCH /channels/{id}/messages/{mid} (an approval notice's decided-state rewrite).
|
|
8
|
+
// - postFiles — a hand-rolled multipart/form-data create-message (post_file's uploads). eve's
|
|
9
|
+
// Discord API is JSON-only, so payload_json + one files[i] part per file.
|
|
10
|
+
// - permalink — the message's own URL (request_access's PR body, OPE-303). NO network: Discord's
|
|
11
|
+
// message URL is a pure composition of ids we already hold.
|
|
12
|
+
// Registered by registerDiscordProvider(). The bot token is a SECRET — header only, never logged; on
|
|
13
|
+
// failure we surface the STATUS only.
|
|
14
|
+
import { discordPostMessage } from "./discord-post.js";
|
|
15
|
+
import { discordFetch } from "./discord-rest.js";
|
|
16
|
+
const API = "https://discord.com/api/v10";
|
|
17
|
+
function botToken() {
|
|
18
|
+
const token = process.env.DISCORD_BOT_TOKEN;
|
|
19
|
+
if (!token)
|
|
20
|
+
throw new Error("DISCORD_BOT_TOKEN is not set — the bot token is required to post to Discord.");
|
|
21
|
+
return token;
|
|
22
|
+
}
|
|
23
|
+
export const discordMessageOps = {
|
|
24
|
+
platform: "discord",
|
|
25
|
+
postText(channelId, text, opts) {
|
|
26
|
+
return discordPostMessage(channelId, text, opts?.replyToMessageId ? { replyToMessageId: opts.replyToMessageId } : undefined);
|
|
27
|
+
},
|
|
28
|
+
async editMessage(channelId, messageId, text) {
|
|
29
|
+
const res = await discordFetch("PATCH /channels/{id}/messages/{id}", `${API}/channels/${channelId}/messages/${messageId}`, {
|
|
30
|
+
method: "PATCH",
|
|
31
|
+
headers: { authorization: `Bot ${botToken()}`, "content-type": "application/json" },
|
|
32
|
+
// 🔴 `components: []` STRIPS THE BUTTONS, AND IT IS LOAD-BEARING (OPE-1260). Discord leaves a
|
|
33
|
+
// message's components untouched when a PATCH omits them, so without this line a card rewritten
|
|
34
|
+
// to a closed state kept four live buttons over copy saying the request was already over.
|
|
35
|
+
// Slack's own rewrite behaves this way already: `chat.update` sends a replacement `blocks`
|
|
36
|
+
// array and the buttons go with it.
|
|
37
|
+
//
|
|
38
|
+
// THE CALLER INVENTORY, because the strip is only safe while it holds. This arm is reached from
|
|
39
|
+
// ONE place — core's `channels/surface-message-ops.ts` `editSurfaceText` — whose only production
|
|
40
|
+
// caller is `access/credential-grant-card.ts` `rewriteGrantCard`. That helper now has FOUR
|
|
41
|
+
// callers: the sweep's expiry, the approve-time catalogue refusal, and (OPE-1278) the owner's
|
|
42
|
+
// own approve and decline. ALL FOUR RUN ONLY AFTER THE ASK ROW IS TERMINAL OR DECIDED — each
|
|
43
|
+
// one follows the CAS that closed the ask — so the strip still deletes no live control. An
|
|
44
|
+
// owner on Discord reaches this arm; an owner on Slack reaches `chat.update` instead.
|
|
45
|
+
// 🔴 A CALLER THAT MEANS "EDIT THE BODY, KEEP THE BUTTONS" MUST NOT USE THIS ARM — it has no way
|
|
46
|
+
// to say so, and this line would delete a live control. The core interface says the same.
|
|
47
|
+
body: JSON.stringify({ components: [], content: text }),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
throw new Error(`Discord edit-message failed (${res.status}) for channel ${channelId}`);
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* `https://discord.com/channels/<guild|@me>/<channel>/<message>` — the canonical Discord message
|
|
54
|
+
* link, composed rather than fetched (there is no getPermalink API and none is needed).
|
|
55
|
+
*
|
|
56
|
+
* 🔴 THE DM CASE IS WHY `ref.dm` EXISTS AND A HEURISTIC WOULD NOT DO. A Discord DM binds
|
|
57
|
+
* `workspace` to the USER id (core identity/binding.ts), and a user id and a guild id are both
|
|
58
|
+
* snowflakes — indistinguishable by shape. Putting a user id in the guild slot yields a URL that
|
|
59
|
+
* looks right and resolves to nothing, which is worse than no link at all in a PR body an admin
|
|
60
|
+
* reads to decide a grant. So a DM composes the `@me` form, and a channel with no workspace
|
|
61
|
+
* resolves `undefined` rather than guessing either way.
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ NOT YET REACHED BY THE ONE CORE CONSUMER, and the reason is in the BINDING, not here.
|
|
64
|
+
* `request_access` asks for a permalink to `binding.threadTs`, and this bundle's identity mapper
|
|
65
|
+
* (channels/discord-identity.ts) does not set that field at all — Discord threads are their own
|
|
66
|
+
* channel ids, so the binding carries a `channelId` and no per-message anchor. So a Discord
|
|
67
|
+
* access request still freezes no permalink, exactly as before, and its PR body falls back to
|
|
68
|
+
* naming the channel + scope. Giving the mapper a `threadTs` is not a free fix: that field also
|
|
69
|
+
* drives approval-card threading and the float notice, so it is its own change. Written down
|
|
70
|
+
* rather than left to be rediscovered — this method is correct and currently idle.
|
|
71
|
+
*/
|
|
72
|
+
async permalink(ref) {
|
|
73
|
+
const guild = ref.dm ? "@me" : ref.workspace;
|
|
74
|
+
return guild ? `https://discord.com/channels/${guild}/${ref.channelId}/${ref.messageId}` : undefined;
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* `{PUT|DELETE} /channels/{id}/messages/{mid}/reactions/{emoji}/@me` — the bot's own reaction,
|
|
78
|
+
* URL-encoded. This is the transport behind core's ✋ stop affordance (OPE-418).
|
|
79
|
+
*
|
|
80
|
+
* NEVER THROWS, deliberately, unlike the three ops above: a reaction is decoration beside work
|
|
81
|
+
* that is already running, so a missing permission must cost the turn its stop button, not the
|
|
82
|
+
* turn. A DELETE of a reaction that was never added 404s — that is the idempotent no-op, not an
|
|
83
|
+
* error.
|
|
84
|
+
*/
|
|
85
|
+
async setReaction(channelId, messageId, emoji, on) {
|
|
86
|
+
const encoded = encodeURIComponent(emoji);
|
|
87
|
+
try {
|
|
88
|
+
const res = await discordFetch(on ? "PUT /channels/{id}/messages/{id}/reactions/{emoji}/@me" : "DELETE /channels/{id}/messages/{id}/reactions/{emoji}/@me", `${API}/channels/${channelId}/messages/${messageId}/reactions/${encoded}/@me`, { method: on ? "PUT" : "DELETE", headers: { authorization: `Bot ${botToken()}` } });
|
|
89
|
+
if (!res.ok && !(!on && res.status === 404)) {
|
|
90
|
+
console.warn(`[open-everywhere:discord] reaction ${on ? "add" : "remove"} failed (${res.status}) for message ${messageId}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.warn(`[open-everywhere:discord] reaction ${on ? "add" : "remove"} error: ${err?.message ?? String(err)}`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async postFiles(channelId, files, comment) {
|
|
98
|
+
const form = new FormData();
|
|
99
|
+
const attachments = files.map((file, i) => ({ id: i, filename: file.filename, ...(file.caption ? { description: file.caption } : {}) }));
|
|
100
|
+
form.append("payload_json", JSON.stringify({ ...(comment ? { content: comment } : {}), attachments }));
|
|
101
|
+
files.forEach((file, i) => {
|
|
102
|
+
// A fresh ArrayBuffer copy so the Blob owns contiguous bytes regardless of the source view.
|
|
103
|
+
const ab = file.bytes.buffer.slice(file.bytes.byteOffset, file.bytes.byteOffset + file.bytes.byteLength);
|
|
104
|
+
form.append(`files[${i}]`, new Blob([ab], { type: file.contentType }), file.filename);
|
|
105
|
+
});
|
|
106
|
+
// No explicit content-type header — fetch derives the multipart boundary from the FormData.
|
|
107
|
+
const res = await discordFetch("POST /channels/{id}/messages", `${API}/channels/${channelId}/messages`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { authorization: `Bot ${botToken()}` },
|
|
110
|
+
body: form,
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
throw new Error(`Discord create-message (files) failed (HTTP ${res.status}) for channel ${channelId}`);
|
|
114
|
+
// Discord's create-message response carries the new message's own `id` — the same field
|
|
115
|
+
// discordPostOne reads for a plain text post (discord-post.ts). Hand it back so post_file's
|
|
116
|
+
// audit row can stamp `detail.id`, matching the reply path's shape (OPE-932). DELIBERATELY
|
|
117
|
+
// UNMARKED when absent, like discordPostOne: a 2xx means the message IS in the channel — this
|
|
118
|
+
// host just cannot name it, never an error worth failing an otherwise-successful upload over.
|
|
119
|
+
const body = (await res.json().catch(() => undefined));
|
|
120
|
+
return typeof body?.id === "string" ? body.id : undefined;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=discord-message-ops.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-message-ops.js","sourceRoot":"","sources":["../../../agent/channels/discord-message-ops.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UnifiedWakeOrigin, WakeOriginCodec } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
type DiscordOrigin = Extract<UnifiedWakeOrigin, {
|
|
3
|
+
platform: "discord";
|
|
4
|
+
}>;
|
|
5
|
+
export declare const discordWakeOriginCodec: WakeOriginCodec<DiscordOrigin>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=discord-origin-codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-origin-codec.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-origin-codec.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAErG,KAAK,aAAa,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AASzE,eAAO,MAAM,sBAAsB,EAAE,eAAe,CAAC,aAAa,CA8EjE,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The Discord UnifiedWakeOrigin codec — MOVED VERBATIM out of core's channels/wake-origin-codecs.ts
|
|
2
|
+
// (OPE-114 stage 4). STORED-DATA COMPATIBILITY IS THE CONTRACT: a Discord origin is JSON-serialised
|
|
3
|
+
// into the durable operational store and fail-closed re-validated on read, so every behaviour here
|
|
4
|
+
// is frozen by core's scripts/wake-origin-fixtures-test.ts (run from the app root with this bundle's
|
|
5
|
+
// boot wiring). Validator tolerances (typeof-only optional checks, extra unknown fields) are
|
|
6
|
+
// LOAD-BEARING for rows already on disk — never tighten them here.
|
|
7
|
+
//
|
|
8
|
+
// The `UnifiedWakeOrigin` TYPE union (including BOTH discord arms) stays in core — it is the wire
|
|
9
|
+
// schema for stored rows; only the codec BEHAVIOUR lives in the bundle, registered by
|
|
10
|
+
// registerDiscordProvider() (../register.ts).
|
|
11
|
+
//
|
|
12
|
+
// OPE-315 added a SECOND discord arm — a `dm: true`-discriminated DM/group-DM variant — so the
|
|
13
|
+
// validator became an XOR. Every existing GUILD row keeps validating byte for byte: the guild branch
|
|
14
|
+
// is the old predicate verbatim, and the additions are all on the new positive discriminant.
|
|
15
|
+
/** COLON-FREE by construction (a task windowId is `[^:]+` — core channels/window-key.ts): any colon
|
|
16
|
+
* in a raw surface id is folded to `_`, deterministically, so spawn + ingress agree on identity. */
|
|
17
|
+
const noColon = (s) => s.replace(/:/g, "_");
|
|
18
|
+
/** A DM-variant origin — narrowed on the positive discriminant, never on the ABSENCE of guildId. */
|
|
19
|
+
const isDmOrigin = (o) => "dm" in o && o.dm === true;
|
|
20
|
+
export const discordWakeOriginCodec = {
|
|
21
|
+
validate: (o) => {
|
|
22
|
+
// TWO valid shapes, enforced as an XOR (never both, never neither) — the Linear arm's model.
|
|
23
|
+
// • GUILD: a non-empty guildId + NO `dm`. Unchanged from the original single shape, byte for
|
|
24
|
+
// byte: guildId is REQUIRED because the Discord privacy floor
|
|
25
|
+
// (discord-gateway.ts discordSingleWindowPrivacyOk) can never have admitted a guildless
|
|
26
|
+
// surface into a shared window, and this validator enforces the same invariant on the reply
|
|
27
|
+
// path. `messageId` and `autoThreadName` stay typeof-only optionals.
|
|
28
|
+
// • DM (OPE-315): `dm === true` + NO guildId + NO autoThreadName. A DM/group-DM reply needs
|
|
29
|
+
// only the channel id (POST /channels/{id}/messages under the bot token).
|
|
30
|
+
//
|
|
31
|
+
// 🔴 THE XOR IS DELIBERATELY BIDIRECTIONAL. "dm:true AND a guildId" is INVALID — not "the guild
|
|
32
|
+
// wins", not "the dm wins". That is the IMPERSONATION direction: a DM-shaped row that acquired
|
|
33
|
+
// guild authority would reach the guild-keyed threadRef, markThreadEngaged, and `discord/<guild>`
|
|
34
|
+
// vouch keys. And the guild arm still refuses guildless BY OMISSION (D1): nothing here relaxes
|
|
35
|
+
// it, so `divertDiscordWakeToUnifiedWindow`'s `guildId: msg.guildId ?? ""` fall-through stays
|
|
36
|
+
// invalid exactly as before — pinned by the two unmodified guildless-rejection fixture rows.
|
|
37
|
+
//
|
|
38
|
+
// autoThreadName is forbidden on the DM shape (D4) because the reply leg's auto-thread branch is
|
|
39
|
+
// guild-keyed (discord-post-arm.ts writes `discord/${origin.guildId}` engagement rows); a DM
|
|
40
|
+
// carrying it would mint `discord/undefined/...`. messageId is allowed — cosmetic
|
|
41
|
+
// message_reference anchoring with no guild-shaped consequence. That is the whole line: forbid
|
|
42
|
+
// exactly the fields with guild-keyed effects.
|
|
43
|
+
//
|
|
44
|
+
// OPE-1719 adds `thread` to the GUILD shape only, and it is a LITERAL-`true` optional rather
|
|
45
|
+
// than a typeof-boolean optional: the field's whole job is to be a POSITIVE thread signal, so
|
|
46
|
+
// `thread: false` must not validate and then read as "a thread that says it is not one". It is
|
|
47
|
+
// forbidden on the DM shape because a Discord DM has no threads at all — the same "forbid
|
|
48
|
+
// exactly the fields whose effects the shape cannot carry" line `autoThreadName` sits on.
|
|
49
|
+
if (typeof o.channelId !== "string" || o.channelId.length === 0)
|
|
50
|
+
return false;
|
|
51
|
+
if (o.messageId !== undefined && typeof o.messageId !== "string")
|
|
52
|
+
return false;
|
|
53
|
+
const isDm = o.dm === true && o.guildId === undefined && o.autoThreadName === undefined && o.thread === undefined;
|
|
54
|
+
const isGuild = o.dm === undefined &&
|
|
55
|
+
typeof o.guildId === "string" &&
|
|
56
|
+
o.guildId.length > 0 &&
|
|
57
|
+
(o.autoThreadName === undefined || typeof o.autoThreadName === "string") &&
|
|
58
|
+
(o.thread === undefined || o.thread === true);
|
|
59
|
+
return isDm || isGuild;
|
|
60
|
+
},
|
|
61
|
+
// The DM namespace is `discord/dm/<channelId>` — a literal `dm` segment where a guild id sits on
|
|
62
|
+
// the guild shape. Collision-free because a Discord guild id is a decimal snowflake and can never
|
|
63
|
+
// be the string "dm". Relied on rather than enforced at the type level, so it is asserted in the
|
|
64
|
+
// fixtures oracle; if it were ever violated the blast radius is ROUTING (a shared thread-ownership
|
|
65
|
+
// row + taskWindowId prefix), not authority — a reply still goes to the stored origin.
|
|
66
|
+
threadRef: (origin) => isDmOrigin(origin)
|
|
67
|
+
? { channelKey: noColon(`discord/dm/${origin.channelId}`), threadKey: noColon(origin.channelId) }
|
|
68
|
+
: { channelKey: noColon(`discord/${origin.guildId}/${origin.channelId}`), threadKey: noColon(origin.channelId) },
|
|
69
|
+
// THE POSITIVE SIGNAL, and nothing else (OPE-1719). A Discord thread is its own channelId, so the
|
|
70
|
+
// id proves nothing; the gateway stamps `thread: true` at mint from the relay payload's channel
|
|
71
|
+
// type, and this reads exactly that stamp. Every other shape — a plain guild channel, a DM, and
|
|
72
|
+
// every stored row minted before the field existed — reads FALSE and routes to the standing seat,
|
|
73
|
+
// which is the fail-closed direction and byte-identical to the behaviour this replaced.
|
|
74
|
+
isThreadScoped: (origin) => !isDmOrigin(origin) && origin.thread === true,
|
|
75
|
+
containerId: (origin) => origin.channelId,
|
|
76
|
+
// THE PLACEMENT AXIS (OPE-1826). Discord is the SECOND surface whose post placement sits OUTSIDE
|
|
77
|
+
// its conversation identity, and until this member existed nothing said so: `threadRef` keys on the
|
|
78
|
+
// channel id alone (a Discord thread IS its own channel id), while the post arm hangs the answer on
|
|
79
|
+
// `messageId` as a `message_reference`. So two wakes in one channel are ONE conversation naming two
|
|
80
|
+
// different messages to reply under — Slack's `replyAnchorTs` shape exactly.
|
|
81
|
+
//
|
|
82
|
+
// 🔴 `messageId` ONLY, NEVER `autoThreadName`. The post arm reads both and only one is a PLACEMENT:
|
|
83
|
+
// `autoThreadName` decides whether the answer opens a NEW thread off the message, which MOVES the
|
|
84
|
+
// post out of the conversation it was asked in. The carry's own guarantee is that it can place a
|
|
85
|
+
// post and never move one, so the name is deliberately outside this axis and outside the carry
|
|
86
|
+
// below. A DM origin carries no message anchor at all — there is nothing to hang under in a 1:1 —
|
|
87
|
+
// and answers `undefined` through the same emptiness-is-absence read every other field takes.
|
|
88
|
+
placementRef: (origin) => (typeof origin.messageId === "string" && origin.messageId.length > 0 ? origin.messageId : undefined),
|
|
89
|
+
// THE CARRY (OPE-1826), narrowing-only and byte-for-byte the contract Slack's arm holds: fill a
|
|
90
|
+
// placement the target LACKS, never overwrite one, change no other field. A target that already
|
|
91
|
+
// names a message keeps it, because the model named THAT wake and answering it where it was asked
|
|
92
|
+
// is the honest reading. Core re-validates the result and re-tests the conversation, so this arm
|
|
93
|
+
// can fill a gap and cannot move a post even if this line were wrong.
|
|
94
|
+
carryPlacement: (target, source) => !(target.messageId && target.messageId.length > 0) && typeof source.messageId === "string" && source.messageId.length > 0
|
|
95
|
+
? { ...target, messageId: source.messageId }
|
|
96
|
+
: target,
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=discord-origin-codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-origin-codec.js","sourceRoot":"","sources":["../../../agent/channels/discord-origin-codec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-post-arm.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-post-arm.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,KAAK,OAAO,EAUb,MAAM,4CAA4C,CAAC;AA+JpD,eAAO,MAAM,cAAc,EAAE,OAqC5B,CAAC"}
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
import { DISCORD_MESSAGE_MAX, discordAddReaction, discordEditMessage, discordExistingThreadForMessage, discordOpenDmChannel, discordPostMessage, discordPostMessageWithButtons, discordThreadForMessage, splitForDiscord, } from "./discord-post.js";
|
|
8
|
+
import { bindFrom, markThreadEngaged, deploymentOrg, recordOwnPost, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
9
|
+
/** UNIFIED: POST /channels/{id}/messages under the bot token. The target channelId is the frozen wake
|
|
10
|
+
* origin's (host-side, never model-authored); a thread IS its own channel id so a threaded trigger
|
|
11
|
+
* replies into its thread. The FIRST chunk anchors to the triggering message via message_reference
|
|
12
|
+
* (fail-soft when the trigger was deleted); Discord caps a message at 2000 chars, so a long reply
|
|
13
|
+
* splits and posts in order. Errors re-wrapped token-free (defensive parity with the other arms —
|
|
14
|
+
* discordPostMessage's own errors are already clean, but never risk echoing a transport body).
|
|
15
|
+
* Auto-thread restore (origin.autoThreadName ⇔ an addressed mention in a base channel): open the
|
|
16
|
+
* thread off the mention and answer there — the native path's UX the divert skips.
|
|
17
|
+
* discordThreadForMessage is idempotent (finds an existing thread) and fail-open (null ⇒ today's
|
|
18
|
+
* in-channel reply, anchored to the mention via message_reference). */
|
|
19
|
+
async function liveDiscordPost(origin, text) {
|
|
20
|
+
let targetChannelId = origin.channelId;
|
|
21
|
+
let anchorToMessage = origin.messageId;
|
|
22
|
+
// The auto-thread branch is GUILD-ONLY, and now says so structurally. `!("dm" in origin)` narrows
|
|
23
|
+
// to the guild variant so `origin.guildId` type-checks below (OPE-315) — no behavioural change:
|
|
24
|
+
// the DM variant cannot carry `autoThreadName` in the first place (the type forbids it and the
|
|
25
|
+
// validator refuses it, D4), precisely because the engagement row this branch writes is keyed on
|
|
26
|
+
// the guild. The narrowing makes that unrepresentable rather than merely unreached.
|
|
27
|
+
if (!("dm" in origin) && origin.autoThreadName && origin.messageId) {
|
|
28
|
+
const threadId = await discordThreadForMessage(origin.channelId, origin.messageId, origin.autoThreadName);
|
|
29
|
+
if (threadId) {
|
|
30
|
+
targetChannelId = threadId;
|
|
31
|
+
anchorToMessage = undefined; // the thread already hangs off the mention — no reference needed
|
|
32
|
+
// ENGAGE the thread we just opened — the native path engages at dispatch, but this thread
|
|
33
|
+
// only exists NOW, so the divert path must enroll it here or non-mention follow-ups inside
|
|
34
|
+
// it are dropped at the wake gate (live-found 2026-07-19: J-DISC's continuity leg — the
|
|
35
|
+
// reply landed, then "expand that..." produced zero audit rows). The partition mirrors what
|
|
36
|
+
// the gate resolves for a message ARRIVING in the thread (workspace=guild, surface=thread id,
|
|
37
|
+
// key=thread channel id — engagementKey). markThreadEngaged is best-effort/never throws, and
|
|
38
|
+
// a later "stop" disengages normally: nothing re-engages until OE next replies here.
|
|
39
|
+
const workspaceKey = `discord/${origin.guildId}`;
|
|
40
|
+
const surfaceKey = `${workspaceKey}/${threadId}`;
|
|
41
|
+
await markThreadEngaged({ org: deploymentOrg(), platform: "discord", workspace: origin.guildId, surface: threadId, workspaceKey, surfaceKey, key: surfaceKey }, threadId, new Date().toISOString(), { participating: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let first;
|
|
45
|
+
try {
|
|
46
|
+
for (const chunk of splitForDiscord(text)) {
|
|
47
|
+
if (!chunk.trim())
|
|
48
|
+
continue; // never post an invisible empty message
|
|
49
|
+
const id = await discordPostMessage(targetChannelId, chunk, first === undefined && anchorToMessage ? { replyToMessageId: anchorToMessage } : undefined);
|
|
50
|
+
first ??= id;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`unified reply couldn't post to Discord — create-message failed on channel ${targetChannelId}`);
|
|
55
|
+
}
|
|
56
|
+
if (first === undefined) {
|
|
57
|
+
throw new Error(`unified reply couldn't post to Discord — nothing to post (empty reply) for channel ${origin.channelId}`);
|
|
58
|
+
}
|
|
59
|
+
// 🔴 OPE-793 — THE ONE PLACE AN ARM POSTS SOMEWHERE ITS SPINE CANNOT NAME. When the auto-thread
|
|
60
|
+
// branch above fires, this reply lands in a thread that did not exist when the wake origin was
|
|
61
|
+
// frozen, so the spine's own ledger write records `origin.channelId` — the BASE channel — and an
|
|
62
|
+
// `edit_reply` addressed at the thread would be refused as not-ours. Recording the real channel
|
|
63
|
+
// here closes that, and only that: the id is one this call just posted, at the locus it posted to.
|
|
64
|
+
// Best-effort by construction (recordOwnPost never throws); a lost row costs an edit, never a post.
|
|
65
|
+
if (targetChannelId !== origin.channelId) {
|
|
66
|
+
await recordOwnPost(deploymentOrg(), { platform: "discord", channelId: targetChannelId }, first);
|
|
67
|
+
}
|
|
68
|
+
return first;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* UNIFIED, WITH A COMPONENT PLANE (OPE-1720): the same single-window post as {@link liveDiscordPost},
|
|
72
|
+
* carrying a row of tappable actions under it. Discord is the SECOND surface to implement this
|
|
73
|
+
* member, and it may only do so because the tap lands: `discord-front.ts` classifies the `oe_input:`
|
|
74
|
+
* custom_id and `discord-approval-card.ts` settles it.
|
|
75
|
+
*
|
|
76
|
+
* 🔴 NO AUTO-THREAD BRANCH, DELIBERATELY. `liveDiscordPost` may open a thread off the mention and
|
|
77
|
+
* answer there. A governed approval card must not move: the people who can answer it are watching the
|
|
78
|
+
* conversation it was asked in, and burying the buttons in a freshly-created thread is how a card
|
|
79
|
+
* goes unanswered. The anchor still rides — the card hangs under the triggering message through the
|
|
80
|
+
* same `message_reference` a plain reply uses — so it is placed and never moved.
|
|
81
|
+
*
|
|
82
|
+
* 🔴 IT FALLS BACK TO TEXT RATHER THAN SPLITTING. Discord caps a message at 2000 characters and a
|
|
83
|
+
* button row cannot be split across messages: a row attached to the wrong part would sit under a
|
|
84
|
+
* fragment. So an over-cap card posts through the ordinary splitting path and loses the SHORTCUT,
|
|
85
|
+
* never the meaning — `DirectMessageAction`'s own rule, which the question bridge's text already
|
|
86
|
+
* satisfies (it prints the prompt, the options, and the line saying a reply may name one).
|
|
87
|
+
*/
|
|
88
|
+
async function liveDiscordPostWithActions(origin, text, actions) {
|
|
89
|
+
const buttons = actions.map((a) => ({ customId: a.id, label: a.label, intent: a.intent }));
|
|
90
|
+
if (buttons.length === 0 || text.length > DISCORD_MESSAGE_MAX)
|
|
91
|
+
return liveDiscordPost(origin, text);
|
|
92
|
+
try {
|
|
93
|
+
return await discordPostMessageWithButtons(origin.channelId, text, buttons, origin.messageId ? { replyToMessageId: origin.messageId } : undefined);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error(`unified reply couldn't post to Discord — create-message with buttons failed on channel ${origin.channelId}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** EDIT (OPE-793): `PATCH /channels/{id}/messages/{mid}` through the same write seam the post path
|
|
100
|
+
* uses. Ownership was already decided upstream against the own-post ledger; this is transport only. */
|
|
101
|
+
async function editDiscordMessage(locus, messageId, text) {
|
|
102
|
+
await discordEditMessage(locus.channelId, messageId, text);
|
|
103
|
+
}
|
|
104
|
+
/** EDIT ADDRESS (OPE-798): the other half of the auto-thread branch in {@link liveDiscordPost}.
|
|
105
|
+
*
|
|
106
|
+
* When that branch fires the reply lands in a thread — and a Discord thread IS its own channel —
|
|
107
|
+
* while the frozen wake origin still names the BASE channel. The arm already records an own-post
|
|
108
|
+
* ledger row on the thread so the fence passes; without this the PATCH still went to the base
|
|
109
|
+
* channel and Discord answered 404 (measured live, deploy #89). Here the edit path resolves the
|
|
110
|
+
* SAME thread the post path used, so the ledger read and the PATCH address one conversation.
|
|
111
|
+
*
|
|
112
|
+
* 🔴 READ-ONLY: `discordExistingThreadForMessage`, never `discordThreadForMessage`. The post path
|
|
113
|
+
* may CREATE the thread; the edit path may not. A create at edit time would open a new empty thread
|
|
114
|
+
* — a visible message in the room, and still a 404 for a message that is not in it.
|
|
115
|
+
*
|
|
116
|
+
* Fail-soft to the base locus on every unknown: a DM origin, a wake with no auto-thread name, a
|
|
117
|
+
* message with no thread, an API failure. That degrades to today's behaviour, which is correct for
|
|
118
|
+
* a reply that genuinely went to the base channel. */
|
|
119
|
+
async function retargetDiscordEditLocus(origin, locus) {
|
|
120
|
+
const o = origin;
|
|
121
|
+
if ("dm" in o || !o.autoThreadName || !o.messageId)
|
|
122
|
+
return locus;
|
|
123
|
+
const threadId = await discordExistingThreadForMessage(o.channelId, o.messageId);
|
|
124
|
+
return threadId ? { ...locus, channelId: threadId } : locus;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* DM (OPE-623): open the 1:1 conversation with a Discord user and post there. The SECOND provider to
|
|
128
|
+
* implement this member — Slack was the only one, and several core behaviours read the registry
|
|
129
|
+
* rather than a platform list precisely so that they tighten the day a second arm appears:
|
|
130
|
+
*
|
|
131
|
+
* • `link_identity` can now offer the DIRECTORY transport on Discord (it needs a directory arm AND
|
|
132
|
+
* a DM arm; channels/discord-directory.ts is the other half).
|
|
133
|
+
* • `claimVenueRefusal` (core identity/link-flow.ts) now REFUSES a `link <code>` claim typed in a
|
|
134
|
+
* Discord channel and points the person at the DM. A code is a bearer secret for its whole TTL,
|
|
135
|
+
* so it must not be posted where a room can read it. That is a deliberate tightening, not a
|
|
136
|
+
* regression: it only arms where a private door exists.
|
|
137
|
+
* • link-set change notices (`notifyLinkSetMembers`) and the governance DM-delivery venue
|
|
138
|
+
* (core access/pane-venue.ts) reach Discord accounts instead of skipping them.
|
|
139
|
+
*
|
|
140
|
+
* The caller names WHO, never WHERE — opening the conversation is this arm's business. `text` goes
|
|
141
|
+
* through the same 2000-char splitter every other Discord post uses, so a long notice arrives whole
|
|
142
|
+
* rather than 400-ing; the FIRST part's id is the returned anchor.
|
|
143
|
+
*/
|
|
144
|
+
async function deliverDiscordDirectMessage(input) {
|
|
145
|
+
const surfaceId = await discordOpenDmChannel(input.principalId);
|
|
146
|
+
// BUTTONS (OPE-622): Discord has a component plane, so a caller's `actions` render as a row under
|
|
147
|
+
// the text and a tap comes back to the interactions endpoint (channels/discord-front.ts). The
|
|
148
|
+
// button path cannot split an over-cap message, so a long text falls back to the plain splitting
|
|
149
|
+
// post rather than failing — the caller's copy always stands on its own without a button.
|
|
150
|
+
const buttons = (input.actions ?? []).map((a) => ({ customId: a.id, label: a.label, intent: a.intent }));
|
|
151
|
+
if (buttons.length > 0 && input.text.length <= DISCORD_MESSAGE_MAX) {
|
|
152
|
+
return { surfaceId, messageId: await discordPostMessageWithButtons(surfaceId, input.text, buttons) };
|
|
153
|
+
}
|
|
154
|
+
const messageId = await discordPostMessage(surfaceId, input.text);
|
|
155
|
+
return { surfaceId, messageId };
|
|
156
|
+
}
|
|
157
|
+
export const discordPostArm = {
|
|
158
|
+
platform: "discord",
|
|
159
|
+
resolveTarget: (bag) => {
|
|
160
|
+
// On Discord the surface channel already IS the thread — no separate thread ts.
|
|
161
|
+
const channelId = bag.str("channelId");
|
|
162
|
+
return channelId ? { platform: "discord", channelId } : null;
|
|
163
|
+
},
|
|
164
|
+
dispatchReply: (target, text) => discordPostMessage(target.channelId, text),
|
|
165
|
+
dispatchUnified: (origin, text, deps) => (deps.postDiscord ?? liveDiscordPost)(origin, text),
|
|
166
|
+
// OPE-1720: Discord declares the unified component plane because the tap route exists on it —
|
|
167
|
+
// `discord-front.ts` classifies the `oe_input:` custom_id and `discord-approval-card.ts` settles
|
|
168
|
+
// it. The injected `postDiscord` override is honoured VERBATIM and drops the buttons: a test that
|
|
169
|
+
// scripted a text-only post must not start receiving a card it never asked for.
|
|
170
|
+
dispatchUnifiedActions: (origin, text, actions, deps) => deps.postDiscord ? deps.postDiscord(origin, text) : liveDiscordPostWithActions(origin, text, actions),
|
|
171
|
+
describeRedriveTarget: (origin) => `discord channel ${origin.channelId}`,
|
|
172
|
+
redriveContainerId: (origin) => origin.channelId,
|
|
173
|
+
editMessage: editDiscordMessage,
|
|
174
|
+
retargetEditLocus: retargetDiscordEditLocus,
|
|
175
|
+
deliverDirectMessage: deliverDiscordDirectMessage,
|
|
176
|
+
// REACT (OPE-987): the message-level (channelId, messageId) pair `setReaction` already takes for
|
|
177
|
+
// the host-placed ✋ glyph — closer to Slack's per-message granularity than to GitHub/Linear's
|
|
178
|
+
// conversation-anchor one (post-arms.ts `ReactTarget`'s header explains why the union carries both
|
|
179
|
+
// shapes). `channelId` comes off the binding's post-back target, same as `resolveTarget` above;
|
|
180
|
+
// `messageId` is NOT on that target (Discord threads reuse `channelId` for their own identity, so
|
|
181
|
+
// the triggering message rides its own `anchorMessageId` field — identity/binding.ts, OPE-671) —
|
|
182
|
+
// read via `bindFrom(bag.ctx)` rather than widening the bag's target shape for one extra field.
|
|
183
|
+
resolveReactTarget: (bag) => {
|
|
184
|
+
const channelId = bag.str("channelId");
|
|
185
|
+
if (!channelId)
|
|
186
|
+
return null;
|
|
187
|
+
const messageId = bindFrom(bag.ctx).anchorMessageId;
|
|
188
|
+
return messageId ? { platform: "discord", channelId, messageId } : null;
|
|
189
|
+
},
|
|
190
|
+
react: (target, emoji) => {
|
|
191
|
+
const t = target;
|
|
192
|
+
return discordAddReaction(t.channelId, t.messageId, emoji);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=discord-post-arm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-post-arm.js","sourceRoot":"","sources":["../../../agent/channels/discord-post-arm.ts"],"names":[],"mappings":"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"}
|