@openclaw/discord 2026.9.8 → 2026.9.33
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/dist/.setup/{account-inspect-De8hOUxG.mjs → account-inspect-Bp8nbBSx.mjs} +2 -2
- package/dist/.setup/{channel-type-DRCrEMss.mjs → allow-list-Kty_8Xca.mjs} +7 -7
- package/dist/.setup/{api-DVbJwZVt.mjs → api-CotnTQLx.mjs} +3 -3
- package/dist/.setup/{approval-handler.runtime-DxCK2Rux.mjs → approval-handler.runtime-DqU_yO2L.mjs} +9 -9
- package/dist/.setup/{approval-message-updates-BKFqavHZ.mjs → approval-message-updates-CdnQ98c8.mjs} +2 -2
- package/dist/.setup/{audit-CLg9I71N.mjs → audit-CqUeNcJH.mjs} +4 -4
- package/dist/.setup/{channel-DxlUVU9t.mjs → channel-Bu1qLdnm.mjs} +28 -23
- package/dist/.setup/{channel.setup-CRo1GjV7.mjs → channel.setup-DRCLt54O.mjs} +4 -4
- package/dist/.setup/{components-Cq1PvIpN.mjs → components-CwpgbGDR.mjs} +3 -3
- package/dist/.setup/{conversation-binding-route-Caz_XuFH.mjs → conversation-binding-route-DC0Y2v1B.mjs} +1 -1
- package/dist/.setup/{directory-config-esqNn0Eo.mjs → directory-config-DLT0PP2j.mjs} +1 -1
- package/dist/.setup/{directory-live-DtKRHuGV.mjs → directory-live-DPVUIJ8-.mjs} +4 -4
- package/dist/.setup/{discord-0xJYfdDD.mjs → discord-DTLP81g6.mjs} +3 -3
- package/dist/.setup/{doctor-BCl1H5tl.mjs → doctor-E3AF74Ms.mjs} +5 -5
- package/dist/.setup/{handle-action-CUvQWnE7.mjs → handle-action-cmTFQysA.mjs} +8 -8
- package/dist/.setup/{handle-action.guild-admin-Df-8gVle.mjs → handle-action.guild-admin-CH8nj2w0.mjs} +2 -2
- package/dist/.setup/{live-policy-config-8Pv20CHc.mjs → live-policy-config-D--q9Zta.mjs} +1 -1
- package/dist/.setup/{message-handler-B-Jk4oDk.mjs → message-handler-BVZO6wCg.mjs} +12 -11
- package/dist/.setup/{message-handler.preflight-v-__NSDq.mjs → message-handler.preflight-Be_b_NFp.mjs} +17 -17
- package/dist/.setup/{message-handler.preflight-pluralkit-CWLUBVcx.mjs → message-handler.preflight-pluralkit-TPGXsd8P.mjs} +7 -7
- package/dist/.setup/{message-handler.process-BNFFuChn.mjs → message-handler.process-BUBGcAoh.mjs} +21 -21
- package/dist/.setup/{outbound-adapter-BobT-kse.mjs → outbound-adapter-CSiCDzCN.mjs} +9 -9
- package/dist/.setup/{outbound-session-route-zVGxxceg.mjs → outbound-session-route-D0ryINch.mjs} +13 -13
- package/dist/.setup/{probe-LYrIHlBF.mjs → probe-DTSDdlpy.mjs} +4 -4
- package/dist/.setup/{provider-B_G5hASF.mjs → provider-BTm60b3t.mjs} +81 -81
- package/dist/.setup/{provider-session.runtime-BYlTBFD2.mjs → provider-session.runtime-DKJ-qSt9.mjs} +3 -3
- package/dist/.setup/provider.runtime-BvoZBuZY.mjs +2 -0
- package/dist/.setup/{realtime-session.runtime-oUd9DOcz.mjs → realtime-session.runtime-JZd7A5bS.mjs} +19 -19
- package/dist/.setup/{resolve-allowlist-common-DPdRggu5.mjs → resolve-allowlist-common-Ck4M_Ot2.mjs} +3 -3
- package/dist/.setup/{resolve-channels-BFPNRmVv.mjs → resolve-channels-CEaKGzuh.mjs} +3 -3
- package/dist/.setup/{resolve-users-0-aBuCy3.mjs → resolve-users-CMz4PVnV.mjs} +2 -2
- package/dist/.setup/{runtime-FN000lxA.mjs → runtime-DwW5C0WK.mjs} +12 -12
- package/dist/.setup/{runtime-sUksZ--Q.mjs → runtime-Lsy4dEx7.mjs} +2 -2
- package/dist/.setup/{send-CrfEI6Bs.mjs → send-DY5EeBPr.mjs} +15 -15
- package/dist/.setup/{send.components-DxQAMfES.mjs → send.components-DGwzyA_4.mjs} +7 -7
- package/dist/.setup/{send.outbound-BISKnb-a.mjs → send.outbound-DR8O-Pvo.mjs} +8 -8
- package/dist/.setup/{retry-pz_cSha2.mjs → send.receipt-D0JBijZQ.mjs} +217 -217
- package/dist/.setup/{send.shared-DGFkbkQR.mjs → send.shared-r0Fj2v_h.mjs} +40 -40
- package/dist/.setup/{sender-identity-B9X5Vb9R.mjs → sender-identity-DQCoikZl.mjs} +1 -1
- package/dist/.setup/setup-plugin-api-BEoo_L8p.mjs +3 -0
- package/dist/.setup/{shared-B3wZ4hN-.mjs → shared-CDq4vJm3.mjs} +7 -7
- package/dist/.setup/{subagent-hooks-nlUE9ow7.mjs → subagent-hooks-Bk0mMyHh.mjs} +2 -2
- package/dist/.setup/{system-events-CdeZnvPV.mjs → system-events-I1rH-U6o.mjs} +2 -2
- package/dist/.setup/{target-resolver-BuLapkhy.mjs → target-resolver-Dcg0WjsF.mjs} +3 -3
- package/dist/.setup/targets-CJkVZIkE.mjs +3 -0
- package/dist/.setup/{thread-bindings-D7TfgjR_.mjs → thread-bindings-E0TiUQUX.mjs} +6 -6
- package/dist/.setup/{thread-bindings.manager-DN-NissX.mjs → thread-bindings.manager-CEoFDOs9.mjs} +14 -14
- package/dist/.setup/{thread-bindings.session-updates-CshfKghV.mjs → thread-bindings.session-updates-DvtjzKO-.mjs} +2 -2
- package/dist/.setup/{transcripts-source-p8EV179h.mjs → transcripts-source-CHhpvL97.mjs} +17 -17
- package/dist/.setup/{typing-BiHCzBRu.mjs → typing-Cr0b2zBq.mjs} +1 -1
- package/dist/.setup/{voice-runtime-B46rcwrG.mjs → voice-runtime-BngIfPc3.mjs} +46 -46
- package/dist/account-inspect-api.js +1 -1
- package/dist/action-runtime-api.js +1 -1
- package/dist/activities-api.js +791 -1
- package/dist/api.js +15 -15
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/directory-contract-api.js +1 -1
- package/dist/doctor-contract-api.js +1 -1
- package/dist/index.js +9 -5
- package/dist/runtime-api.js +20 -20
- package/dist/setup-entry.js +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/src/voice/audio-worker.runtime.js +3 -3
- package/dist/subagent-hooks-api.js +1 -1
- package/dist/transcripts-source-api.js +1 -1
- package/package.json +4 -4
- package/dist/.setup/activities-api-tvbsU2e3.mjs +0 -792
- package/dist/.setup/provider.runtime-CUE3UxER.mjs +0 -2
- package/dist/.setup/setup-plugin-api-BQUBxbhe.mjs +0 -3
- package/dist/.setup/targets-0KUcEIYx.mjs +0 -3
- package/dist/.setup/{accounts-ChaMcixG.mjs → accounts-DS-A-gCx.mjs} +3 -3
- package/dist/.setup/{config-schema-oxELUSDx.mjs → config-schema-DKUSr985.mjs} +2 -2
- package/dist/.setup/{receive-recovery-CHPOsnLc.mjs → receive-recovery-D6eikHf5.mjs} +2 -2
- package/dist/.setup/{thread-bindings.state-g6v9zAym.mjs → thread-bindings.state-755lJHln.mjs} +3 -3
|
@@ -1,792 +0,0 @@
|
|
|
1
|
-
import { S as parseDiscordActivityCustomId, i as buildDiscordPresentationComponents } from "./components-Cq1PvIpN.mjs";
|
|
2
|
-
import { D as getDiscordEndpointRuntime } from "./channel-type-DRCrEMss.mjs";
|
|
3
|
-
import { c as resolveDiscordAccount } from "./accounts-ChaMcixG.mjs";
|
|
4
|
-
import { r as sendDiscordComponentMessage } from "./send.components-DxQAMfES.mjs";
|
|
5
|
-
import { x as resolveDiscordChannelId$1 } from "./retry-pz_cSha2.mjs";
|
|
6
|
-
import { r as setDiscordActivitiesRuntime, t as DiscordActivitiesRuntime } from "./runtime-sUksZ--Q.mjs";
|
|
7
|
-
import fs from "node:fs/promises";
|
|
8
|
-
import { logError } from "openclaw/plugin-sdk/logging-core";
|
|
9
|
-
import { resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
|
|
10
|
-
import { WEBHOOK_BODY_READ_DEFAULTS, readJsonBodyWithLimit, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
11
|
-
import { WIDGET_CDN_ORIGINS } from "openclaw/plugin-sdk/widget-html";
|
|
12
|
-
import { randomBytes } from "node:crypto";
|
|
13
|
-
import { fetchWithSsrFGuard as fetchWithSsrFGuard$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
14
|
-
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
|
15
|
-
//#region extensions/discord/src/activities/discord-api.ts
|
|
16
|
-
const DISCORD_TOKEN_URL = "https://discord.com/api/oauth2/token";
|
|
17
|
-
const DISCORD_USER_URL = "https://discord.com/api/v10/users/@me";
|
|
18
|
-
const DISCORD_HOST = "discord.com";
|
|
19
|
-
const DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS = 15e3;
|
|
20
|
-
const JSON_MAX_BYTES = 65536;
|
|
21
|
-
const INSTANCE_ID_MAX_LENGTH = 256;
|
|
22
|
-
function normalizeInstanceId(value) {
|
|
23
|
-
const instanceId = value?.trim();
|
|
24
|
-
let hasControlCharacter = false;
|
|
25
|
-
for (let index = 0; index < (instanceId?.length ?? 0); index += 1) {
|
|
26
|
-
const codePoint = instanceId?.charCodeAt(index) ?? 0;
|
|
27
|
-
if (codePoint < 32 || codePoint === 127) {
|
|
28
|
-
hasControlCharacter = true;
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
if (!instanceId || instanceId.length > INSTANCE_ID_MAX_LENGTH || hasControlCharacter) return;
|
|
33
|
-
return instanceId;
|
|
34
|
-
}
|
|
35
|
-
async function fetchDiscordJson(params) {
|
|
36
|
-
const endpoint = Object.hasOwn(params, "endpointRuntime") ? params.endpointRuntime : getDiscordEndpointRuntime();
|
|
37
|
-
const liveUrl = new URL(params.url);
|
|
38
|
-
const endpointPath = liveUrl.pathname === "/api/oauth2/token" ? "/oauth2/token" : liveUrl.pathname.startsWith("/api/v10/") ? liveUrl.pathname.slice(8) : void 0;
|
|
39
|
-
if (endpoint && !endpointPath) throw new Error("Discord Activity request is outside the configured endpoint routes");
|
|
40
|
-
const { response, release } = endpoint ? {
|
|
41
|
-
response: await endpoint.fetch(`${endpoint.descriptor.restApiBaseUrl}${endpointPath}${liveUrl.search}`, {
|
|
42
|
-
...params.init,
|
|
43
|
-
signal: params.init.signal ? AbortSignal.any([params.init.signal, AbortSignal.timeout(DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS)
|
|
44
|
-
}),
|
|
45
|
-
release: async () => {}
|
|
46
|
-
} : await params.fetchGuard({
|
|
47
|
-
url: params.url,
|
|
48
|
-
fetchImpl: params.fetchImpl,
|
|
49
|
-
init: params.init,
|
|
50
|
-
policy: { allowedHostnames: [DISCORD_HOST] },
|
|
51
|
-
auditContext: params.auditContext,
|
|
52
|
-
timeoutMs: DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS
|
|
53
|
-
});
|
|
54
|
-
try {
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
await response.body?.cancel().catch(() => void 0);
|
|
57
|
-
return {
|
|
58
|
-
ok: false,
|
|
59
|
-
status: response.status
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
ok: true,
|
|
64
|
-
status: response.status,
|
|
65
|
-
body: await readProviderJsonResponse(response, "Discord Activity OAuth", { maxBytes: JSON_MAX_BYTES })
|
|
66
|
-
};
|
|
67
|
-
} finally {
|
|
68
|
-
await release();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function resolveActivityInstanceChannel(params) {
|
|
72
|
-
const endpointRuntime = getDiscordEndpointRuntime() ?? null;
|
|
73
|
-
let result;
|
|
74
|
-
try {
|
|
75
|
-
result = await fetchDiscordJson({
|
|
76
|
-
fetchGuard: params.fetchGuard,
|
|
77
|
-
fetchImpl: params.proxyFetch,
|
|
78
|
-
url: `https://discord.com/api/v10/applications/${encodeURIComponent(params.applicationId)}/activity-instances/${encodeURIComponent(params.instanceId)}`,
|
|
79
|
-
init: { headers: { Authorization: `Bot ${params.botAuth}` } },
|
|
80
|
-
auditContext: "discord.activities.instance",
|
|
81
|
-
endpointRuntime
|
|
82
|
-
});
|
|
83
|
-
} catch {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (!result.ok || !Array.isArray(result.body?.users) || !result.body.users.includes(params.discordUserId) || !result.body.location || typeof result.body.location !== "object") return;
|
|
87
|
-
const channelId = result.body.location.channel_id;
|
|
88
|
-
return typeof channelId === "string" && /^\d+$/.test(channelId) ? channelId : void 0;
|
|
89
|
-
}
|
|
90
|
-
//#endregion
|
|
91
|
-
//#region extensions/discord/src/activities/rate-limit.ts
|
|
92
|
-
const TOKEN_RATE_LIMIT = 10;
|
|
93
|
-
const TOKEN_GLOBAL_RATE_LIMIT = 60;
|
|
94
|
-
const TOKEN_RATE_WINDOW_MS = 6e4;
|
|
95
|
-
const TOKEN_RATE_MAX_KEYS = 1024;
|
|
96
|
-
var TokenRateLimiter = class {
|
|
97
|
-
constructor(now) {
|
|
98
|
-
this.now = now;
|
|
99
|
-
this.attempts = /* @__PURE__ */ new Map();
|
|
100
|
-
this.globalAttempts = [];
|
|
101
|
-
}
|
|
102
|
-
allowKey(key) {
|
|
103
|
-
const now = this.now();
|
|
104
|
-
const active = (this.attempts.get(key) ?? []).filter((timestamp) => timestamp > now - TOKEN_RATE_WINDOW_MS);
|
|
105
|
-
if (active.length >= TOKEN_RATE_LIMIT) {
|
|
106
|
-
this.attempts.delete(key);
|
|
107
|
-
this.attempts.set(key, active);
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
if (!this.attempts.has(key) && this.attempts.size >= TOKEN_RATE_MAX_KEYS) {
|
|
111
|
-
const oldestKey = this.attempts.keys().next().value;
|
|
112
|
-
if (typeof oldestKey === "string") this.attempts.delete(oldestKey);
|
|
113
|
-
}
|
|
114
|
-
active.push(now);
|
|
115
|
-
this.attempts.delete(key);
|
|
116
|
-
this.attempts.set(key, active);
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
reserveGlobal() {
|
|
120
|
-
const now = this.now();
|
|
121
|
-
this.globalAttempts = this.globalAttempts.filter((reservation) => reservation.at > now - TOKEN_RATE_WINDOW_MS);
|
|
122
|
-
if (this.globalAttempts.length >= TOKEN_GLOBAL_RATE_LIMIT) return null;
|
|
123
|
-
const reservation = { at: now };
|
|
124
|
-
this.globalAttempts.push(reservation);
|
|
125
|
-
return reservation;
|
|
126
|
-
}
|
|
127
|
-
releaseGlobal(reservation) {
|
|
128
|
-
const index = this.globalAttempts.indexOf(reservation);
|
|
129
|
-
if (index >= 0) this.globalAttempts.splice(index, 1);
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
//#endregion
|
|
133
|
-
//#region extensions/discord/src/activities/shell.ts
|
|
134
|
-
const DISCORD_ACTIVITY_ROUTE_PREFIX = "/discord/activity";
|
|
135
|
-
const DISCORD_ACTIVITY_TOKEN_REQUEST_TIMEOUT_MS = 35e3;
|
|
136
|
-
const DISCORD_ACTIVITY_WIDGET_REQUEST_TIMEOUT_MS = 2e4;
|
|
137
|
-
const DISCORD_ACTIVITY_SHELL_CSP = "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; connect-src 'self'; frame-src 'self'; img-src data:; base-uri 'none'; frame-ancestors *";
|
|
138
|
-
const DISCORD_ACTIVITY_SHELL_HTML = `<!doctype html>
|
|
139
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
140
|
-
<title>OpenClaw widget</title><style>
|
|
141
|
-
:root{color-scheme:dark;font:14px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1e1f22;color:#dbdee1}
|
|
142
|
-
*{box-sizing:border-box}html,body,#app{height:100%;margin:0}#app{display:grid;place-items:center}main{max-width:560px;padding:32px;text-align:center}
|
|
143
|
-
h1{font-size:18px;margin:0 0 8px;color:#f2f3f5}p{margin:0;color:#b5bac1;line-height:1.5}.widget{display:grid;grid-template-rows:42px 1fr;width:100%;height:100%;background:#111214}
|
|
144
|
-
.bar{display:flex;align-items:center;padding:0 14px;border-bottom:1px solid #2b2d31;font-weight:600;color:#f2f3f5}.widget iframe{border:0;width:100%;height:100%;background:#111214}
|
|
145
|
-
</style></head><body><div id="app"><main><h1>Opening widget</h1><p>Connecting to Discord…</p></main></div>
|
|
146
|
-
<script type="module" src="./shell.js"><\/script></body></html>`;
|
|
147
|
-
const DISCORD_ACTIVITY_SHELL_JS = `import { DiscordSDK } from "./vendor/embedded-app-sdk.mjs";
|
|
148
|
-
|
|
149
|
-
const tokenRequestTimeoutMs = ${DISCORD_ACTIVITY_TOKEN_REQUEST_TIMEOUT_MS};
|
|
150
|
-
const widgetRequestTimeoutMs = ${DISCORD_ACTIVITY_WIDGET_REQUEST_TIMEOUT_MS};
|
|
151
|
-
const app = document.querySelector("#app");
|
|
152
|
-
function show(message, detail) {
|
|
153
|
-
app.className = "";
|
|
154
|
-
app.innerHTML = "";
|
|
155
|
-
const main = document.createElement("main");
|
|
156
|
-
const heading = document.createElement("h1");
|
|
157
|
-
const paragraph = document.createElement("p");
|
|
158
|
-
heading.textContent = message;
|
|
159
|
-
paragraph.textContent = detail;
|
|
160
|
-
main.append(heading, paragraph);
|
|
161
|
-
app.append(main);
|
|
162
|
-
}
|
|
163
|
-
function proxiedDocUrl(value) {
|
|
164
|
-
const url = new URL(value, window.location.origin);
|
|
165
|
-
if (window.location.hostname.endsWith(".discordsays.com")) {
|
|
166
|
-
// The ROOT mapping target already includes this prefix; strip it before proxying.
|
|
167
|
-
const gatewayPrefix = "/discord/activity";
|
|
168
|
-
const mappedPath = url.pathname.startsWith(gatewayPrefix + "/")
|
|
169
|
-
? url.pathname.slice(gatewayPrefix.length)
|
|
170
|
-
: url.pathname;
|
|
171
|
-
return "/.proxy" + mappedPath + url.search;
|
|
172
|
-
}
|
|
173
|
-
return url.pathname + url.search;
|
|
174
|
-
}
|
|
175
|
-
async function readJson(response) {
|
|
176
|
-
let body;
|
|
177
|
-
try {
|
|
178
|
-
body = await response.json();
|
|
179
|
-
} catch (error) {
|
|
180
|
-
// Error responses may legitimately omit JSON details; successful responses must not
|
|
181
|
-
// turn an aborted or malformed body into an apparently valid empty payload.
|
|
182
|
-
if (response.ok) throw error;
|
|
183
|
-
body = {};
|
|
184
|
-
}
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
const error = new Error(typeof body.error === "string" ? body.error : "request failed");
|
|
187
|
-
error.status = response.status;
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
return body;
|
|
191
|
-
}
|
|
192
|
-
async function fetchJsonWithDeadline(input, init, timeoutMs) {
|
|
193
|
-
// Activities also run in mobile webviews; avoid requiring the newer
|
|
194
|
-
// AbortSignal.timeout() static API just to enforce this request boundary.
|
|
195
|
-
const controller = new AbortController();
|
|
196
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
197
|
-
try {
|
|
198
|
-
return await readJson(await fetch(input, { ...init, signal: controller.signal }));
|
|
199
|
-
} finally {
|
|
200
|
-
clearTimeout(timeout);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function run() {
|
|
204
|
-
const match = window.location.hostname.match(/^(\\d+)\\.discordsays\\.com$/i);
|
|
205
|
-
if (!match) {
|
|
206
|
-
show("Open inside Discord", "This widget must be launched from its Discord button.");
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const clientId = match[1];
|
|
210
|
-
const sdk = new DiscordSDK(clientId);
|
|
211
|
-
await sdk.ready();
|
|
212
|
-
const { code } = await sdk.commands.authorize({
|
|
213
|
-
client_id: clientId,
|
|
214
|
-
response_type: "code",
|
|
215
|
-
state: "",
|
|
216
|
-
prompt: "none",
|
|
217
|
-
scope: ["identify"],
|
|
218
|
-
});
|
|
219
|
-
const auth = await fetchJsonWithDeadline(
|
|
220
|
-
"./api/token",
|
|
221
|
-
{
|
|
222
|
-
method: "POST",
|
|
223
|
-
headers: { "Content-Type": "application/json" },
|
|
224
|
-
body: JSON.stringify({ code }),
|
|
225
|
-
},
|
|
226
|
-
tokenRequestTimeoutMs,
|
|
227
|
-
);
|
|
228
|
-
await sdk.commands.authenticate({ access_token: auth.access_token });
|
|
229
|
-
const query = new URLSearchParams({
|
|
230
|
-
custom_id: sdk.customId ?? "",
|
|
231
|
-
instance_id: sdk.instanceId,
|
|
232
|
-
});
|
|
233
|
-
const widget = await fetchJsonWithDeadline(
|
|
234
|
-
"./api/widget?" + query,
|
|
235
|
-
{ headers: { Authorization: "Bearer " + auth.session_token } },
|
|
236
|
-
widgetRequestTimeoutMs,
|
|
237
|
-
);
|
|
238
|
-
app.className = "widget";
|
|
239
|
-
app.innerHTML = "";
|
|
240
|
-
const bar = document.createElement("div");
|
|
241
|
-
const frame = document.createElement("iframe");
|
|
242
|
-
bar.className = "bar";
|
|
243
|
-
bar.textContent = widget.title;
|
|
244
|
-
frame.title = widget.title;
|
|
245
|
-
// Intentionally minimal: no top-navigation, popups, or same-origin access.
|
|
246
|
-
frame.setAttribute("sandbox", "allow-scripts");
|
|
247
|
-
frame.referrerPolicy = "no-referrer";
|
|
248
|
-
frame.src = proxiedDocUrl(widget.docUrl);
|
|
249
|
-
app.append(bar, frame);
|
|
250
|
-
}
|
|
251
|
-
run().catch((error) => {
|
|
252
|
-
if (error?.status === 404) {
|
|
253
|
-
show("Widget unavailable", "No widget could be resolved for this channel.");
|
|
254
|
-
} else {
|
|
255
|
-
show("Gateway offline", "The OpenClaw gateway could not load this widget. Try again shortly.");
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
`;
|
|
259
|
-
//#endregion
|
|
260
|
-
//#region extensions/discord/src/activities/http.ts
|
|
261
|
-
const BODY_MAX_BYTES = 8192;
|
|
262
|
-
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
263
|
-
const WIDGET_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
264
|
-
const DOC_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
265
|
-
const DISCORD_ACTIVITY_WIDGET_CSP = `sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline' ${WIDGET_CDN_ORIGINS.join(" ")}; style-src 'unsafe-inline' ${WIDGET_CDN_ORIGINS.join(" ")}; img-src data: blob:; font-src data: ${WIDGET_CDN_ORIGINS.join(" ")}; connect-src 'none'; frame-ancestors *`;
|
|
266
|
-
function setCommonHeaders(res) {
|
|
267
|
-
res.setHeader("Cache-Control", "no-store");
|
|
268
|
-
res.setHeader("Referrer-Policy", "no-referrer");
|
|
269
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
270
|
-
}
|
|
271
|
-
function respond(res, statusCode, body, contentType, headers) {
|
|
272
|
-
res.statusCode = statusCode;
|
|
273
|
-
setCommonHeaders(res);
|
|
274
|
-
res.setHeader("Content-Type", contentType);
|
|
275
|
-
for (const [key, value] of Object.entries(headers ?? {})) res.setHeader(key, value);
|
|
276
|
-
res.end(body);
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
function jsonBody(body) {
|
|
280
|
-
return `${JSON.stringify(body)}\n`;
|
|
281
|
-
}
|
|
282
|
-
function respondJson(res, statusCode, body) {
|
|
283
|
-
return respond(res, statusCode, jsonBody(body), JSON_CONTENT_TYPE);
|
|
284
|
-
}
|
|
285
|
-
function notFound(res, widgetDocument = false) {
|
|
286
|
-
return respond(res, 404, "not found", "text/plain; charset=utf-8", widgetDocument ? { "Content-Security-Policy": DISCORD_ACTIVITY_WIDGET_CSP } : void 0);
|
|
287
|
-
}
|
|
288
|
-
function readHeader(req, name) {
|
|
289
|
-
const value = req.headers[name];
|
|
290
|
-
return Array.isArray(value) ? value[0] : value;
|
|
291
|
-
}
|
|
292
|
-
function extractApplicationId(req) {
|
|
293
|
-
for (const header of [readHeader(req, "origin"), readHeader(req, "referer")]) {
|
|
294
|
-
if (!header) continue;
|
|
295
|
-
try {
|
|
296
|
-
const match = new URL(header).hostname.match(/^(\d+)\.discordsays\.com$/i);
|
|
297
|
-
if (match?.[1]) return match[1];
|
|
298
|
-
} catch {}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function bearerToken(req) {
|
|
302
|
-
return ((readHeader(req, "authorization")?.trim())?.match(/^Bearer\s+([A-Za-z0-9_-]{43})$/i))?.[1];
|
|
303
|
-
}
|
|
304
|
-
function widgetIdFromCustomId(customId) {
|
|
305
|
-
if (WIDGET_ID_PATTERN.test(customId)) return customId;
|
|
306
|
-
return parseDiscordActivityCustomId(customId)?.widgetId;
|
|
307
|
-
}
|
|
308
|
-
function createDiscordActivityHttpHandler(deps) {
|
|
309
|
-
const fetchGuard = deps.fetchGuard ?? fetchWithSsrFGuard$1;
|
|
310
|
-
const limiter = new TokenRateLimiter(deps.now ?? Date.now);
|
|
311
|
-
const readVendorAsset = deps.readVendorAsset ?? ((assetPath) => fs.readFile(assetPath));
|
|
312
|
-
const reportError = deps.logError ?? logError;
|
|
313
|
-
const bodyTimeoutMs = deps.bodyTimeoutMs ?? WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
|
|
314
|
-
let vendorAsset;
|
|
315
|
-
let pendingLaunchFailureLogged = false;
|
|
316
|
-
function logPendingLaunchFailure(error) {
|
|
317
|
-
if (pendingLaunchFailureLogged) return;
|
|
318
|
-
pendingLaunchFailureLogged = true;
|
|
319
|
-
reportError(`discord activity: failed to consume pending launch: ${String(error)}`);
|
|
320
|
-
}
|
|
321
|
-
async function handleToken(req, res) {
|
|
322
|
-
const cfg = deps.runtime.currentConfig();
|
|
323
|
-
const sourceIp = resolveRequestClientIp(req, cfg.gateway?.trustedProxies, cfg.gateway?.allowRealIpFallback === true) ?? "unknown";
|
|
324
|
-
if (!limiter.allowKey(sourceIp)) return respondJson(res, 429, { error: "too many token requests" });
|
|
325
|
-
const applicationId = extractApplicationId(req);
|
|
326
|
-
const account = deps.runtime.resolveHttpAccount(applicationId);
|
|
327
|
-
if (!account) return respondJson(res, 503, { error: "Discord Activities is not fully configured" });
|
|
328
|
-
const endpointRuntime = getDiscordEndpointRuntime() ?? null;
|
|
329
|
-
const bodyResult = await readJsonBodyWithLimit(req, {
|
|
330
|
-
maxBytes: BODY_MAX_BYTES,
|
|
331
|
-
timeoutMs: bodyTimeoutMs,
|
|
332
|
-
emptyObjectOnEmpty: true,
|
|
333
|
-
destroyOnLimit: false
|
|
334
|
-
});
|
|
335
|
-
if (!bodyResult.ok && (bodyResult.code === "REQUEST_BODY_TIMEOUT" || bodyResult.code === "PAYLOAD_TOO_LARGE")) {
|
|
336
|
-
const timedOut = bodyResult.code === "REQUEST_BODY_TIMEOUT";
|
|
337
|
-
await sendHttpRequestRejection(req, res, timedOut ? 408 : 413, jsonBody({ error: timedOut ? "request body timeout" : "request body too large" }), JSON_CONTENT_TYPE);
|
|
338
|
-
return true;
|
|
339
|
-
}
|
|
340
|
-
const body = bodyResult.ok && bodyResult.value && typeof bodyResult.value === "object" && !Array.isArray(bodyResult.value) ? bodyResult.value : null;
|
|
341
|
-
const code = typeof body?.code === "string" ? body.code.trim() : "";
|
|
342
|
-
if (!code) return respondJson(res, 401, { error: "invalid authorization code" });
|
|
343
|
-
const reservation = limiter.reserveGlobal();
|
|
344
|
-
if (reservation === null) return respondJson(res, 429, { error: "too many token requests" });
|
|
345
|
-
let completed = false;
|
|
346
|
-
try {
|
|
347
|
-
let tokenResponse;
|
|
348
|
-
try {
|
|
349
|
-
tokenResponse = await fetchDiscordJson({
|
|
350
|
-
fetchGuard,
|
|
351
|
-
fetchImpl: account.proxyFetch,
|
|
352
|
-
url: DISCORD_TOKEN_URL,
|
|
353
|
-
init: {
|
|
354
|
-
method: "POST",
|
|
355
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
356
|
-
body: new URLSearchParams({
|
|
357
|
-
grant_type: "authorization_code",
|
|
358
|
-
client_id: account.applicationId,
|
|
359
|
-
client_secret: account.clientSecret,
|
|
360
|
-
code
|
|
361
|
-
})
|
|
362
|
-
},
|
|
363
|
-
auditContext: "discord.activities.oauth.token",
|
|
364
|
-
endpointRuntime
|
|
365
|
-
});
|
|
366
|
-
} catch {
|
|
367
|
-
return respondJson(res, 503, { error: "Discord token exchange unavailable" });
|
|
368
|
-
}
|
|
369
|
-
const granted = typeof tokenResponse.body?.access_token === "string" ? tokenResponse.body.access_token.trim() : "";
|
|
370
|
-
if (!tokenResponse.ok || !granted) return respondJson(res, 401, { error: "invalid authorization code" });
|
|
371
|
-
let userResponse;
|
|
372
|
-
try {
|
|
373
|
-
userResponse = await fetchDiscordJson({
|
|
374
|
-
fetchGuard,
|
|
375
|
-
fetchImpl: account.proxyFetch,
|
|
376
|
-
url: DISCORD_USER_URL,
|
|
377
|
-
init: { headers: { Authorization: `Bearer ${granted}` } },
|
|
378
|
-
auditContext: "discord.activities.oauth.user",
|
|
379
|
-
endpointRuntime
|
|
380
|
-
});
|
|
381
|
-
} catch {
|
|
382
|
-
return respondJson(res, 503, { error: "Discord user lookup unavailable" });
|
|
383
|
-
}
|
|
384
|
-
const discordUserId = typeof userResponse.body?.id === "string" ? userResponse.body.id : void 0;
|
|
385
|
-
if (!userResponse.ok || !discordUserId) return respondJson(res, 401, { error: "Discord user lookup failed" });
|
|
386
|
-
const minted = await deps.runtime.store.createSession({
|
|
387
|
-
discordUserId,
|
|
388
|
-
accountId: account.accountId
|
|
389
|
-
});
|
|
390
|
-
completed = true;
|
|
391
|
-
return respondJson(res, 200, {
|
|
392
|
-
access_token: granted,
|
|
393
|
-
session_token: minted
|
|
394
|
-
});
|
|
395
|
-
} finally {
|
|
396
|
-
if (!completed) limiter.releaseGlobal(reservation);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
async function handleWidget(req, res, url) {
|
|
400
|
-
const token = bearerToken(req);
|
|
401
|
-
const session = token ? await deps.runtime.store.lookupSession(token) : void 0;
|
|
402
|
-
if (!session) return respondJson(res, 401, { error: "invalid session" });
|
|
403
|
-
const customId = url.searchParams.get("custom_id")?.trim() ?? "";
|
|
404
|
-
const instanceId = normalizeInstanceId(url.searchParams.get("instance_id"));
|
|
405
|
-
const account = deps.runtime.resolveAccount(session.accountId);
|
|
406
|
-
const channelId = instanceId && account ? await resolveActivityInstanceChannel({
|
|
407
|
-
fetchGuard,
|
|
408
|
-
applicationId: account.applicationId,
|
|
409
|
-
instanceId,
|
|
410
|
-
discordUserId: session.discordUserId,
|
|
411
|
-
botAuth: account.botAuth,
|
|
412
|
-
proxyFetch: account.proxyFetch
|
|
413
|
-
}) : void 0;
|
|
414
|
-
if (!channelId) return respondJson(res, 404, { error: "widget not found" });
|
|
415
|
-
let resolved = null;
|
|
416
|
-
const requestedWidgetId = widgetIdFromCustomId(customId);
|
|
417
|
-
if (requestedWidgetId) {
|
|
418
|
-
const widget = await deps.runtime.store.lookupWidget(requestedWidgetId);
|
|
419
|
-
if (widget?.accountId !== session.accountId || widget.channelId !== channelId) return respondJson(res, 404, { error: "widget not found" });
|
|
420
|
-
resolved = {
|
|
421
|
-
id: requestedWidgetId,
|
|
422
|
-
widget
|
|
423
|
-
};
|
|
424
|
-
try {
|
|
425
|
-
await deps.runtime.store.retirePendingLaunch(session.accountId, channelId, session.discordUserId, requestedWidgetId);
|
|
426
|
-
} catch (error) {
|
|
427
|
-
logPendingLaunchFailure(error);
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
try {
|
|
431
|
-
const pendingLaunch = await deps.runtime.store.consumePendingLaunch(session.accountId, channelId, session.discordUserId);
|
|
432
|
-
if (pendingLaunch) {
|
|
433
|
-
const widget = await deps.runtime.store.lookupWidget(pendingLaunch.widgetId);
|
|
434
|
-
if (widget?.accountId === session.accountId && widget.channelId === channelId) resolved = {
|
|
435
|
-
id: pendingLaunch.widgetId,
|
|
436
|
-
widget
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
} catch (error) {
|
|
440
|
-
logPendingLaunchFailure(error);
|
|
441
|
-
}
|
|
442
|
-
resolved ??= await deps.runtime.store.latestPostedWidgetForChannel(session.accountId, channelId);
|
|
443
|
-
}
|
|
444
|
-
if (!resolved) return respondJson(res, 404, { error: "widget not found" });
|
|
445
|
-
const docToken = await deps.runtime.store.createDocToken({
|
|
446
|
-
widgetId: resolved.id,
|
|
447
|
-
accountId: session.accountId
|
|
448
|
-
});
|
|
449
|
-
return respondJson(res, 200, {
|
|
450
|
-
id: resolved.id,
|
|
451
|
-
title: resolved.widget.title,
|
|
452
|
-
docUrl: `${DISCORD_ACTIVITY_ROUTE_PREFIX}/api/widget/${encodeURIComponent(resolved.id)}/doc?wt=${encodeURIComponent(docToken)}`
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
async function handleDocument(res, widgetId, token) {
|
|
456
|
-
if (!WIDGET_ID_PATTERN.test(widgetId) || !DOC_TOKEN_PATTERN.test(token)) return notFound(res, true);
|
|
457
|
-
const capability = await deps.runtime.store.consumeDocToken(token);
|
|
458
|
-
if (!capability || capability.widgetId !== widgetId) return notFound(res, true);
|
|
459
|
-
const widget = await deps.runtime.store.lookupWidget(widgetId);
|
|
460
|
-
if (!widget || widget.accountId !== capability.accountId) return notFound(res, true);
|
|
461
|
-
return respond(res, 200, widget.html, "text/html; charset=utf-8", { "Content-Security-Policy": DISCORD_ACTIVITY_WIDGET_CSP });
|
|
462
|
-
}
|
|
463
|
-
return { async handleHttpRequest(req, res) {
|
|
464
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
465
|
-
if (url.pathname !== "/discord/activity" && !url.pathname.startsWith(`/discord/activity/`)) return false;
|
|
466
|
-
if (!deps.runtime.hasEnabledAccounts()) return false;
|
|
467
|
-
const relative = url.pathname.slice(17) || "/";
|
|
468
|
-
if (req.method === "GET" && (relative === "/" || relative === "/index.html")) return respond(res, 200, DISCORD_ACTIVITY_SHELL_HTML, "text/html; charset=utf-8", { "Content-Security-Policy": DISCORD_ACTIVITY_SHELL_CSP });
|
|
469
|
-
if (req.method === "GET" && relative === "/shell.js") return respond(res, 200, DISCORD_ACTIVITY_SHELL_JS, "text/javascript; charset=utf-8");
|
|
470
|
-
if (req.method === "GET" && relative === "/vendor/embedded-app-sdk.mjs") {
|
|
471
|
-
const pendingAsset = vendorAsset ??= readVendorAsset(deps.vendorAssetPath);
|
|
472
|
-
try {
|
|
473
|
-
return respond(res, 200, await pendingAsset, "text/javascript; charset=utf-8");
|
|
474
|
-
} catch {
|
|
475
|
-
if (vendorAsset === pendingAsset) vendorAsset = void 0;
|
|
476
|
-
return notFound(res);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (req.method === "POST" && relative === "/api/token") return await handleToken(req, res);
|
|
480
|
-
if (req.method === "GET" && relative === "/api/widget") return await handleWidget(req, res, url);
|
|
481
|
-
const documentMatch = relative.match(/^\/api\/widget\/([^/]+)\/doc$/);
|
|
482
|
-
if (req.method === "GET" && documentMatch?.[1]) {
|
|
483
|
-
let widgetId;
|
|
484
|
-
try {
|
|
485
|
-
widgetId = decodeURIComponent(documentMatch[1]);
|
|
486
|
-
} catch {
|
|
487
|
-
return notFound(res, true);
|
|
488
|
-
}
|
|
489
|
-
return await handleDocument(res, widgetId, url.searchParams.get("wt") ?? "");
|
|
490
|
-
}
|
|
491
|
-
return notFound(res);
|
|
492
|
-
} };
|
|
493
|
-
}
|
|
494
|
-
//#endregion
|
|
495
|
-
//#region extensions/discord/src/activities/presenter.ts
|
|
496
|
-
const DISCORD_WIDGET_HTML_MAX_BYTES = 49152;
|
|
497
|
-
function resolveDiscordChannelId(context) {
|
|
498
|
-
const raw = context.nativeChannelId?.trim() || context.currentMessagingTarget?.trim() || context.currentChannelId?.trim() || context.deliveryContext?.to?.trim();
|
|
499
|
-
if (!raw) return;
|
|
500
|
-
try {
|
|
501
|
-
return resolveDiscordChannelId$1(raw);
|
|
502
|
-
} catch {
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function resolveDiscordPresentationRoute(context, runtime) {
|
|
507
|
-
if (context.messageChannel !== "discord") return;
|
|
508
|
-
const cfg = runtime.currentConfig();
|
|
509
|
-
const account = resolveDiscordAccount({
|
|
510
|
-
cfg,
|
|
511
|
-
accountId: context.accountId ?? context.deliveryContext?.accountId
|
|
512
|
-
});
|
|
513
|
-
const channelId = resolveDiscordChannelId(context);
|
|
514
|
-
const activityAccount = runtime.resolveAccount(account.accountId, cfg);
|
|
515
|
-
if (!channelId || !activityAccount) return;
|
|
516
|
-
return {
|
|
517
|
-
account: activityAccount,
|
|
518
|
-
cfg,
|
|
519
|
-
channelId
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
/** Presents a canonical core widget document in the active Discord channel. */
|
|
523
|
-
function createDiscordWidgetPresenter(runtime, deps = {}) {
|
|
524
|
-
return {
|
|
525
|
-
target: "current_channel",
|
|
526
|
-
description: "Post an Activity launch button in the current Discord channel",
|
|
527
|
-
capabilities: {
|
|
528
|
-
sourceKinds: ["html"],
|
|
529
|
-
maxSourceBytes: DISCORD_WIDGET_HTML_MAX_BYTES
|
|
530
|
-
},
|
|
531
|
-
match: (context) => resolveDiscordPresentationRoute(context, runtime) !== void 0,
|
|
532
|
-
async availability(context) {
|
|
533
|
-
return resolveDiscordPresentationRoute(context, runtime) ? {
|
|
534
|
-
ok: true,
|
|
535
|
-
value: { available: true }
|
|
536
|
-
} : {
|
|
537
|
-
ok: false,
|
|
538
|
-
error: {
|
|
539
|
-
code: "unavailable",
|
|
540
|
-
message: "Discord Activities are unavailable for the current channel and account."
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
},
|
|
544
|
-
async present({ context, document, title }) {
|
|
545
|
-
const route = resolveDiscordPresentationRoute(context, runtime);
|
|
546
|
-
if (!route) return {
|
|
547
|
-
ok: false,
|
|
548
|
-
error: {
|
|
549
|
-
code: "unavailable",
|
|
550
|
-
message: "Discord Activities are unavailable for the current channel and account."
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
if (Array.from(title).length > 80) return {
|
|
554
|
-
ok: false,
|
|
555
|
-
error: {
|
|
556
|
-
code: "presentation_error",
|
|
557
|
-
message: "title must be 80 characters or fewer"
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
const widgetId = await runtime.store.createWidget({
|
|
561
|
-
html: document.html,
|
|
562
|
-
title,
|
|
563
|
-
channelId: route.channelId,
|
|
564
|
-
accountId: route.account.accountId,
|
|
565
|
-
createdAt: (deps.now ?? Date.now)()
|
|
566
|
-
});
|
|
567
|
-
let result;
|
|
568
|
-
let deliveredResult;
|
|
569
|
-
let deliveryRecord;
|
|
570
|
-
let deliveryRecordError;
|
|
571
|
-
const recordDelivery = async (deliveryResult) => {
|
|
572
|
-
deliveredResult = deliveryResult;
|
|
573
|
-
deliveryRecord ??= runtime.store.markWidgetDelivered(widgetId, deliveryResult.messageId);
|
|
574
|
-
try {
|
|
575
|
-
await deliveryRecord;
|
|
576
|
-
} catch (error) {
|
|
577
|
-
deliveryRecordError ??= new Error("Discord widget was delivered, but its delivery state could not be saved", { cause: error });
|
|
578
|
-
throw deliveryRecordError;
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
try {
|
|
582
|
-
const components = buildDiscordPresentationComponents({ blocks: [{
|
|
583
|
-
type: "buttons",
|
|
584
|
-
buttons: [{
|
|
585
|
-
label: "Open widget",
|
|
586
|
-
action: {
|
|
587
|
-
type: "web-app",
|
|
588
|
-
widgetId
|
|
589
|
-
}
|
|
590
|
-
}]
|
|
591
|
-
}] });
|
|
592
|
-
if (!components) throw new Error("Discord widget launch button could not be rendered");
|
|
593
|
-
result = await (deps.sendComponentMessage ?? sendDiscordComponentMessage)(`channel:${route.channelId}`, {
|
|
594
|
-
...components,
|
|
595
|
-
text: title
|
|
596
|
-
}, {
|
|
597
|
-
cfg: route.cfg,
|
|
598
|
-
accountId: route.account.accountId,
|
|
599
|
-
allowedMentions: { parse: [] },
|
|
600
|
-
onDeliveryResult: recordDelivery
|
|
601
|
-
});
|
|
602
|
-
await recordDelivery(result);
|
|
603
|
-
} catch (error) {
|
|
604
|
-
if (deliveryRecordError) throw deliveryRecordError;
|
|
605
|
-
if (!deliveredResult) {
|
|
606
|
-
await runtime.store.deleteWidget(widgetId);
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
609
|
-
result = deliveredResult;
|
|
610
|
-
}
|
|
611
|
-
return {
|
|
612
|
-
ok: true,
|
|
613
|
-
value: {
|
|
614
|
-
kind: "message",
|
|
615
|
-
receipt: result.receipt
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
//#endregion
|
|
622
|
-
//#region extensions/discord/src/activities/store.ts
|
|
623
|
-
const DAY_MS = 864e5;
|
|
624
|
-
const DISCORD_EPOCH_MS = 14200704e5;
|
|
625
|
-
const WIDGET_TTL_MS = 7 * DAY_MS;
|
|
626
|
-
const SESSION_TTL_MS = 9e5;
|
|
627
|
-
const DOC_TOKEN_TTL_MS = 6e4;
|
|
628
|
-
const PENDING_LAUNCH_TTL_MS = 12e4;
|
|
629
|
-
function requireAtomicComparison(store) {
|
|
630
|
-
if (!store.observe || !store.compareAndApply) throw new Error("Discord Activities require atomic plugin state comparisons");
|
|
631
|
-
return store;
|
|
632
|
-
}
|
|
633
|
-
function openDiscordActivityStores(openKeyedStore) {
|
|
634
|
-
return {
|
|
635
|
-
widgets: requireAtomicComparison(openKeyedStore({
|
|
636
|
-
namespace: "activities-widgets",
|
|
637
|
-
maxEntries: 64,
|
|
638
|
-
overflowPolicy: "evict-oldest",
|
|
639
|
-
defaultTtlMs: WIDGET_TTL_MS
|
|
640
|
-
})),
|
|
641
|
-
sessions: openKeyedStore({
|
|
642
|
-
namespace: "activities-sessions",
|
|
643
|
-
maxEntries: 256,
|
|
644
|
-
overflowPolicy: "evict-oldest",
|
|
645
|
-
defaultTtlMs: SESSION_TTL_MS
|
|
646
|
-
}),
|
|
647
|
-
docTokens: openKeyedStore({
|
|
648
|
-
namespace: "activities-doc-tokens",
|
|
649
|
-
maxEntries: 256,
|
|
650
|
-
overflowPolicy: "evict-oldest",
|
|
651
|
-
defaultTtlMs: DOC_TOKEN_TTL_MS
|
|
652
|
-
}),
|
|
653
|
-
launches: requireAtomicComparison(openKeyedStore({
|
|
654
|
-
namespace: "activities-launches",
|
|
655
|
-
maxEntries: 256,
|
|
656
|
-
overflowPolicy: "evict-oldest",
|
|
657
|
-
defaultTtlMs: PENDING_LAUNCH_TTL_MS
|
|
658
|
-
}))
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
function pendingLaunchKey(accountId, channelId, discordUserId) {
|
|
662
|
-
return `${accountId}:${channelId}:${discordUserId}`;
|
|
663
|
-
}
|
|
664
|
-
async function applyActivityStoreIntent(store, key, intent) {
|
|
665
|
-
let observed = await store.observe(key);
|
|
666
|
-
while (true) {
|
|
667
|
-
const result = await store.compareAndApply(key, observed.comparison, intent(observed.value));
|
|
668
|
-
if (result.status !== "conflict") return result.status;
|
|
669
|
-
observed = result.current;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
function pendingLaunchForWidget(existing, widgetId, createdAt) {
|
|
673
|
-
return existing && (existing.state === "ambiguous" || existing.widgetId !== widgetId) ? {
|
|
674
|
-
state: "ambiguous",
|
|
675
|
-
createdAt
|
|
676
|
-
} : {
|
|
677
|
-
state: "single",
|
|
678
|
-
widgetId,
|
|
679
|
-
createdAt
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
var DiscordActivityStore = class {
|
|
683
|
-
constructor(stores) {
|
|
684
|
-
this.stores = stores;
|
|
685
|
-
this.lastWidgetCreatedAt = 0;
|
|
686
|
-
}
|
|
687
|
-
async createWidget(value) {
|
|
688
|
-
const id = randomBytes(16).toString("base64url");
|
|
689
|
-
const createdAt = Math.max(value.createdAt, this.lastWidgetCreatedAt + 1);
|
|
690
|
-
this.lastWidgetCreatedAt = createdAt;
|
|
691
|
-
await this.stores.widgets.register(id, {
|
|
692
|
-
...value,
|
|
693
|
-
createdAt,
|
|
694
|
-
deliveredMessageId: null
|
|
695
|
-
});
|
|
696
|
-
return id;
|
|
697
|
-
}
|
|
698
|
-
async markWidgetDelivered(id, messageId) {
|
|
699
|
-
if (!/^\d+$/u.test(messageId)) throw new Error("Discord Activity delivery returned an invalid message ID");
|
|
700
|
-
if (await applyActivityStoreIntent(this.stores.widgets, id, (widget) => widget ? {
|
|
701
|
-
operation: "update",
|
|
702
|
-
action: "set",
|
|
703
|
-
value: {
|
|
704
|
-
...widget,
|
|
705
|
-
deliveredMessageId: messageId
|
|
706
|
-
}
|
|
707
|
-
} : {
|
|
708
|
-
operation: "update",
|
|
709
|
-
action: "keep"
|
|
710
|
-
}) === "unchanged") throw new Error("Discord Activity widget disappeared before delivery was recorded");
|
|
711
|
-
}
|
|
712
|
-
async deleteWidget(id) {
|
|
713
|
-
await this.stores.widgets.delete(id);
|
|
714
|
-
}
|
|
715
|
-
async lookupWidget(id) {
|
|
716
|
-
return await this.stores.widgets.lookup(id);
|
|
717
|
-
}
|
|
718
|
-
async latestPostedWidgetForChannel(accountId, channelId) {
|
|
719
|
-
const entries = await this.stores.widgets.entries();
|
|
720
|
-
let match;
|
|
721
|
-
for (const entry of entries) {
|
|
722
|
-
if (entry.value.accountId !== accountId || entry.value.channelId !== channelId) continue;
|
|
723
|
-
if (entry.value.deliveredMessageId === null) continue;
|
|
724
|
-
const deliveryOrder = entry.value.deliveredMessageId ? BigInt(entry.value.deliveredMessageId) : BigInt(Math.max(0, Math.trunc(entry.value.createdAt - DISCORD_EPOCH_MS))) << 22n;
|
|
725
|
-
if (!match || deliveryOrder > match.deliveryOrder) match = {
|
|
726
|
-
entry,
|
|
727
|
-
deliveryOrder
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
return match ? {
|
|
731
|
-
id: match.entry.key,
|
|
732
|
-
widget: match.entry.value
|
|
733
|
-
} : null;
|
|
734
|
-
}
|
|
735
|
-
async createSession(value) {
|
|
736
|
-
const token = randomBytes(32).toString("base64url");
|
|
737
|
-
await this.stores.sessions.register(token, value);
|
|
738
|
-
return token;
|
|
739
|
-
}
|
|
740
|
-
async lookupSession(token) {
|
|
741
|
-
return await this.stores.sessions.lookup(token);
|
|
742
|
-
}
|
|
743
|
-
async createDocToken(value) {
|
|
744
|
-
const token = randomBytes(32).toString("base64url");
|
|
745
|
-
await this.stores.docTokens.register(token, value);
|
|
746
|
-
return token;
|
|
747
|
-
}
|
|
748
|
-
async consumeDocToken(token) {
|
|
749
|
-
return await this.stores.docTokens.consume(token);
|
|
750
|
-
}
|
|
751
|
-
async recordPendingLaunch(params) {
|
|
752
|
-
const key = pendingLaunchKey(params.accountId, params.channelId, params.discordUserId);
|
|
753
|
-
const { widgetId, createdAt } = params;
|
|
754
|
-
await applyActivityStoreIntent(this.stores.launches, key, (existing) => ({
|
|
755
|
-
operation: "update",
|
|
756
|
-
action: "set",
|
|
757
|
-
value: pendingLaunchForWidget(existing, widgetId, createdAt)
|
|
758
|
-
}));
|
|
759
|
-
}
|
|
760
|
-
async retirePendingLaunch(accountId, channelId, discordUserId, widgetId) {
|
|
761
|
-
const key = pendingLaunchKey(accountId, channelId, discordUserId);
|
|
762
|
-
await applyActivityStoreIntent(this.stores.launches, key, (existing) => ({
|
|
763
|
-
operation: "delete",
|
|
764
|
-
action: existing?.state === "single" && existing.widgetId === widgetId ? "delete" : "keep"
|
|
765
|
-
}));
|
|
766
|
-
}
|
|
767
|
-
async consumePendingLaunch(accountId, channelId, discordUserId) {
|
|
768
|
-
const launch = await this.stores.launches.consume(pendingLaunchKey(accountId, channelId, discordUserId));
|
|
769
|
-
return launch?.state === "single" ? launch : void 0;
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
//#endregion
|
|
773
|
-
//#region extensions/discord/src/activities/register.ts
|
|
774
|
-
function registerDiscordActivities(api) {
|
|
775
|
-
setDiscordActivitiesRuntime(void 0);
|
|
776
|
-
const store = new DiscordActivityStore(openDiscordActivityStores(api.runtime.state.openKeyedStore.bind(api.runtime.state)));
|
|
777
|
-
const runtime = new DiscordActivitiesRuntime(store, api.config, api.runtime.config?.current ? () => api.runtime.config.current() : void 0);
|
|
778
|
-
setDiscordActivitiesRuntime(runtime);
|
|
779
|
-
const http = createDiscordActivityHttpHandler({
|
|
780
|
-
runtime,
|
|
781
|
-
vendorAssetPath: api.resolvePath("assets/embedded-app-sdk.mjs")
|
|
782
|
-
});
|
|
783
|
-
api.registerHttpRoute({
|
|
784
|
-
path: DISCORD_ACTIVITY_ROUTE_PREFIX,
|
|
785
|
-
auth: "plugin",
|
|
786
|
-
match: "prefix",
|
|
787
|
-
handler: async (req, res) => await http.handleHttpRequest(req, res)
|
|
788
|
-
});
|
|
789
|
-
api.registerWidgetPresenter(createDiscordWidgetPresenter(runtime));
|
|
790
|
-
}
|
|
791
|
-
//#endregion
|
|
792
|
-
export { registerDiscordActivities as t };
|