@open-everywhere/provider-discord 0.1.5 → 0.3.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/NOTICE +9 -9
- package/README.md +1 -1
- package/agent/approvals/discord-card.ts +11 -12
- package/agent/approvals/discord-flow.ts +4 -3
- package/agent/channels/discord-dispatch-target.ts +2 -2
- package/agent/channels/discord-front.ts +10 -52
- package/agent/channels/discord-gateway.ts +6 -3
- package/agent/channels/discord-post-arm.ts +0 -34
- package/agent/channels/discord-post.ts +1 -9
- package/agent/channels/discord.ts +4 -1
- package/agent/commands/discord-commands.ts +23 -9
- package/agent/contributions.ts +1 -1
- package/agent/surface-parity.ts +0 -6
- package/dist/agent/approvals/discord-card.d.ts.map +1 -1
- package/dist/agent/approvals/discord-card.js +11 -12
- package/dist/agent/approvals/discord-card.js.map +1 -1
- package/dist/agent/approvals/discord-flow.d.ts +1 -1
- package/dist/agent/approvals/discord-flow.d.ts.map +1 -1
- package/dist/agent/approvals/discord-flow.js +3 -2
- package/dist/agent/approvals/discord-flow.js.map +1 -1
- package/dist/agent/channels/discord-dispatch-target.js +2 -2
- package/dist/agent/channels/discord-dispatch-target.js.map +1 -1
- package/dist/agent/channels/discord-front.d.ts +0 -18
- package/dist/agent/channels/discord-front.d.ts.map +1 -1
- package/dist/agent/channels/discord-front.js +10 -37
- package/dist/agent/channels/discord-front.js.map +1 -1
- package/dist/agent/channels/discord-gateway.d.ts.map +1 -1
- package/dist/agent/channels/discord-gateway.js +6 -3
- package/dist/agent/channels/discord-gateway.js.map +1 -1
- package/dist/agent/channels/discord-post-arm.d.ts.map +1 -1
- package/dist/agent/channels/discord-post-arm.js +0 -34
- package/dist/agent/channels/discord-post-arm.js.map +1 -1
- package/dist/agent/channels/discord-post.d.ts +2 -10
- package/dist/agent/channels/discord-post.d.ts.map +1 -1
- package/dist/agent/channels/discord-post.js +2 -9
- package/dist/agent/channels/discord-post.js.map +1 -1
- package/dist/agent/channels/discord.js +4 -1
- package/dist/agent/channels/discord.js.map +1 -1
- package/dist/agent/commands/discord-commands.d.ts +2 -0
- package/dist/agent/commands/discord-commands.d.ts.map +1 -1
- package/dist/agent/commands/discord-commands.js +19 -9
- package/dist/agent/commands/discord-commands.js.map +1 -1
- package/dist/agent/contributions.d.ts +1 -1
- package/dist/agent/contributions.d.ts.map +1 -1
- package/dist/agent/contributions.js +1 -1
- package/dist/agent/contributions.js.map +1 -1
- package/dist/agent/surface-parity.d.ts.map +1 -1
- package/dist/agent/surface-parity.js +0 -6
- package/dist/agent/surface-parity.js.map +1 -1
- package/package.json +3 -7
- package/agent/channels/discord-approval-card.ts +0 -373
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// application command" and dispatches a full agent turn. "/oe" is deliberately intercepted
|
|
25
25
|
// at the FRONT (before onCommand ever sees it) so it costs zero inference — see
|
|
26
26
|
// channels/discord-front.ts's classifyDiscordInteraction.
|
|
27
|
-
import { bindFrom, MODEL_AB_RATING_MAX, MODEL_AB_RATING_MIN,
|
|
27
|
+
import { bindFrom, MODEL_AB_RATING_MAX, MODEL_AB_RATING_MIN, agentDisplayName, oeCommandName, oeSlash, runOeCommand, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
28
28
|
import { buildDiscordAuth } from "../channels/discord-identity.js";
|
|
29
29
|
// Discord interaction-response wire constants, defined locally (same "leaf-over-import" call
|
|
30
30
|
// approvals/discord-flow.ts and approvals/discord-card.ts make for their own copies): importing
|
|
@@ -103,7 +103,7 @@ export function parseDiscordOeCommandOptions(rawOptions) {
|
|
|
103
103
|
if (score === undefined || !Number.isInteger(score) || score < MODEL_AB_RATING_MIN || score > MODEL_AB_RATING_MAX) {
|
|
104
104
|
return {
|
|
105
105
|
kind: "error",
|
|
106
|
-
message: `"
|
|
106
|
+
message: `"${oeSlash("rate")}" needs a whole "score" from ${MODEL_AB_RATING_MIN} to ${MODEL_AB_RATING_MAX} (got ${JSON.stringify(scoreOpt?.value)}).`,
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
return { kind: "rate", score };
|
|
@@ -121,14 +121,14 @@ export function parseDiscordOeCommandOptions(rawOptions) {
|
|
|
121
121
|
if (usd === undefined || usd <= 0) {
|
|
122
122
|
return {
|
|
123
123
|
kind: "error",
|
|
124
|
-
message: `"
|
|
124
|
+
message: `"${oeSlash("budget raise")}" needs a positive "usd" amount (got ${JSON.stringify(usdOpt?.value)}).`,
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
return { kind: "budget-raise", usd };
|
|
128
128
|
}
|
|
129
129
|
return { kind: "error", message: `Unknown "budget" subcommand "${subName}".` };
|
|
130
130
|
}
|
|
131
|
-
return { kind: "error", message: `Unknown "
|
|
131
|
+
return { kind: "error", message: `Unknown "${oeSlash()}" subcommand "${name ?? "?"}" — try "${oeSlash("help")}".` };
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* Parse a raw (already Ed25519-verified) Discord interaction body into the auth + command a
|
|
@@ -142,7 +142,7 @@ export function parseOeCommandInteraction(raw) {
|
|
|
142
142
|
if (!isObject(raw) || raw["type"] !== 2)
|
|
143
143
|
return null;
|
|
144
144
|
const data = isObject(raw["data"]) ? raw["data"] : undefined;
|
|
145
|
-
if (!data || data["name"] !==
|
|
145
|
+
if (!data || data["name"] !== oeCommandName())
|
|
146
146
|
return null;
|
|
147
147
|
const member = isObject(raw["member"]) ? raw["member"] : undefined;
|
|
148
148
|
const memberUser = member && isObject(member["user"]) ? member["user"] : undefined;
|
|
@@ -190,7 +190,7 @@ export async function handleDiscordOeCommandInteraction(raw, attachSession) {
|
|
|
190
190
|
// (e.g. "propose from a DM needs an explicit scope") — surface it verbatim; anything else
|
|
191
191
|
// (a genuinely unexpected store/network failure) gets a generic fail-soft note. Either way,
|
|
192
192
|
// never throw out of this function.
|
|
193
|
-
console.error(`[open-everywhere:discord-commands] "
|
|
193
|
+
console.error(`[open-everywhere:discord-commands] "${oeSlash()}" command failed softly: ${String(err)}`);
|
|
194
194
|
return ephemeral(`Couldn't complete that: ${err instanceof Error ? err.message : String(err)}`);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -202,6 +202,14 @@ const SUB_COMMAND_GROUP = 2;
|
|
|
202
202
|
const OPTION_INTEGER = 4;
|
|
203
203
|
const OPTION_NUMBER = 10;
|
|
204
204
|
const CHAT_INPUT = 1;
|
|
205
|
+
/** Discord's limit on a command description. A longer one fails the registration call. */
|
|
206
|
+
export const DISCORD_COMMAND_DESCRIPTION_MAX = 100;
|
|
207
|
+
/** The command's description, naming the deployment — or, when the name would push it past
|
|
208
|
+
* Discord's limit, the same sentence without the name. */
|
|
209
|
+
function discordCommandDescription(name) {
|
|
210
|
+
const named = `${name} control-plane commands (status, budget) — no model turn.`;
|
|
211
|
+
return named.length <= DISCORD_COMMAND_DESCRIPTION_MAX ? named : "Control-plane commands (status, budget) — no model turn.";
|
|
212
|
+
}
|
|
205
213
|
/**
|
|
206
214
|
* The Discord ApplicationCommand body for "/oe" — pure, no network, so the registration
|
|
207
215
|
* script (scripts/register-discord-commands.ts) and its test share exactly one definition.
|
|
@@ -210,9 +218,11 @@ const CHAT_INPUT = 1;
|
|
|
210
218
|
*/
|
|
211
219
|
export function buildOeDiscordCommandDefinition() {
|
|
212
220
|
return {
|
|
213
|
-
|
|
221
|
+
// Both from the `identity.*` settings (OPE-2334): the name the router below matches on, and the
|
|
222
|
+
// deployment's configured name in the description.
|
|
223
|
+
name: oeCommandName(),
|
|
214
224
|
type: CHAT_INPUT,
|
|
215
|
-
description:
|
|
225
|
+
description: discordCommandDescription(agentDisplayName()),
|
|
216
226
|
options: [
|
|
217
227
|
{
|
|
218
228
|
name: "budget",
|
|
@@ -259,7 +269,7 @@ export function buildOeDiscordCommandDefinition() {
|
|
|
259
269
|
},
|
|
260
270
|
],
|
|
261
271
|
},
|
|
262
|
-
{ name: "help", description:
|
|
272
|
+
{ name: "help", description: `List the ${oeSlash()} commands.`, type: SUB_COMMAND },
|
|
263
273
|
],
|
|
264
274
|
};
|
|
265
275
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discord-commands.js","sourceRoot":"","sources":["../../../agent/commands/discord-commands.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,mFAAmF;AACnF,yDAAyD;AACzD,EAAE;AACF,wFAAwF;AACxF,2FAA2F;AAC3F,kGAAkG;AAClG,+FAA+F;AAC/F,wEAAwE;AACxE,4EAA4E;AAC5E,wFAAwF;AACxF,8FAA8F;AAC9F,yFAAyF;AACzF,+FAA+F;AAC/F,8FAA8F;AAC9F,wFAAwF;AACxF,+FAA+F;AAC/F,6FAA6F;AAC7F,6FAA6F;AAC7F,2EAA2E;AAC3E,EAAE;AACF,2FAA2F;AAC3F,oFAAoF;AACpF,2FAA2F;AAC3F,gFAAgF;AAChF,0DAA0D;AAE1D,OAAO,EACL,QAAQ,EACR,mBAAmB,EACnB,mBAAmB,EACnB,
|
|
1
|
+
{"version":3,"file":"discord-commands.js","sourceRoot":"","sources":["../../../agent/commands/discord-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ export declare const CONTRIBUTIONS: {
|
|
|
6
6
|
readonly kind: "channel";
|
|
7
7
|
readonly name: "discord";
|
|
8
8
|
readonly module: "agent/channels/discord.js";
|
|
9
|
-
readonly summary: "The Discord surface. Wires Discord's HTTP Interactions (slash commands) → an
|
|
9
|
+
readonly summary: "The Discord surface. Wires Discord's HTTP Interactions (slash commands) → an agent session, and — the point of the \"Discord-binding\" slice — populates the session auth context with the REAL…";
|
|
10
10
|
}, {
|
|
11
11
|
readonly kind: "channel";
|
|
12
12
|
readonly name: "discord-gateway";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../agent/contributions.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,aAAa;aACxB,aAAa,EAAE,CAAC;aAChB,OAAO,EAAE,mCAAmC;aAC5C,QAAQ,EAAE,2CAA2C;aACrD,aAAa;iBACT,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,SAAS;iBAAE,MAAM,EAAE,2BAA2B;iBAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../agent/contributions.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,aAAa;aACxB,aAAa,EAAE,CAAC;aAChB,OAAO,EAAE,mCAAmC;aAC5C,QAAQ,EAAE,2CAA2C;aACrD,aAAa;iBACT,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,SAAS;iBAAE,MAAM,EAAE,2BAA2B;iBAAE,OAAO,EAAE,kMAAkM;;iBAClR,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,iBAAiB;iBAAE,MAAM,EAAE,mCAAmC;iBAAE,OAAO,EAAE,qFAAqF;;CAEjL,CAAC"}
|
|
@@ -24,7 +24,7 @@ export const CONTRIBUTIONS = {
|
|
|
24
24
|
package: "@open-everywhere/provider-discord",
|
|
25
25
|
register: "agent/register.js#registerDiscordProvider",
|
|
26
26
|
contributions: [
|
|
27
|
-
{ kind: "channel", name: "discord", module: "agent/channels/discord.js", summary: "The Discord surface. Wires Discord's HTTP Interactions (slash commands) → an
|
|
27
|
+
{ kind: "channel", name: "discord", module: "agent/channels/discord.js", summary: "The Discord surface. Wires Discord's HTTP Interactions (slash commands) → an agent session, and — the point of the \"Discord-binding\" slice — populates the session auth context with the REAL…" },
|
|
28
28
|
{ kind: "channel", name: "discord-gateway", module: "agent/channels/discord-gateway.js", summary: "The Discord GATEWAY ingestion surface — Milestone 2 of the Discord Gateway feature." },
|
|
29
29
|
],
|
|
30
30
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contributions.js","sourceRoot":"","sources":["../../agent/contributions.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,2FAA2F;AAC3F,4FAA4F;AAC5F,EAAE;AACF,+FAA+F;AAC/F,oGAAoG;AACpG,qGAAqG;AACrG,iGAAiG;AACjG,iGAAiG;AACjG,mGAAmG;AACnG,iCAAiC;AACjC,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,6FAA6F;AAC7F,qGAAqG;AACrG,kGAAkG;AAClG,0DAA0D;AAC1D,uEAAuE;AACvE,EAAE;AACF,+FAA+F;AAE/F,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,aAAa,EAAE,CAAC;IAChB,OAAO,EAAE,mCAAmC;IAC5C,QAAQ,EAAE,2CAA2C;IACrD,aAAa,EAAE;QACb,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,2BAA2B,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"contributions.js","sourceRoot":"","sources":["../../agent/contributions.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,2FAA2F;AAC3F,4FAA4F;AAC5F,EAAE;AACF,+FAA+F;AAC/F,oGAAoG;AACpG,qGAAqG;AACrG,iGAAiG;AACjG,iGAAiG;AACjG,mGAAmG;AACnG,iCAAiC;AACjC,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,6FAA6F;AAC7F,qGAAqG;AACrG,kGAAkG;AAClG,0DAA0D;AAC1D,uEAAuE;AACvE,EAAE;AACF,+FAA+F;AAE/F,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,aAAa,EAAE,CAAC;IAChB,OAAO,EAAE,mCAAmC;IAC5C,QAAQ,EAAE,2CAA2C;IACrD,aAAa,EAAE;QACb,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,2BAA2B,EAAE,OAAO,EAAE,kMAAkM,EAAE;QACtR,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,mCAAmC,EAAE,OAAO,EAAE,qFAAqF,EAAE;KAC1L;CACO,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface-parity.d.ts","sourceRoot":"","sources":["../../agent/surface-parity.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAE3F,eAAO,MAAM,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"surface-parity.d.ts","sourceRoot":"","sources":["../../agent/surface-parity.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAE3F,eAAO,MAAM,sBAAsB,EAAE,wBA+EpC,CAAC"}
|
|
@@ -10,12 +10,6 @@ export const DISCORD_SURFACE_PARITY = {
|
|
|
10
10
|
"capability-manifest": "supported",
|
|
11
11
|
"post-arm": "supported",
|
|
12
12
|
"wake-origin-codec": "supported",
|
|
13
|
-
// The arm renders a Discord action row (`discord-post-arm.ts`), the front routes the tap back
|
|
14
|
-
// (`discord-front.ts` classifies the `oe_input:` custom_id, `discord-approval-card.ts` settles it),
|
|
15
|
-
// and the settle DELIVERS through core's `approvalCardRespondVia` — all three halves, as the row
|
|
16
|
-
// requires. The third is not a formality: this front bound its own `args.from(bareToken)` until
|
|
17
|
-
// OPE-1863, and that address is channel-local, so every tap threw and the approval stayed parked.
|
|
18
|
-
"unified-action-plane": "supported",
|
|
19
13
|
// `messageId` is the `message_reference` anchor and sits outside `threadRef`, which keys on the
|
|
20
14
|
// channel id alone — Slack's shape exactly. Both members are implemented; `autoThreadName` stays
|
|
21
15
|
// OUT of the axis because opening a thread MOVES a post rather than placing one.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface-parity.js","sourceRoot":"","sources":["../../agent/surface-parity.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,wFAAwF;AACxF,EAAE;AACF,yFAAyF;AACzF,iGAAiG;AACjG,uFAAuF;AAIvF,MAAM,CAAC,MAAM,sBAAsB,GAA6B;IAC9D,4BAA4B;IAC5B,iBAAiB,EAAE,WAAW;IAC9B,qBAAqB,EAAE,WAAW;IAClC,UAAU,EAAE,WAAW;IACvB,mBAAmB,EAAE,WAAW;IAChC,
|
|
1
|
+
{"version":3,"file":"surface-parity.js","sourceRoot":"","sources":["../../agent/surface-parity.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,wFAAwF;AACxF,EAAE;AACF,yFAAyF;AACzF,iGAAiG;AACjG,uFAAuF;AAIvF,MAAM,CAAC,MAAM,sBAAsB,GAA6B;IAC9D,4BAA4B;IAC5B,iBAAiB,EAAE,WAAW;IAC9B,qBAAqB,EAAE,WAAW;IAClC,UAAU,EAAE,WAAW;IACvB,mBAAmB,EAAE,WAAW;IAChC,gGAAgG;IAChG,iGAAiG;IACjG,iFAAiF;IACjF,wBAAwB,EAAE,WAAW;IACrC,cAAc,EAAE,WAAW;IAC3B,0BAA0B,EAAE,WAAW;IACvC,eAAe,EAAE,WAAW;IAC5B,+FAA+F;IAC/F,8FAA8F;IAC9F,+CAA+C;IAC/C,yBAAyB,EAAE,EAAE,SAAS,EAAE,sEAAsE,EAAE;IAChH,iGAAiG;IACjG,iGAAiG;IACjG,kGAAkG;IAClG,6EAA6E;IAC7E,iGAAiG;IACjG,uFAAuF;IACvF,uBAAuB,EAAE;QACvB,WAAW,EACT,yMAAyM;KAC5M;IACD,8FAA8F;IAC9F,4FAA4F;IAC5F,+FAA+F;IAC/F,yFAAyF;IACzF,sBAAsB,EAAE,EAAE,SAAS,EAAE,8FAA8F,EAAE;IAErI,oCAAoC;IACpC,eAAe,EAAE,WAAW;IAC5B,aAAa,EAAE,WAAW;IAC1B,iGAAiG;IACjG,mGAAmG;IACnG,oGAAoG;IACpG,eAAe,EAAE,WAAW;IAC5B,+FAA+F;IAC/F,qEAAqE;IACrE,wBAAwB,EAAE,WAAW;IACrC,iGAAiG;IACjG,wEAAwE;IACxE,eAAe,EAAE,WAAW;IAC5B,iGAAiG;IACjG,iGAAiG;IACjG,4BAA4B;IAC5B,mBAAmB,EAAE,WAAW;IAChC,mBAAmB,EAAE,WAAW;IAChC,qFAAqF;IACrF,iGAAiG;IACjG,iGAAiG;IACjG,gEAAgE;IAChE,mBAAmB,EAAE,EAAE,WAAW,EAAE,2GAA2G,EAAE;IACjJ,sFAAsF;IACtF,OAAO,EAAE,EAAE,SAAS,EAAE,8BAA8B,EAAE;IACtD,KAAK,EAAE,WAAW;IAClB,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,WAAW;IAEjB,oBAAoB;IACpB,KAAK,EAAE,WAAW;IAClB,UAAU,EAAE,WAAW;IACvB,8FAA8F;IAC9F,KAAK,EAAE,WAAW;IAClB,QAAQ,EAAE,WAAW;IACrB,YAAY,EAAE,WAAW;IACzB,WAAW,EAAE,WAAW;IACxB,MAAM,EAAE,EAAE,WAAW,EAAE,8EAA8E,EAAE;IACvG,YAAY,EAAE,WAAW;IACzB,SAAS,EAAE,WAAW;IACtB,eAAe,EAAE,WAAW;IAC5B,IAAI,EAAE,EAAE,SAAS,EAAE,yCAAyC,EAAE;IAC9D,cAAc,EAAE,WAAW;IAC3B,eAAe,EAAE,WAAW;IAC5B,UAAU,EAAE,EAAE,SAAS,EAAE,yEAAyE,EAAE;IACpG,SAAS,EAAE,WAAW;CACvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-everywhere/provider-discord",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Open Everywhere Discord provider — Discord channel and DM surfaces, gateway wiring, identity mapping and approval cards.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -56,10 +56,6 @@
|
|
|
56
56
|
"types": "./dist/agent/approvals/discord-scope-label.d.ts",
|
|
57
57
|
"default": "./dist/agent/approvals/discord-scope-label.js"
|
|
58
58
|
},
|
|
59
|
-
"./agent/channels/discord-approval-card.js": {
|
|
60
|
-
"types": "./dist/agent/channels/discord-approval-card.d.ts",
|
|
61
|
-
"default": "./dist/agent/channels/discord-approval-card.js"
|
|
62
|
-
},
|
|
63
59
|
"./agent/channels/discord-backscroll.js": {
|
|
64
60
|
"types": "./dist/agent/channels/discord-backscroll.d.ts",
|
|
65
61
|
"default": "./dist/agent/channels/discord-backscroll.js"
|
|
@@ -172,11 +168,11 @@
|
|
|
172
168
|
"test": "echo 'provider-discord: its test suite runs at the APP TIER (root scripts/) because the discord surface composes deeply with core internals (a custom gateway, admin approval cards, guild-admin bypass, routine dispatch) — see the root test chain (test:discord*, test:scenarios*, test:oe-commands, and the read/dispatch/audit/wake integration tests). This keeps the core adapter-api barrel a small port surface rather than exporting test-only internals.'"
|
|
173
169
|
},
|
|
174
170
|
"dependencies": {
|
|
175
|
-
"@open-everywhere/core": "0.
|
|
171
|
+
"@open-everywhere/core": "0.3.0",
|
|
176
172
|
"zod": "4.5.4"
|
|
177
173
|
},
|
|
178
174
|
"peerDependencies": {
|
|
179
|
-
"eve": "0.
|
|
175
|
+
"eve": "0.71.0"
|
|
180
176
|
},
|
|
181
177
|
"devDependencies": {
|
|
182
178
|
"@types/node": "latest",
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
// THE DISCORD RETURN LEG FOR A BRIDGED APPROVAL CARD (OPE-1720) — what makes the button the unified
|
|
2
|
-
// post arm now renders actually settle the approval it names. The sibling of
|
|
3
|
-
// providers/slack/agent/channels/slack-approval-card.ts, funnelling through the SAME core decision
|
|
4
|
-
// (`resolveApprovalCardClick`) exactly as access/discord-grant-click.ts funnels through
|
|
5
|
-
// `resolveGrantCardClick`. Core decides; this file reads Discord's envelope and writes Discord's
|
|
6
|
-
// answer.
|
|
7
|
-
//
|
|
8
|
-
// WHY THIS EXISTS RATHER THAN EVE'S OWN HITL ROUTE — the Slack reasoning verbatim, and it is
|
|
9
|
-
// structural rather than a not-yet. eve's Discord channel resolves its own `eve_input:` component
|
|
10
|
-
// clicks against the DISCORD channel's state and session address. A DM seat's parked request lives on
|
|
11
|
-
// the UNIFIED window — another channel, another session, another state bag — so a click on a card OE
|
|
12
|
-
// posted there has nothing for eve's route to resolve. It is OE's card, so it is OE's click.
|
|
13
|
-
//
|
|
14
|
-
// 🔴 THE SAME ADMISSION, THOUGH, LEG B INCLUDED. `admitInputResponse` is the ONE decision both HITL
|
|
15
|
-
// answer doors take, and `ingressSpendGate` with door `"answer"` is the same spend floor, consulted
|
|
16
|
-
// FIRST for the same reason: a tap RESUMES a parked turn, which is a decision to spend more on a scope
|
|
17
|
-
// nobody asked whether it could afford. Leg B — the positive per-answerer guest verdict Slack supplies
|
|
18
|
-
// with `slackClickerIsGuest` — rides Discord's own envelope instead of a probe: `member.pending` is
|
|
19
|
-
// this platform's analogue of an unverified account, and the same bundle already reads it for the
|
|
20
|
-
// inbound trust grain. A DM tap carries no member record, so the leg is INERT there rather than a
|
|
21
|
-
// refusal: "we could not ask" is never a no.
|
|
22
|
-
//
|
|
23
|
-
// 🔴 A REFUSAL LEAVES THE CARD LIVE. Nothing here consumes a row on any outcome but a delivered
|
|
24
|
-
// answer: the approval stays parked, the card stays tappable for somebody who may answer it, and the
|
|
25
|
-
// question stays answerable IN WORDS through the text leg (`question-bridge.ts`), which is untouched.
|
|
26
|
-
//
|
|
27
|
-
// 🔴 ACK, THEN SETTLE, THEN EDIT — and Discord is the surface where that ordering is forced (the
|
|
28
|
-
// OPE-1282 finding this door inherits). A settle calls `respond` into eve, which resumes a parked
|
|
29
|
-
// turn; Discord does NOT redeliver an interaction it has not seen answered, and an expired token
|
|
30
|
-
// leaves the tapper on a red "This interaction failed" while the approval really did settle. So this
|
|
31
|
-
// door answers with a DEFERRED EPHEMERAL ack (`type 5`), hands the settle to the route's `waitUntil`,
|
|
32
|
-
// and edits that placeholder with the outcome. The ephemeral flag rides the ACK, because Discord
|
|
33
|
-
// fixes a response's visibility at the first response.
|
|
34
|
-
//
|
|
35
|
-
// 🔴 NO `waitUntil` ⇒ NO DEFER. Without somewhere to run the work a deferred ack would leave the
|
|
36
|
-
// tapper on a permanent "thinking…", which is strictly worse than a slow answer. The handler awaits
|
|
37
|
-
// and answers `type 4` instead, and says so in the log.
|
|
38
|
-
//
|
|
39
|
-
// NEVER THROWS. This endpoint answers a person who is waiting within three seconds, and an unhandled
|
|
40
|
-
// throw becomes a red "interaction failed" they cannot act on.
|
|
41
|
-
|
|
42
|
-
import {
|
|
43
|
-
admitInputResponse,
|
|
44
|
-
APPROVAL_BUTTON_LABELS,
|
|
45
|
-
APPROVAL_CARD_REFUSED_AUDIT_ACTION as CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION,
|
|
46
|
-
APPROVAL_CARD_SETTLED_AUDIT_ACTION as CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION,
|
|
47
|
-
auditApprovalCardTap,
|
|
48
|
-
deploymentOrg,
|
|
49
|
-
forgetApprovalCardTicket,
|
|
50
|
-
forgetBridgedQuestion,
|
|
51
|
-
getOperationalStore,
|
|
52
|
-
ingressSpendGate,
|
|
53
|
-
OE_INPUT_CLICK_PREFIX,
|
|
54
|
-
parseApprovalClickId,
|
|
55
|
-
readApprovalCardTicket,
|
|
56
|
-
readBridgedQuestion,
|
|
57
|
-
resolveApprovalCardClick,
|
|
58
|
-
resolvePartition,
|
|
59
|
-
unifiedThreadRef,
|
|
60
|
-
type ApprovalCardRespondFn,
|
|
61
|
-
type BindableContext,
|
|
62
|
-
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
63
|
-
import { buildDiscordAuth } from "./discord-identity.js";
|
|
64
|
-
import { deferredEphemeralResponse, editOriginalInteractionResponse, interactionFollowUp, type InteractionFollowUp } from "./discord-interaction-defer.js";
|
|
65
|
-
|
|
66
|
-
const log = (m: string): void => console.log(`[open-everywhere:discord] approval card: ${m}`);
|
|
67
|
-
|
|
68
|
-
/** The `input_response.routed` sibling for a tap — the SAME two action strings the Slack leg writes,
|
|
69
|
-
* so "how often is a bridged approval settled by a button" is one query across surfaces rather than
|
|
70
|
-
* one per surface. RE-EXPORTED FROM CORE since OPE-1856: two files spelling one action name is two
|
|
71
|
-
* places it can drift, and the drift is silent. */
|
|
72
|
-
export const APPROVAL_CARD_SETTLED_AUDIT_ACTION = CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION;
|
|
73
|
-
export const APPROVAL_CARD_REFUSED_AUDIT_ACTION = CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION;
|
|
74
|
-
|
|
75
|
-
// Discord interaction-response wire constants, defined locally for the same reason every sibling
|
|
76
|
-
// handler in this bundle defines its own: importing eve's channel module for two integers would
|
|
77
|
-
// widen this leaf's graph.
|
|
78
|
-
const RESPONSE_CHANNEL_MESSAGE = 4; // CHANNEL_MESSAGE_WITH_SOURCE
|
|
79
|
-
const EPHEMERAL = 64; // MessageFlags.EPHEMERAL — only the tapper sees it
|
|
80
|
-
|
|
81
|
-
/** The interaction response body this handler returns. Loosely typed, exactly as its three sibling
|
|
82
|
-
* handlers type theirs, so the front route holds ONE seam type across all four. */
|
|
83
|
-
export type DiscordApprovalCardResponse = Record<string, unknown>;
|
|
84
|
-
|
|
85
|
-
/** The settle delivery this leg needs. CORE-OWNED and bound by the front route through
|
|
86
|
-
* `approvalCardRespondVia(args.attachSession)` — re-exported here, never re-spelled, so the address
|
|
87
|
-
* the answer takes cannot drift per surface (OPE-1863). */
|
|
88
|
-
export type ApprovalRespondFn = ApprovalCardRespondFn;
|
|
89
|
-
|
|
90
|
-
/** The host edges this door needs to defer. Both injected so a test drives the whole ack-then-edit
|
|
91
|
-
* sequence with no Discord and no host. */
|
|
92
|
-
export interface DiscordApprovalCardDeps {
|
|
93
|
-
/** The route's own `waitUntil`. ABSENT ⇒ no defer (see the header). */
|
|
94
|
-
readonly waitUntil?: (p: Promise<unknown>) => void;
|
|
95
|
-
/** The follow-up edit. Live default is {@link editOriginalInteractionResponse}. */
|
|
96
|
-
readonly edit?: (followUp: InteractionFollowUp, content: string) => Promise<boolean>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** What one verified tap did, for the front route's log line and for the test to assert on. */
|
|
100
|
-
export interface ApprovalCardClickOutcome {
|
|
101
|
-
readonly handled: boolean;
|
|
102
|
-
readonly reason: string;
|
|
103
|
-
/** A line to show the tapper alone, or undefined when the outcome needs no note. */
|
|
104
|
-
readonly notice?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The interaction fields this leg reads, narrowed so nothing here needs Discord's own types. */
|
|
108
|
-
interface ParsedApprovalClick {
|
|
109
|
-
readonly actionId: string;
|
|
110
|
-
readonly channelId: string;
|
|
111
|
-
readonly userId: string;
|
|
112
|
-
readonly guildId?: string;
|
|
113
|
-
/**
|
|
114
|
-
* LEG B — Discord's own analogue of a guest: a guild member who has NOT cleared membership
|
|
115
|
-
* screening (`member.pending`). The same bundle already reads it off an interaction payload for the
|
|
116
|
-
* inbound trust grain (`discord-identity.ts`), and it is a POSITIVE per-person verdict, which is
|
|
117
|
-
* exactly what `admitInputResponse`'s guest leg takes. Absent ⇒ the probe could not be run (a DM tap
|
|
118
|
-
* carries no member record), and the leg is then INERT: "we could not ask" never becomes a refusal.
|
|
119
|
-
*/
|
|
120
|
-
readonly answererIsGuest?: boolean;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isObject(v: unknown): v is Record<string, unknown> {
|
|
124
|
-
return typeof v === "object" && v !== null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Read the `oe_input:` custom_id out of a VERIFIED MESSAGE_COMPONENT interaction. `undefined` when
|
|
129
|
-
* the payload is not one of ours or is missing a field the decision needs — the front route then
|
|
130
|
-
* forwards it rather than half-handling it.
|
|
131
|
-
*
|
|
132
|
-
* THE CLICKER AND THE CHANNEL COME FROM THE ENVELOPE, never from a payload field the card could
|
|
133
|
-
* carry: the front has already verified Discord's Ed25519 signature over the raw body, so these are
|
|
134
|
-
* Discord's own statements. Discord puts the clicker at `user` in a DM and at `member.user` in a
|
|
135
|
-
* guild; both are read, so a DM tap and a guild tap are equally attributable.
|
|
136
|
-
*/
|
|
137
|
-
export function parseDiscordApprovalClick(raw: unknown): ParsedApprovalClick | undefined {
|
|
138
|
-
if (!isObject(raw)) return undefined;
|
|
139
|
-
if (raw["type"] !== 3) return undefined; // not a MESSAGE_COMPONENT interaction
|
|
140
|
-
const data = isObject(raw["data"]) ? raw["data"] : undefined;
|
|
141
|
-
const actionId = typeof data?.["custom_id"] === "string" ? (data["custom_id"] as string) : undefined;
|
|
142
|
-
if (actionId === undefined || !actionId.startsWith(OE_INPUT_CLICK_PREFIX)) return undefined;
|
|
143
|
-
const direct = isObject(raw["user"]) ? raw["user"]["id"] : undefined;
|
|
144
|
-
const member = isObject(raw["member"]) ? raw["member"]["user"] : undefined;
|
|
145
|
-
const viaMember = isObject(member) ? member["id"] : undefined;
|
|
146
|
-
const userId = typeof direct === "string" && direct.length > 0 ? direct : typeof viaMember === "string" ? viaMember : undefined;
|
|
147
|
-
const channelRaw = raw["channel_id"];
|
|
148
|
-
const channelViaObject = isObject(raw["channel"]) ? raw["channel"]["id"] : undefined;
|
|
149
|
-
const channelId = typeof channelRaw === "string" && channelRaw.length > 0 ? channelRaw : typeof channelViaObject === "string" ? channelViaObject : undefined;
|
|
150
|
-
if (typeof userId !== "string" || userId.length === 0) return undefined;
|
|
151
|
-
if (typeof channelId !== "string" || channelId.length === 0) return undefined;
|
|
152
|
-
const guildId = typeof raw["guild_id"] === "string" && raw["guild_id"].length > 0 ? (raw["guild_id"] as string) : undefined;
|
|
153
|
-
// LEG B, read off the SAME verified envelope. `member.pending` is Discord's own statement that this
|
|
154
|
-
// account has not cleared the guild's membership screening; a payload that carries no member record
|
|
155
|
-
// (every DM tap) leaves the field absent, which the admission reads as "not asked".
|
|
156
|
-
const memberPending = isObject(raw["member"]) ? raw["member"]["pending"] : undefined;
|
|
157
|
-
return {
|
|
158
|
-
actionId,
|
|
159
|
-
channelId,
|
|
160
|
-
userId,
|
|
161
|
-
...(guildId ? { guildId } : {}),
|
|
162
|
-
...(typeof memberPending === "boolean" ? { answererIsGuest: memberPending } : {}),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** The bindable a partition resolve and the spend gate need, built through the bundle's OWN auth
|
|
167
|
-
* builder rather than a hand-spelled attribute bag: the partition a tap files under must be the one
|
|
168
|
-
* a MESSAGE in the same conversation files under, and two spellings is how those drift. */
|
|
169
|
-
function bindableForClick(click: ParsedApprovalClick): BindableContext {
|
|
170
|
-
return {
|
|
171
|
-
channel: { kind: "discord" },
|
|
172
|
-
session: {
|
|
173
|
-
auth: {
|
|
174
|
-
current: buildDiscordAuth({
|
|
175
|
-
channelId: click.channelId,
|
|
176
|
-
userId: click.userId,
|
|
177
|
-
...(click.guildId ? { guildId: click.guildId } : {}),
|
|
178
|
-
}),
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
} as unknown as BindableContext;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* The host copy that rides the delivered answer's `text` — the LABEL THIS DEPLOYMENT PUT ON THE
|
|
186
|
-
* BUTTON, read back out of the same allowlist that minted it, so the transcript says what the person
|
|
187
|
-
* actually chose. A tap carries no words, and writing the person's prose for them is not an option.
|
|
188
|
-
* The DECISION is never this value: eve settles on `optionId` alone.
|
|
189
|
-
*/
|
|
190
|
-
function approvalAnswerText(optionId: string): string {
|
|
191
|
-
return APPROVAL_BUTTON_LABELS.get(optionId.toLowerCase())?.label ?? optionId.charAt(0).toUpperCase() + optionId.slice(1);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Settle one verified `oe_input:` tap: resolve it, admit it, deliver the answer to eve, consume the
|
|
196
|
-
* rows. NEVER THROWS.
|
|
197
|
-
*
|
|
198
|
-
* 🔴 CONSUME-ONCE, AND BOTH ROWS, and only after eve accepted the delivery. The ticket goes so a
|
|
199
|
-
* second tap cannot answer a request eve has already settled; the outstanding-question row goes so
|
|
200
|
-
* the room's NEXT message is an ordinary wake again rather than a second answer. A throw above skips
|
|
201
|
-
* both, which leaves the card tappable and the question answerable in words — the safe direction.
|
|
202
|
-
*
|
|
203
|
-
* THE LIMIT OF THAT, stated rather than implied and identical to the Slack leg's: the read and the
|
|
204
|
-
* delete are not one atomic step, so two deliveries arriving together each resolve against rows that
|
|
205
|
-
* are both still present. Both must be admitted and both must name an option the request offered, and
|
|
206
|
-
* eve's own resolution refuses the second delivery for a request it already settled — so one approval
|
|
207
|
-
* can READ as decided by two taps without being decided twice.
|
|
208
|
-
*/
|
|
209
|
-
export async function settleDiscordApprovalCardClick(click: ParsedApprovalClick, respond: ApprovalRespondFn): Promise<ApprovalCardClickOutcome> {
|
|
210
|
-
// 🔴 HOISTED OUT OF THE `try` SO THE `catch` CAN STAMP ITS ROW (OPE-1856) — the Slack sibling's
|
|
211
|
-
// reasoning verbatim. A throw anywhere below, the partition resolve included, must still leave a
|
|
212
|
-
// row saying this deployment took a tap and settled nothing.
|
|
213
|
-
let auditPartition: ReturnType<typeof resolvePartition> | undefined;
|
|
214
|
-
try {
|
|
215
|
-
const parsed = parseApprovalClickId(click.actionId);
|
|
216
|
-
if (parsed === undefined) return { handled: false, reason: "oe_input: custom_id did not parse" };
|
|
217
|
-
const org = deploymentOrg();
|
|
218
|
-
const store = getOperationalStore();
|
|
219
|
-
const bindable = bindableForClick(click);
|
|
220
|
-
const partition = resolvePartition(bindable);
|
|
221
|
-
auditPartition = partition;
|
|
222
|
-
// THE SPEND GATE FIRST, ahead of the identity admission — the same order and the same reason the
|
|
223
|
-
// Slack leg takes: over budget outranks identity, and a decline must not spend a roster lookup.
|
|
224
|
-
// A decline leaves the request PENDING.
|
|
225
|
-
// 🔴 THE `post` ARM CAPTURES, IT DOES NOT POST. The gate's refusal copy would otherwise land as
|
|
226
|
-
// a PUBLIC message in the channel the card sits in, telling the room that a person's tap was
|
|
227
|
-
// declined. This door already owns a private answer — the interaction response — so the copy is
|
|
228
|
-
// captured here and shown to the tapper alone, through the outcome's `notice`. The binding is
|
|
229
|
-
// local to this call, so two taps in flight cannot read each other's copy.
|
|
230
|
-
let declineNotice: string | undefined;
|
|
231
|
-
const spend = await ingressSpendGate({
|
|
232
|
-
bindable,
|
|
233
|
-
door: "answer",
|
|
234
|
-
principalId: click.userId,
|
|
235
|
-
...(click.guildId ? { workspaceRef: click.guildId } : {}),
|
|
236
|
-
post: async (text) => {
|
|
237
|
-
declineNotice = text;
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
if (spend === "declined") {
|
|
241
|
-
// AUDITED, not only logged (OPE-1856): a decline is this deployment refusing a person's tap,
|
|
242
|
-
// and a refusal nobody can count is indistinguishable from a tap that never arrived.
|
|
243
|
-
await auditApprovalCardTap({ platform: "discord", principalId: click.userId, partition, outcome: { kind: "refused", reason: "spend-declined" } });
|
|
244
|
-
log("tap declined by the spend gate — the approval stays parked");
|
|
245
|
-
return { handled: true, reason: "spend-declined", ...(declineNotice ? { notice: declineNotice } : {}) };
|
|
246
|
-
}
|
|
247
|
-
// The room the tap came from, in the SAME vocabulary the card's ticket stored — derived through
|
|
248
|
-
// core's own codec rather than spelled here, so the two sides cannot drift on the key's shape. A
|
|
249
|
-
// guild tap and a DM tap key differently, which is exactly what the codec already knows.
|
|
250
|
-
const containerKey = unifiedThreadRef(
|
|
251
|
-
click.guildId ? { platform: "discord", channelId: click.channelId, guildId: click.guildId } : { platform: "discord", channelId: click.channelId, dm: true },
|
|
252
|
-
).channelKey;
|
|
253
|
-
const routing = await resolveApprovalCardClick(
|
|
254
|
-
{ ticket: parsed.ticket, optionId: parsed.optionId, containerKey },
|
|
255
|
-
{
|
|
256
|
-
read: (ticket) => readApprovalCardTicket(store, org, ticket),
|
|
257
|
-
// The join with the TEXT leg — fail-CLOSED on an unreadable row: a store this leg cannot read
|
|
258
|
-
// cannot say the request is still parked, and settling on that guess is the one direction
|
|
259
|
-
// that spends a governed decision twice.
|
|
260
|
-
stillOutstanding: async (bareToken, requestId) => {
|
|
261
|
-
const outstanding = await readBridgedQuestion(store, org, bareToken).catch(() => undefined);
|
|
262
|
-
return outstanding?.requestId === requestId;
|
|
263
|
-
},
|
|
264
|
-
admit: async () => {
|
|
265
|
-
const verdict = await admitInputResponse({
|
|
266
|
-
org: partition.org,
|
|
267
|
-
platform: partition.platform,
|
|
268
|
-
workspace: partition.workspace,
|
|
269
|
-
surface: partition.surface,
|
|
270
|
-
surfaceKey: partition.surfaceKey,
|
|
271
|
-
principalId: click.userId,
|
|
272
|
-
responses: 1,
|
|
273
|
-
// Leg B — the positive per-answerer verdict, and INERT when the envelope could not answer
|
|
274
|
-
// it (`undefined` never refuses), exactly as the Slack sibling's probe is.
|
|
275
|
-
...(click.answererIsGuest === undefined ? {} : { answererIsGuest: click.answererIsGuest }),
|
|
276
|
-
});
|
|
277
|
-
return { ok: verdict.ok, floor: verdict.ok ? verdict.floor : "", notice: verdict.ok ? "" : verdict.notice };
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
);
|
|
281
|
-
if (routing.kind === "refuse") {
|
|
282
|
-
await auditApprovalCardTap({ platform: "discord", principalId: click.userId, partition, outcome: { kind: "refused", reason: routing.reason } });
|
|
283
|
-
log(`tap settled nothing (${routing.reason}) — the approval stays parked and answerable in words`);
|
|
284
|
-
return { handled: true, reason: routing.reason, ...(routing.notice ? { notice: routing.notice } : {}) };
|
|
285
|
-
}
|
|
286
|
-
const label = approvalAnswerText(routing.optionId);
|
|
287
|
-
// 🔴 DELIVERED BY SESSION ID, never by the window token (OPE-1863) — the Slack sibling's
|
|
288
|
-
// reasoning verbatim: a continuation token is channel-local, this route is the DISCORD channel's,
|
|
289
|
-
// and `from(bareToken)` here named a session that never existed. The token's job is the
|
|
290
|
-
// outstanding-question row on the two lines that follow.
|
|
291
|
-
await respond(routing.sessionId, { requestId: routing.requestId, optionId: routing.optionId, text: label });
|
|
292
|
-
await forgetApprovalCardTicket(store, org, parsed.ticket);
|
|
293
|
-
await forgetBridgedQuestion(store, org, routing.bareToken);
|
|
294
|
-
await auditApprovalCardTap({
|
|
295
|
-
platform: "discord",
|
|
296
|
-
principalId: click.userId,
|
|
297
|
-
partition,
|
|
298
|
-
outcome: { kind: "settled", requestId: routing.requestId, optionId: routing.optionId, floor: routing.floor },
|
|
299
|
-
});
|
|
300
|
-
log(`tap settled ${routing.requestId} as "${routing.optionId}" (floor ${routing.floor})`);
|
|
301
|
-
return { handled: true, reason: "settled", notice: `${label} — recorded. The request is settled.` };
|
|
302
|
-
} catch (err) {
|
|
303
|
-
// The approval is still parked and the card is still tappable, so the honest outcome is to say so
|
|
304
|
-
// and leave both rows in place.
|
|
305
|
-
// AUDITED, not only logged (OPE-1856): a tap that threw left no trace at all, so it could not be
|
|
306
|
-
// told apart from a tap that never arrived. `auditApprovalCardTap` never throws.
|
|
307
|
-
await auditApprovalCardTap({ platform: "discord", principalId: click.userId, ...(auditPartition === undefined ? {} : { partition: auditPartition }), outcome: { kind: "refused", reason: "error" } });
|
|
308
|
-
log(`tap failed (${(err as Error)?.message ?? String(err)}) — nothing was settled, the approval stays parked`);
|
|
309
|
-
return { handled: true, reason: "error", notice: "That could not be answered right now. Nothing was settled — the request is still open, and you can answer it in words." };
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* The line the tapper reads, for an outcome that carried no notice of its own.
|
|
315
|
-
*
|
|
316
|
-
* 🔴 A REFUSAL MUST NEVER READ AS A SETTLEMENT. This door ANSWERS — it edits a deferred placeholder —
|
|
317
|
-
* so a default of "Recorded." would tell somebody whose tap settled nothing that their approval had
|
|
318
|
-
* landed, and they would stop waiting for it. Core attaches a `notice` only to the `refused` arm, so
|
|
319
|
-
* `no-ticket`, `foreign-room`, `unoffered-option` and `already-answered` all arrive here bare. The
|
|
320
|
-
* Slack sibling can be SILENT in these cases, which is not a false statement; this one cannot, so the
|
|
321
|
-
* default is the honest sentence and "Recorded." is reserved for a settle.
|
|
322
|
-
*/
|
|
323
|
-
function tapperLine(outcome: ApprovalCardClickOutcome): string {
|
|
324
|
-
if (outcome.notice !== undefined && outcome.notice.length > 0) return outcome.notice;
|
|
325
|
-
if (outcome.reason === "settled") return "Recorded.";
|
|
326
|
-
return "Nothing was settled — the request is still open, and you can answer it in words.";
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Handle one verified `oe_input:` component click.
|
|
331
|
-
*
|
|
332
|
-
* Returns the interaction response body to send, or `null` for anything that is NOT one of ours —
|
|
333
|
-
* the front route then forwards it, so a click is never silently swallowed. NEVER throws.
|
|
334
|
-
*/
|
|
335
|
-
export async function handleDiscordApprovalCardInteraction(
|
|
336
|
-
raw: unknown,
|
|
337
|
-
respond: ApprovalRespondFn,
|
|
338
|
-
deps: DiscordApprovalCardDeps = {},
|
|
339
|
-
): Promise<DiscordApprovalCardResponse | null> {
|
|
340
|
-
const click = parseDiscordApprovalClick(raw);
|
|
341
|
-
if (click === undefined) return null;
|
|
342
|
-
const followUp = interactionFollowUp(raw);
|
|
343
|
-
if (deps.waitUntil === undefined || followUp === undefined) {
|
|
344
|
-
// NO PLACE TO RUN THE WORK, OR NO ADDRESS TO ANSWER AT ⇒ settle on the response path. Slow, and
|
|
345
|
-
// still correct: a deferred ack we could not follow up would leave a permanent "thinking…".
|
|
346
|
-
log(`tap answering synchronously (${deps.waitUntil === undefined ? "no waitUntil" : "no follow-up address"}) — a slow settle may outlive the interaction token`);
|
|
347
|
-
const outcome = await settleDiscordApprovalCardClick(click, respond);
|
|
348
|
-
if (!outcome.handled) return null;
|
|
349
|
-
return { type: RESPONSE_CHANNEL_MESSAGE, data: { content: tapperLine(outcome), flags: EPHEMERAL } };
|
|
350
|
-
}
|
|
351
|
-
const edit = deps.edit ?? editOriginalInteractionResponse;
|
|
352
|
-
deps.waitUntil(
|
|
353
|
-
(async () => {
|
|
354
|
-
// TOTAL. This runs after the response has gone; a throw here reaches the host's unhandled
|
|
355
|
-
// rejection handler and tells the tapper nothing. `settleDiscordApprovalCardClick` is already
|
|
356
|
-
// total, so the catch is for the edit's own transport.
|
|
357
|
-
let text: string;
|
|
358
|
-
try {
|
|
359
|
-
const outcome = await settleDiscordApprovalCardClick(click, respond);
|
|
360
|
-
text = tapperLine(outcome);
|
|
361
|
-
} catch (err) {
|
|
362
|
-
log(`deferred tap settle failed: ${(err as Error)?.message ?? String(err)}`);
|
|
363
|
-
text = "That could not be answered right now. Nothing was settled — the request is still open, and you can answer it in words.";
|
|
364
|
-
}
|
|
365
|
-
// 🔴 A FAILED EDIT IS LOUD BUT NOT FATAL: the settle has already landed and is audited by core.
|
|
366
|
-
// What is lost is the tapper's explanation — which is exactly what an expired token loses — so
|
|
367
|
-
// it must be findable in the log.
|
|
368
|
-
const shown = await edit(followUp, text);
|
|
369
|
-
if (!shown) log("tap answer could NOT be shown to the tapper — the settle landed and is audited, the tapper was not told");
|
|
370
|
-
})(),
|
|
371
|
-
);
|
|
372
|
-
return deferredEphemeralResponse();
|
|
373
|
-
}
|