@finalai/final-agent-provider 0.7.0 → 0.9.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/HOSTING.md +3 -3
- package/README.md +26 -91
- package/dist/modules/channel-factories.d.mts +5 -6
- package/dist/modules/channel-factories.mjs +7 -11
- package/dist/modules/channel-factories.mjs.map +3 -3
- package/dist/modules/channels/inbound.mjs +2 -2
- package/dist/modules/channels/inbound.mjs.map +2 -2
- package/dist/modules/channels/index.d.mts +0 -7
- package/dist/modules/channels/index.mjs +0 -7
- package/dist/modules/channels/index.mjs.map +2 -2
- package/dist/modules/channels/prompt.d.mts +2 -2
- package/dist/modules/channels/prompt.mjs +3 -3
- package/dist/modules/channels/prompt.mjs.map +2 -2
- package/dist/modules/channels/routing.d.mts +1 -1
- package/dist/modules/channels/routing.mjs +1 -1
- package/dist/modules/channels/routing.mjs.map +1 -1
- package/dist/modules/sandboxes/bytes.d.mts +1 -0
- package/dist/modules/sandboxes/bytes.mjs +7 -1
- package/dist/modules/sandboxes/bytes.mjs.map +2 -2
- package/dist/modules/sandboxes/computer-processes.mjs +5 -3
- package/dist/modules/sandboxes/computer-processes.mjs.map +2 -2
- package/dist/modules/sandboxes/computer.mjs +5 -1
- package/dist/modules/sandboxes/computer.mjs.map +2 -2
- package/dist/modules/sandboxes/computers-api-transport.d.mts +1 -1
- package/dist/modules/sandboxes/computers-api-transport.mjs +8 -0
- package/dist/modules/sandboxes/computers-api-transport.mjs.map +2 -2
- package/dist/modules/sandboxes/device-api.mjs +8 -0
- package/dist/modules/sandboxes/device-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device.mjs +6 -0
- package/dist/modules/sandboxes/device.mjs.map +2 -2
- package/dist/modules/sandboxes/session.d.mts +1 -0
- package/dist/modules/sandboxes/session.mjs +106 -36
- package/dist/modules/sandboxes/session.mjs.map +3 -3
- package/dist/modules/sandboxes/testing/conformance.mjs +3 -0
- package/dist/modules/sandboxes/testing/conformance.mjs.map +2 -2
- package/dist/modules/sandboxes/testing/fake-machine.mjs +11 -0
- package/dist/modules/sandboxes/testing/fake-machine.mjs.map +2 -2
- package/dist/modules/sandboxes/transport.d.mts +1 -0
- package/dist/modules/sandboxes/transport.mjs.map +2 -2
- package/package.json +4 -40
- package/dist/modules/channels/channel-utils.d.mts +0 -132
- package/dist/modules/channels/channel-utils.mjs +0 -445
- package/dist/modules/channels/channel-utils.mjs.map +0 -7
- package/dist/modules/channels/discord-dispatch.d.mts +0 -17
- package/dist/modules/channels/discord-dispatch.mjs +0 -139
- package/dist/modules/channels/discord-dispatch.mjs.map +0 -7
- package/dist/modules/channels/discord-gateway.d.mts +0 -19
- package/dist/modules/channels/discord-gateway.mjs +0 -233
- package/dist/modules/channels/discord-gateway.mjs.map +0 -7
- package/dist/modules/channels/discord.d.mts +0 -9
- package/dist/modules/channels/discord.mjs +0 -904
- package/dist/modules/channels/discord.mjs.map +0 -7
- package/dist/modules/channels/github.d.mts +0 -5
- package/dist/modules/channels/github.mjs +0 -720
- package/dist/modules/channels/github.mjs.map +0 -7
- package/dist/modules/channels/linear.d.mts +0 -4
- package/dist/modules/channels/linear.mjs +0 -621
- package/dist/modules/channels/linear.mjs.map +0 -7
- package/dist/modules/channels/phone.d.mts +0 -8
- package/dist/modules/channels/phone.mjs +0 -382
- package/dist/modules/channels/phone.mjs.map +0 -7
- package/dist/modules/channels/reply-receipts.d.mts +0 -31
- package/dist/modules/channels/reply-receipts.mjs +0 -63
- package/dist/modules/channels/reply-receipts.mjs.map +0 -7
- package/dist/modules/channels/slack.d.mts +0 -7
- package/dist/modules/channels/slack.mjs +0 -1134
- package/dist/modules/channels/slack.mjs.map +0 -7
- package/dist/modules/channels/voice-recording.d.mts +0 -7
- package/dist/modules/channels/voice-recording.mjs +0 -49
- package/dist/modules/channels/voice-recording.mjs.map +0 -7
- package/dist/modules/channels/voice.d.mts +0 -33
- package/dist/modules/channels/voice.mjs +0 -851
- package/dist/modules/channels/voice.mjs.map +0 -7
- package/dist/modules/channels/webhook.d.mts +0 -5
- package/dist/modules/channels/webhook.mjs +0 -76
- package/dist/modules/channels/webhook.mjs.map +0 -7
|
@@ -1,904 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineChannel,
|
|
3
|
-
packageRef,
|
|
4
|
-
placeRefSchema,
|
|
5
|
-
jsonValueSchema
|
|
6
|
-
} from "@finalai/agent-provider";
|
|
7
|
-
import * as z from "zod";
|
|
8
|
-
import {
|
|
9
|
-
mirrorReceiptKey,
|
|
10
|
-
settleMirrorReaction,
|
|
11
|
-
shareReplyReceipt
|
|
12
|
-
} from "./reply-receipts.mjs";
|
|
13
|
-
import { createDiscordDispatch } from "./discord-dispatch.mjs";
|
|
14
|
-
import { runDiscordGateway } from "./discord-gateway.mjs";
|
|
15
|
-
import {
|
|
16
|
-
channelBaseUrl,
|
|
17
|
-
addSharedReaction,
|
|
18
|
-
removeSharedReaction,
|
|
19
|
-
providerPlaceKey,
|
|
20
|
-
confirmOAuth,
|
|
21
|
-
deliveryRetention,
|
|
22
|
-
createQueue,
|
|
23
|
-
decisionChoices,
|
|
24
|
-
decisionSummary,
|
|
25
|
-
externalKey,
|
|
26
|
-
freshTimestamp,
|
|
27
|
-
hexBytes,
|
|
28
|
-
parseJson,
|
|
29
|
-
personFor,
|
|
30
|
-
receivePersonMessage,
|
|
31
|
-
refString,
|
|
32
|
-
responseJson,
|
|
33
|
-
startOAuth,
|
|
34
|
-
stateKey,
|
|
35
|
-
takeOAuth,
|
|
36
|
-
textOf,
|
|
37
|
-
completionText
|
|
38
|
-
} from "./channel-utils.mjs";
|
|
39
|
-
const configSchema = z.object({
|
|
40
|
-
baseUrl: z.string().url().optional(),
|
|
41
|
-
publicKey: z.string().optional(),
|
|
42
|
-
botId: z.string().optional(),
|
|
43
|
-
history: z.union([z.literal("none"), z.literal("linked-members")]).default("none")
|
|
44
|
-
});
|
|
45
|
-
const missingMessage = /* @__PURE__ */ Symbol("missing Discord message");
|
|
46
|
-
const missingReactionMessage = /* @__PURE__ */ Symbol("missing Discord reaction message");
|
|
47
|
-
const api = async (ctx, path, body, method) => {
|
|
48
|
-
const app = await ctx.credentials.app();
|
|
49
|
-
const response = await ctx.credentials.fetch(`https://discord.com/api/v10${path}`, {
|
|
50
|
-
signal: ctx.signal,
|
|
51
|
-
method: method ?? (body == null ? "GET" : "POST"),
|
|
52
|
-
headers: { authorization: `Bot ${app.accessToken}`, "content-type": "application/json" },
|
|
53
|
-
...body == null ? {} : { body: JSON.stringify(body) }
|
|
54
|
-
});
|
|
55
|
-
if (response.status === 400 || response.status === 404) {
|
|
56
|
-
const text = await response.text();
|
|
57
|
-
const error = z.object({ code: z.number() }).safeParse(parseJson(text));
|
|
58
|
-
const code = error.success ? error.data.code : null;
|
|
59
|
-
if (response.status === 404 && (method === "DELETE" || method === "PATCH"))
|
|
60
|
-
return path.includes("/reactions/") && code === 10008 ? missingReactionMessage : missingMessage;
|
|
61
|
-
throw new Error(`Channel request failed (${response.status}): ${text.slice(0, 500)}`, {
|
|
62
|
-
cause: { status: response.status, code }
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return responseJson(response);
|
|
66
|
-
};
|
|
67
|
-
const classifyMirrorReaction = async (cause) => {
|
|
68
|
-
const error = z.object({ status: z.number(), code: z.number().nullable().optional() }).safeParse(cause instanceof Error ? cause.cause : null);
|
|
69
|
-
if (!error.success) return "retry";
|
|
70
|
-
if (error.data.status === 429 || error.data.status >= 500) return "retry";
|
|
71
|
-
if (error.data.code === 10008) return "missing";
|
|
72
|
-
if (error.data.code === 10014 || error.data.code === 30010) return "unavailable";
|
|
73
|
-
return error.data.status === 403 || error.data.status === 404 ? "unavailable" : "retry";
|
|
74
|
-
};
|
|
75
|
-
const botFor = async (ctx, config) => {
|
|
76
|
-
if (config.botId != null) return config.botId;
|
|
77
|
-
const cached = z.string().safeParse(await ctx.store.get("discord-bot-id"));
|
|
78
|
-
if (cached.success) return cached.data;
|
|
79
|
-
const id = z.object({ id: z.string() }).parse(await api(ctx, "/users/@me")).id;
|
|
80
|
-
await ctx.store.set("discord-bot-id", id, deliveryRetention);
|
|
81
|
-
return id;
|
|
82
|
-
};
|
|
83
|
-
const confirmDecision = async (ctx, decisionId) => {
|
|
84
|
-
await ctx.store.set(stateKey("discord-decision-resolved", decisionId), true, {
|
|
85
|
-
ttlSeconds: 3600
|
|
86
|
-
});
|
|
87
|
-
const saved = z.object({ channel: z.string(), messageId: z.string() }).safeParse(await ctx.store.get(stateKey("discord-decision-delivered", decisionId)));
|
|
88
|
-
if (!saved.success) return;
|
|
89
|
-
try {
|
|
90
|
-
await api(
|
|
91
|
-
ctx,
|
|
92
|
-
`/channels/${saved.data.channel}/messages/${saved.data.messageId}`,
|
|
93
|
-
{
|
|
94
|
-
content: "This decision has been answered in Final.",
|
|
95
|
-
components: [],
|
|
96
|
-
allowed_mentions: { parse: [] }
|
|
97
|
-
},
|
|
98
|
-
"PATCH"
|
|
99
|
-
);
|
|
100
|
-
} catch {
|
|
101
|
-
ctx.log("Discord decision confirmation unavailable.");
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const normalizedText = (text, bot) => text.replace(/<@!?([0-9A-Za-z_-]+)>/g, (_match, id) => id === bot ? "" : `@${id}`).trim();
|
|
105
|
-
const messageSchema = z.object({
|
|
106
|
-
id: z.string(),
|
|
107
|
-
channel_id: z.string(),
|
|
108
|
-
guild_id: z.string().optional(),
|
|
109
|
-
content: z.string().default(""),
|
|
110
|
-
author: z.object({ id: z.string(), username: z.string(), bot: z.boolean().optional() }),
|
|
111
|
-
mentions: z.array(z.object({ id: z.string() })).default([])
|
|
112
|
-
});
|
|
113
|
-
const replyKey = (ctx, replyOf) => stateKey(
|
|
114
|
-
"discord-reply",
|
|
115
|
-
replyOf?.externalKey ?? refString(ctx.origin.ref, "externalKey") ?? ctx.origin.place.id,
|
|
116
|
-
replyOf?.turnId ?? ctx.turn.id
|
|
117
|
-
);
|
|
118
|
-
const reply = async (ctx, text, final, key = replyKey(ctx)) => {
|
|
119
|
-
const channel = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
120
|
-
if (channel == null || text.length === 0) return;
|
|
121
|
-
const saved = z.object({
|
|
122
|
-
id: z.string(),
|
|
123
|
-
updatedAt: z.number(),
|
|
124
|
-
final: z.boolean(),
|
|
125
|
-
text: z.string().optional()
|
|
126
|
-
}).safeParse(await ctx.store.get(key));
|
|
127
|
-
if (saved.success && saved.data.final) return;
|
|
128
|
-
if (saved.success && !final && Date.now() - saved.data.updatedAt < 1e3) {
|
|
129
|
-
await ctx.store.set(key, { ...saved.data, text: text.slice(0, 1900) }, deliveryRetention);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const content = text.length > 1900 ? `${text.slice(0, 1850)}
|
|
133
|
-
Continue in Final \u2192` : text;
|
|
134
|
-
const data = await api(
|
|
135
|
-
ctx,
|
|
136
|
-
`/channels/${channel}/messages${saved.success ? `/${saved.data.id}` : ""}`,
|
|
137
|
-
{ content, allowed_mentions: { parse: [] } },
|
|
138
|
-
saved.success ? "PATCH" : "POST"
|
|
139
|
-
);
|
|
140
|
-
if (saved.success && data === missingMessage) {
|
|
141
|
-
await ctx.store.set(key, null);
|
|
142
|
-
await reply(ctx, text, final, key);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const id = saved.success ? saved.data.id : z.object({ id: z.string() }).parse(data).id;
|
|
146
|
-
await ctx.store.set(
|
|
147
|
-
key,
|
|
148
|
-
{ id, updatedAt: Date.now(), final, text: text.slice(0, 1900) },
|
|
149
|
-
deliveryRetention
|
|
150
|
-
);
|
|
151
|
-
await ctx.store.set(
|
|
152
|
-
stateKey("discord-origin", channel, id),
|
|
153
|
-
jsonValueSchema.parse({
|
|
154
|
-
...refString(ctx.origin.ref, "requester") == null ? {} : { principal: { id: refString(ctx.origin.ref, "requester") } },
|
|
155
|
-
place: ctx.origin.place,
|
|
156
|
-
externalKey: refString(ctx.origin.ref, "externalKey") ?? ctx.origin.place.id,
|
|
157
|
-
turnId: ctx.turn.id
|
|
158
|
-
}),
|
|
159
|
-
deliveryRetention
|
|
160
|
-
);
|
|
161
|
-
};
|
|
162
|
-
const savedPartial = async (ctx) => {
|
|
163
|
-
const receipt = z.object({ text: z.string() }).safeParse(await ctx.store.get(replyKey(ctx)));
|
|
164
|
-
return receipt.success ? receipt.data.text : "";
|
|
165
|
-
};
|
|
166
|
-
const sharedReply = async (ctx, args) => {
|
|
167
|
-
const channel = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
168
|
-
await shareReplyReceipt({
|
|
169
|
-
bindingId: ctx.binding.id,
|
|
170
|
-
store: ctx.store,
|
|
171
|
-
destination: stateKey("discord", channel),
|
|
172
|
-
messageIds: args.messageIds,
|
|
173
|
-
originKey: replyKey(ctx),
|
|
174
|
-
send: (key) => reply(ctx, args.text, args.final, key),
|
|
175
|
-
removeDuplicate: async (receipt, canonical) => {
|
|
176
|
-
const schema = z.object({ id: z.string() });
|
|
177
|
-
const saved = schema.safeParse(receipt);
|
|
178
|
-
const target = schema.safeParse(canonical);
|
|
179
|
-
if (saved.success && target.success && saved.data.id !== target.data.id)
|
|
180
|
-
await api(ctx, `/channels/${channel}/messages/${saved.data.id}`, void 0, "DELETE");
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
};
|
|
184
|
-
const discordChannel = (options = {}) => {
|
|
185
|
-
const config = configSchema.parse(options);
|
|
186
|
-
return defineChannel({
|
|
187
|
-
name: "discord",
|
|
188
|
-
config,
|
|
189
|
-
installed: packageRef("@finalai/final-agent-provider/channels/discord"),
|
|
190
|
-
credentials: { app: "discord" },
|
|
191
|
-
identity: {
|
|
192
|
-
kind: "redirect",
|
|
193
|
-
start: async (args, ctx) => {
|
|
194
|
-
const app = await ctx.credentials.appInfo();
|
|
195
|
-
return startOAuth(
|
|
196
|
-
{
|
|
197
|
-
...args,
|
|
198
|
-
authorizeUrl: "https://discord.com/oauth2/authorize",
|
|
199
|
-
clientId: app.clientId,
|
|
200
|
-
redirectUri: `${channelBaseUrl(config)}/b/${ctx.binding.id}/oauth/callback`,
|
|
201
|
-
scope: "identify"
|
|
202
|
-
},
|
|
203
|
-
ctx
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
places: {
|
|
208
|
-
kinds: ["dm", "channel", "thread"],
|
|
209
|
-
defaults: (place) => place.kind === "dm" ? { create: "direct-with-agent", replies: "origin", audience: "members" } : null
|
|
210
|
-
},
|
|
211
|
-
routes: [
|
|
212
|
-
{
|
|
213
|
-
method: "GET",
|
|
214
|
-
path: "/oauth/callback",
|
|
215
|
-
handler: async (ctx) => {
|
|
216
|
-
const saved = await takeOAuth(ctx.request, ctx);
|
|
217
|
-
if (saved == null)
|
|
218
|
-
return new Response("Invalid or expired link request", { status: 400 });
|
|
219
|
-
const app = await ctx.credentials.appInfo();
|
|
220
|
-
const token = z.object({ access_token: z.string() }).parse(
|
|
221
|
-
await responseJson(
|
|
222
|
-
await ctx.credentials.fetch("https://discord.com/api/oauth2/token", {
|
|
223
|
-
method: "POST",
|
|
224
|
-
body: new URLSearchParams({
|
|
225
|
-
client_id: app.clientId,
|
|
226
|
-
client_secret: app.clientSecret,
|
|
227
|
-
grant_type: "authorization_code",
|
|
228
|
-
code: saved.code,
|
|
229
|
-
redirect_uri: saved.redirectUri
|
|
230
|
-
})
|
|
231
|
-
})
|
|
232
|
-
)
|
|
233
|
-
);
|
|
234
|
-
const user = z.object({ id: z.string(), username: z.string() }).parse(
|
|
235
|
-
await responseJson(
|
|
236
|
-
await ctx.credentials.fetch("https://discord.com/api/users/@me", {
|
|
237
|
-
headers: { authorization: `Bearer ${token.access_token}` }
|
|
238
|
-
})
|
|
239
|
-
)
|
|
240
|
-
);
|
|
241
|
-
return confirmOAuth(
|
|
242
|
-
{
|
|
243
|
-
ticket: saved.ticket,
|
|
244
|
-
expected: saved.principal,
|
|
245
|
-
actual: { id: user.id, displayName: user.username }
|
|
246
|
-
},
|
|
247
|
-
ctx
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
method: "POST",
|
|
253
|
-
path: "/webhook",
|
|
254
|
-
handler: async (ctx) => {
|
|
255
|
-
const body = await ctx.request.text();
|
|
256
|
-
const timestamp = ctx.request.headers.get("x-signature-timestamp");
|
|
257
|
-
const signature = hexBytes(ctx.request.headers.get("x-signature-ed25519") ?? "");
|
|
258
|
-
const bytes = hexBytes(
|
|
259
|
-
config.publicKey ?? (await ctx.credentials.appInfo()).signingSecret ?? ""
|
|
260
|
-
);
|
|
261
|
-
if (!freshTimestamp(timestamp) || signature == null || bytes == null)
|
|
262
|
-
return new Response(null, { status: 401 });
|
|
263
|
-
const key = await crypto.subtle.importKey(
|
|
264
|
-
"raw",
|
|
265
|
-
new Uint8Array(bytes),
|
|
266
|
-
"Ed25519",
|
|
267
|
-
false,
|
|
268
|
-
["verify"]
|
|
269
|
-
);
|
|
270
|
-
if (!await crypto.subtle.verify(
|
|
271
|
-
"Ed25519",
|
|
272
|
-
key,
|
|
273
|
-
new Uint8Array(signature),
|
|
274
|
-
new TextEncoder().encode(timestamp + body)
|
|
275
|
-
))
|
|
276
|
-
return new Response(null, { status: 401 });
|
|
277
|
-
const raw = jsonValueSchema.parse(parseJson(body));
|
|
278
|
-
const type = z.object({ type: z.number() }).safeParse(raw);
|
|
279
|
-
if (type.success && type.data.type === 1) return Response.json({ type: 1 });
|
|
280
|
-
const result = await ctx.receive(raw);
|
|
281
|
-
const interactionId = z.object({ id: z.string() }).safeParse(raw);
|
|
282
|
-
const commandText = interactionId.success ? await ctx.store.get(stateKey("discord-command-response", interactionId.data.id)) : null;
|
|
283
|
-
return Response.json({
|
|
284
|
-
type: 4,
|
|
285
|
-
data: {
|
|
286
|
-
flags: 64,
|
|
287
|
-
content: typeof commandText === "string" ? commandText : "accepted" in result && !result.accepted ? `${result.refusal.message} ${result.refusal.linkUrl ?? result.refusal.connectUrl ?? ""}` : "Accepted in Final"
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
],
|
|
293
|
-
listeners: [
|
|
294
|
-
{
|
|
295
|
-
key: "gateway",
|
|
296
|
-
run: async (ctx) => {
|
|
297
|
-
const app = await ctx.credentials.app();
|
|
298
|
-
const gateway = z.object({ url: z.string() }).parse(await api(ctx, "/gateway/bot"));
|
|
299
|
-
const botId = await botFor(ctx, config);
|
|
300
|
-
const dispatch = createDiscordDispatch({
|
|
301
|
-
store: ctx.store,
|
|
302
|
-
botId,
|
|
303
|
-
receive: (input, signal) => {
|
|
304
|
-
const active = { ...ctx, signal };
|
|
305
|
-
return ctxReceive(input, active, { ...config, botId });
|
|
306
|
-
},
|
|
307
|
-
request: ({ path, body, method, signal }) => api({ ...ctx, signal: signal ?? ctx.signal }, path, body, method)
|
|
308
|
-
});
|
|
309
|
-
await runDiscordGateway({
|
|
310
|
-
ctx,
|
|
311
|
-
url: gateway.url,
|
|
312
|
-
token: app.accessToken,
|
|
313
|
-
receive: dispatch.receive,
|
|
314
|
-
accepts: dispatch.accepts,
|
|
315
|
-
connect: (url) => {
|
|
316
|
-
const socket = new WebSocket(url);
|
|
317
|
-
const messages = createQueue();
|
|
318
|
-
let code = null;
|
|
319
|
-
socket.addEventListener("message", (event) => {
|
|
320
|
-
if (typeof event.data === "string") messages.push(event.data);
|
|
321
|
-
});
|
|
322
|
-
socket.addEventListener("close", (event) => {
|
|
323
|
-
code = event.code;
|
|
324
|
-
messages.close();
|
|
325
|
-
});
|
|
326
|
-
socket.addEventListener("error", () => {
|
|
327
|
-
messages.close();
|
|
328
|
-
socket.close();
|
|
329
|
-
});
|
|
330
|
-
return {
|
|
331
|
-
messages: messages.stream,
|
|
332
|
-
send: (data) => socket.send(data),
|
|
333
|
-
close: () => {
|
|
334
|
-
socket.close();
|
|
335
|
-
messages.close();
|
|
336
|
-
},
|
|
337
|
-
closeCode: () => code
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
],
|
|
344
|
-
receive: async (input, ctx) => ctxReceive(input, ctx, config),
|
|
345
|
-
events: {
|
|
346
|
-
"message.posted": async (data, ctx) => {
|
|
347
|
-
const key = stateKey("discord-mirror", providerPlaceKey(ctx.origin.place), data.message.id);
|
|
348
|
-
if (await ctx.store.get(key) != null) return;
|
|
349
|
-
const channel = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
350
|
-
const result = z.object({ id: z.string() }).parse(
|
|
351
|
-
await api(ctx, `/channels/${channel}/messages`, {
|
|
352
|
-
content: `From ${data.author.displayName} in Final
|
|
353
|
-
${textOf(data.message.content)}`.slice(
|
|
354
|
-
0,
|
|
355
|
-
2e3
|
|
356
|
-
),
|
|
357
|
-
allowed_mentions: { parse: [] }
|
|
358
|
-
})
|
|
359
|
-
);
|
|
360
|
-
await ctx.store.set(key, result.id, deliveryRetention);
|
|
361
|
-
},
|
|
362
|
-
"message.delta": async (data, ctx) => {
|
|
363
|
-
const text = (ctx.state.get()?.text ?? await savedPartial(ctx)) + (data.delta.type === "text" && typeof data.delta.text === "string" ? data.delta.text : "");
|
|
364
|
-
ctx.state.set({ text });
|
|
365
|
-
await sharedReply(ctx, { text, final: false, messageIds: [data.messageId] });
|
|
366
|
-
},
|
|
367
|
-
"turn.completed": async (data, ctx) => sharedReply(ctx, {
|
|
368
|
-
text: completionText(data, ctx.state.get()?.text ?? await savedPartial(ctx)),
|
|
369
|
-
final: true,
|
|
370
|
-
messageIds: data.messages?.map((message) => message.id) ?? []
|
|
371
|
-
}),
|
|
372
|
-
"decision.created": async (data, ctx) => {
|
|
373
|
-
const deliveredKey = stateKey("discord-decision-delivered", data.decisionId);
|
|
374
|
-
if (await ctx.store.get(deliveredKey) != null) return;
|
|
375
|
-
const user = refString(ctx.origin.ref, "requester");
|
|
376
|
-
if (user == null) return;
|
|
377
|
-
const choices = decisionChoices(data.request);
|
|
378
|
-
const components = [];
|
|
379
|
-
for (const choice of choices) {
|
|
380
|
-
const id = crypto.randomUUID();
|
|
381
|
-
await ctx.store.set(
|
|
382
|
-
stateKey("discord-decision", id),
|
|
383
|
-
{ decisionId: data.decisionId, user, outcome: choice.outcome },
|
|
384
|
-
{ ttlSeconds: 3600 }
|
|
385
|
-
);
|
|
386
|
-
components.push({
|
|
387
|
-
type: 2,
|
|
388
|
-
style: choice.answer === "approve" ? 3 : 4,
|
|
389
|
-
label: choice.label,
|
|
390
|
-
custom_id: id
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
try {
|
|
394
|
-
const dm = z.object({ id: z.string() }).parse(await api(ctx, "/users/@me/channels", { recipient_id: user }));
|
|
395
|
-
const message = z.object({ id: z.string() }).parse(
|
|
396
|
-
await api(ctx, `/channels/${dm.id}/messages`, {
|
|
397
|
-
content: `${decisionSummary(data.request)}
|
|
398
|
-
${choices.length === 0 ? "Answer in Final \u2192" : "Choose an option below."}`,
|
|
399
|
-
...components.length === 0 ? {} : { components: [{ type: 1, components }] }
|
|
400
|
-
})
|
|
401
|
-
);
|
|
402
|
-
await ctx.store.set(
|
|
403
|
-
deliveredKey,
|
|
404
|
-
{ channel: dm.id, messageId: message.id },
|
|
405
|
-
{ ttlSeconds: 3600 }
|
|
406
|
-
);
|
|
407
|
-
} catch {
|
|
408
|
-
const channel = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
409
|
-
await api(ctx, `/channels/${channel}/messages`, {
|
|
410
|
-
content: "This request needs an approval. Answer in Final.",
|
|
411
|
-
allowed_mentions: { parse: [] }
|
|
412
|
-
});
|
|
413
|
-
await ctx.store.set(deliveredKey, true, { ttlSeconds: 3600 });
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
"decision.resolved": async (data, ctx) => confirmDecision(ctx, data.decisionId),
|
|
417
|
-
"message.edited": async (data, ctx) => {
|
|
418
|
-
if (data.replyOf == null && data.externalKey.startsWith("final:")) {
|
|
419
|
-
const key = mirrorReceiptKey({
|
|
420
|
-
provider: "discord",
|
|
421
|
-
place: ctx.origin.place,
|
|
422
|
-
externalKey: data.externalKey,
|
|
423
|
-
messageId: data.message.id
|
|
424
|
-
});
|
|
425
|
-
if (key == null) return;
|
|
426
|
-
const saved2 = z.string().safeParse(await ctx.store.get(key));
|
|
427
|
-
if (!saved2.success) return;
|
|
428
|
-
const channel2 = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
429
|
-
const result = await api(
|
|
430
|
-
ctx,
|
|
431
|
-
`/channels/${channel2}/messages/${saved2.data}`,
|
|
432
|
-
{
|
|
433
|
-
content: `From ${data.author.displayName} in Final
|
|
434
|
-
${textOf(data.message.content)}`.slice(
|
|
435
|
-
0,
|
|
436
|
-
2e3
|
|
437
|
-
),
|
|
438
|
-
allowed_mentions: { parse: [] }
|
|
439
|
-
},
|
|
440
|
-
"PATCH"
|
|
441
|
-
);
|
|
442
|
-
if (result === missingMessage)
|
|
443
|
-
await ctx.store.set(key, { deleted: true }, deliveryRetention);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
if (data.replyOf == null) return;
|
|
447
|
-
const channel = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
448
|
-
const saved = z.object({ id: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
|
|
449
|
-
if (channel != null && saved.success)
|
|
450
|
-
await api(
|
|
451
|
-
ctx,
|
|
452
|
-
`/channels/${channel}/messages/${saved.data.id}`,
|
|
453
|
-
{
|
|
454
|
-
content: textOf(data.message.content).slice(0, 2e3),
|
|
455
|
-
allowed_mentions: { parse: [] }
|
|
456
|
-
},
|
|
457
|
-
"PATCH"
|
|
458
|
-
);
|
|
459
|
-
},
|
|
460
|
-
"message.removed": async (data, ctx) => {
|
|
461
|
-
if (data.replyOf == null && data.externalKey.startsWith("final:")) {
|
|
462
|
-
const key = mirrorReceiptKey({
|
|
463
|
-
provider: "discord",
|
|
464
|
-
place: ctx.origin.place,
|
|
465
|
-
externalKey: data.externalKey,
|
|
466
|
-
messageId: data.message.id
|
|
467
|
-
});
|
|
468
|
-
if (key == null) return;
|
|
469
|
-
const saved2 = z.string().safeParse(await ctx.store.get(key));
|
|
470
|
-
if (!saved2.success) return;
|
|
471
|
-
const channel2 = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
472
|
-
await api(ctx, `/channels/${channel2}/messages/${saved2.data}`, void 0, "DELETE");
|
|
473
|
-
await ctx.store.set(key, { deleted: true }, deliveryRetention);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (data.replyOf == null) return;
|
|
477
|
-
const channel = refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel") ?? ctx.origin.place.id;
|
|
478
|
-
const saved = z.object({ id: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
|
|
479
|
-
if (channel != null && saved.success)
|
|
480
|
-
await api(ctx, `/channels/${channel}/messages/${saved.data.id}`, void 0, "DELETE");
|
|
481
|
-
},
|
|
482
|
-
"reaction.added": async (data, ctx) => {
|
|
483
|
-
const mirror = data.replyOf == null && data.externalKey.startsWith("final:");
|
|
484
|
-
const mirrorKey = mirrorReceiptKey({
|
|
485
|
-
provider: "discord",
|
|
486
|
-
place: ctx.origin.place,
|
|
487
|
-
externalKey: data.externalKey
|
|
488
|
-
});
|
|
489
|
-
const mirrored = z.string().safeParse(mirrorKey == null ? null : await ctx.store.get(mirrorKey));
|
|
490
|
-
if (mirror && !mirrored.success) return;
|
|
491
|
-
const channel = (data.replyOf == null && !mirror ? refString(ctx.origin.ref, "channel") : refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel")) ?? ctx.origin.place.id;
|
|
492
|
-
const saved = z.object({ id: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
|
|
493
|
-
const original = z.tuple([z.string().nullable(), z.string(), z.string()]).safeParse(parseJson(data.externalKey));
|
|
494
|
-
const id = mirror && mirrored.success ? mirrored.data : data.replyOf == null ? refString(ctx.origin.ref, "messageId") ?? (original.success ? original.data[2] : null) : saved.success ? saved.data.id : null;
|
|
495
|
-
if (channel != null && id != null)
|
|
496
|
-
await settleMirrorReaction({
|
|
497
|
-
ctx,
|
|
498
|
-
key: mirror ? mirrorKey : null,
|
|
499
|
-
classify: classifyMirrorReaction,
|
|
500
|
-
work: () => addSharedReaction({
|
|
501
|
-
store: ctx.store,
|
|
502
|
-
key: stateKey("discord-shared-reaction", channel, id, data.emoji),
|
|
503
|
-
principal: data.principal,
|
|
504
|
-
add: async () => {
|
|
505
|
-
await api(
|
|
506
|
-
ctx,
|
|
507
|
-
`/channels/${channel}/messages/${id}/reactions/${encodeURIComponent(data.emoji)}/@me`,
|
|
508
|
-
void 0,
|
|
509
|
-
"PUT"
|
|
510
|
-
);
|
|
511
|
-
return true;
|
|
512
|
-
}
|
|
513
|
-
})
|
|
514
|
-
});
|
|
515
|
-
},
|
|
516
|
-
"reaction.removed": async (data, ctx) => {
|
|
517
|
-
const mirror = data.replyOf == null && data.externalKey.startsWith("final:");
|
|
518
|
-
const mirrorKey = mirrorReceiptKey({
|
|
519
|
-
provider: "discord",
|
|
520
|
-
place: ctx.origin.place,
|
|
521
|
-
externalKey: data.externalKey
|
|
522
|
-
});
|
|
523
|
-
const mirrored = z.string().safeParse(mirrorKey == null ? null : await ctx.store.get(mirrorKey));
|
|
524
|
-
if (mirror && !mirrored.success) return;
|
|
525
|
-
const channel = (data.replyOf == null && !mirror ? refString(ctx.origin.ref, "channel") : refString(ctx.origin.ref, "replyChannel") ?? refString(ctx.origin.ref, "channel")) ?? ctx.origin.place.id;
|
|
526
|
-
const saved = z.object({ id: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
|
|
527
|
-
const original = z.tuple([z.string().nullable(), z.string(), z.string()]).safeParse(parseJson(data.externalKey));
|
|
528
|
-
const id = mirror && mirrored.success ? mirrored.data : data.replyOf == null ? refString(ctx.origin.ref, "messageId") ?? (original.success ? original.data[2] : null) : saved.success ? saved.data.id : null;
|
|
529
|
-
if (channel != null && id != null)
|
|
530
|
-
await settleMirrorReaction({
|
|
531
|
-
ctx,
|
|
532
|
-
key: mirror ? mirrorKey : null,
|
|
533
|
-
classify: classifyMirrorReaction,
|
|
534
|
-
work: () => removeSharedReaction({
|
|
535
|
-
store: ctx.store,
|
|
536
|
-
key: stateKey("discord-shared-reaction", channel, id, data.emoji),
|
|
537
|
-
principal: data.principal,
|
|
538
|
-
remove: async () => {
|
|
539
|
-
const result = await api(
|
|
540
|
-
ctx,
|
|
541
|
-
`/channels/${channel}/messages/${id}/reactions/${encodeURIComponent(data.emoji)}/@me`,
|
|
542
|
-
void 0,
|
|
543
|
-
"DELETE"
|
|
544
|
-
);
|
|
545
|
-
if (mirror && result === missingReactionMessage)
|
|
546
|
-
throw new Error("Discord reaction message missing.", {
|
|
547
|
-
cause: { status: 404, code: 10008 }
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
})
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
};
|
|
556
|
-
const ctxReceive = async (incoming, ctx, config) => {
|
|
557
|
-
const gatewayInteraction = z.object({ event: z.literal("INTERACTION_CREATE"), data: jsonValueSchema }).safeParse(incoming);
|
|
558
|
-
const input = gatewayInteraction.success ? gatewayInteraction.data.data : incoming;
|
|
559
|
-
const command = z.object({
|
|
560
|
-
id: z.string(),
|
|
561
|
-
type: z.literal(2),
|
|
562
|
-
channel_id: z.string(),
|
|
563
|
-
guild_id: z.string().optional(),
|
|
564
|
-
channel: z.object({ type: z.number(), parent_id: z.string().nullish(), name: z.string().optional() }).optional(),
|
|
565
|
-
member: z.object({ user: z.object({ id: z.string() }) }).optional(),
|
|
566
|
-
user: z.object({ id: z.string() }).optional(),
|
|
567
|
-
data: z.object({
|
|
568
|
-
name: z.literal("final"),
|
|
569
|
-
options: z.array(z.object({ name: z.string() })).optional()
|
|
570
|
-
})
|
|
571
|
-
}).safeParse(input);
|
|
572
|
-
if (command.success) {
|
|
573
|
-
const user = command.data.member?.user.id ?? command.data.user?.id;
|
|
574
|
-
if (user == null) return { ignored: true, reason: "missing-command-user" };
|
|
575
|
-
const principal = { id: user };
|
|
576
|
-
const by = await personFor(ctx, principal);
|
|
577
|
-
const verb = command.data.data.options?.[0]?.name;
|
|
578
|
-
const respond = async (text3) => {
|
|
579
|
-
await ctx.store.set(stateKey("discord-command-response", command.data.id), text3, {
|
|
580
|
-
ttlSeconds: 900
|
|
581
|
-
});
|
|
582
|
-
return { accepted: true };
|
|
583
|
-
};
|
|
584
|
-
if (typeof by !== "string") return respond(`${by.message} ${by.linkUrl ?? ""}`);
|
|
585
|
-
if (verb === "unlink") {
|
|
586
|
-
await ctx.people.revoke({ principal, reason: "provider-revoked" });
|
|
587
|
-
return respond("Unlinked your Discord account.");
|
|
588
|
-
}
|
|
589
|
-
let metadata = command.data.channel ?? null;
|
|
590
|
-
if (metadata == null && command.data.guild_id != null) {
|
|
591
|
-
try {
|
|
592
|
-
metadata = z.object({
|
|
593
|
-
type: z.number(),
|
|
594
|
-
parent_id: z.string().nullish(),
|
|
595
|
-
name: z.string().optional()
|
|
596
|
-
}).parse(await api(ctx, `/channels/${command.data.channel_id}`));
|
|
597
|
-
} catch {
|
|
598
|
-
return respond(
|
|
599
|
-
"This app cannot read the channel. Invite it or open Final to manage the connection."
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
const thread = metadata != null && [10, 11, 12].includes(metadata.type);
|
|
604
|
-
const place2 = {
|
|
605
|
-
kind: command.data.guild_id == null ? "dm" : thread ? "thread" : "channel",
|
|
606
|
-
...command.data.guild_id == null ? {} : { tenant: command.data.guild_id },
|
|
607
|
-
id: command.data.channel_id,
|
|
608
|
-
...metadata?.name == null ? {} : { title: metadata.name },
|
|
609
|
-
...thread && metadata?.parent_id != null ? { parent: { kind: "channel", id: metadata.parent_id, tenant: command.data.guild_id } } : {}
|
|
610
|
-
};
|
|
611
|
-
let text2;
|
|
612
|
-
if (verb === "disconnect") {
|
|
613
|
-
try {
|
|
614
|
-
await ctx.places.disconnect(place2, by);
|
|
615
|
-
text2 = "Disconnected this place from Final.";
|
|
616
|
-
} catch {
|
|
617
|
-
text2 = "Could not disconnect this place. Open Final to manage its connection or disconnect its connected parent place.";
|
|
618
|
-
}
|
|
619
|
-
} else {
|
|
620
|
-
const route = await ctx.places.resolve(place2);
|
|
621
|
-
text2 = route.kind === "connected" ? "This place is connected to Final." : `Connect this place in Final: ${route.connectUrl}`;
|
|
622
|
-
}
|
|
623
|
-
return respond(text2);
|
|
624
|
-
}
|
|
625
|
-
const interaction = z.object({
|
|
626
|
-
type: z.number(),
|
|
627
|
-
member: z.object({ user: z.object({ id: z.string() }) }).optional(),
|
|
628
|
-
user: z.object({ id: z.string() }).optional(),
|
|
629
|
-
data: z.object({ custom_id: z.string().optional() }).optional()
|
|
630
|
-
}).safeParse(input);
|
|
631
|
-
if (interaction.success && interaction.data.type === 3) {
|
|
632
|
-
const user = interaction.data.member?.user.id ?? interaction.data.user?.id;
|
|
633
|
-
const id = interaction.data.data?.custom_id;
|
|
634
|
-
if (user == null || id == null) return { ignored: true, reason: "invalid-action" };
|
|
635
|
-
const saved = z.object({ user: z.string(), decisionId: z.string(), outcome: jsonValueSchema }).safeParse(await ctx.store.get(stateKey("discord-decision", id)));
|
|
636
|
-
if (!saved.success || saved.data.user !== user)
|
|
637
|
-
return {
|
|
638
|
-
accepted: false,
|
|
639
|
-
refusal: {
|
|
640
|
-
refused: "no-access",
|
|
641
|
-
message: saved.success ? "Only the requester can answer this decision." : "This approval expired. Answer in Final."
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
if (await ctx.store.get(stateKey("discord-decision-resolved", saved.data.decisionId)) != null)
|
|
645
|
-
return {
|
|
646
|
-
accepted: false,
|
|
647
|
-
refusal: {
|
|
648
|
-
refused: "no-access",
|
|
649
|
-
message: "This decision has already been answered in Final."
|
|
650
|
-
}
|
|
651
|
-
};
|
|
652
|
-
const by = await personFor(ctx, { id: user });
|
|
653
|
-
if (typeof by !== "string") return { accepted: false, refusal: by };
|
|
654
|
-
const result2 = await ctx.decisions.resolve({
|
|
655
|
-
decisionId: saved.data.decisionId,
|
|
656
|
-
outcome: saved.data.outcome,
|
|
657
|
-
by
|
|
658
|
-
});
|
|
659
|
-
if ("refused" in result2) return { accepted: false, refusal: result2 };
|
|
660
|
-
await confirmDecision(ctx, saved.data.decisionId);
|
|
661
|
-
return result2.resolved ? { accepted: true } : {
|
|
662
|
-
accepted: false,
|
|
663
|
-
refusal: {
|
|
664
|
-
refused: "no-access",
|
|
665
|
-
message: "This decision has already been answered in Final."
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
const event = z.object({ event: z.string(), data: jsonValueSchema }).safeParse(input);
|
|
670
|
-
if (!event.success) return { ignored: true, reason: "unsupported-event" };
|
|
671
|
-
if (event.data.event === "GUILD_DELETE") {
|
|
672
|
-
const guild = z.object({ id: z.string(), unavailable: z.boolean().optional() }).safeParse(event.data.data);
|
|
673
|
-
if (!guild.success || guild.data.unavailable === true)
|
|
674
|
-
return { ignored: true, reason: "guild-unavailable" };
|
|
675
|
-
await ctx.people.revoke({ tenant: guild.data.id, reason: "uninstalled" });
|
|
676
|
-
return { accepted: true };
|
|
677
|
-
}
|
|
678
|
-
if (event.data.event === "MESSAGE_DELETE_BULK") {
|
|
679
|
-
const bulk = z.object({ channel_id: z.string(), ids: z.array(z.string()).max(100) }).safeParse(event.data.data);
|
|
680
|
-
if (!bulk.success) return { ignored: true, reason: "invalid-bulk-delete" };
|
|
681
|
-
let refusal = null;
|
|
682
|
-
for (const id of bulk.data.ids) {
|
|
683
|
-
const result2 = await ctxReceive(
|
|
684
|
-
{ event: "MESSAGE_DELETE", data: { channel_id: bulk.data.channel_id, id } },
|
|
685
|
-
ctx,
|
|
686
|
-
config
|
|
687
|
-
);
|
|
688
|
-
if ("accepted" in result2 && !result2.accepted) refusal ??= result2;
|
|
689
|
-
}
|
|
690
|
-
return refusal ?? { accepted: true };
|
|
691
|
-
}
|
|
692
|
-
if (event.data.event !== "MESSAGE_CREATE") {
|
|
693
|
-
const change = z.object({
|
|
694
|
-
id: z.string().optional(),
|
|
695
|
-
channel_id: z.string(),
|
|
696
|
-
author: z.object({ id: z.string() }).optional(),
|
|
697
|
-
content: z.string().optional(),
|
|
698
|
-
user_id: z.string().optional(),
|
|
699
|
-
message_id: z.string().optional(),
|
|
700
|
-
emoji: z.object({ name: z.string(), id: z.string().nullish() }).optional()
|
|
701
|
-
}).safeParse(event.data.data);
|
|
702
|
-
if (!change.success) return { ignored: true, reason: "unsupported-change" };
|
|
703
|
-
const messageId = change.data.message_id ?? change.data.id;
|
|
704
|
-
if (messageId == null) return { ignored: true, reason: "missing-message" };
|
|
705
|
-
const saved = z.object({
|
|
706
|
-
principal: z.object({ id: z.string() }),
|
|
707
|
-
externalKey: z.string(),
|
|
708
|
-
turnId: z.string().optional(),
|
|
709
|
-
place: placeRefSchema
|
|
710
|
-
}).safeParse(
|
|
711
|
-
await ctx.store.get(stateKey("discord-origin", change.data.channel_id, messageId))
|
|
712
|
-
);
|
|
713
|
-
if (!saved.success) return { ignored: true, reason: "unknown-origin" };
|
|
714
|
-
const by = await personFor(ctx, {
|
|
715
|
-
id: change.data.user_id ?? change.data.author?.id ?? saved.data.principal.id
|
|
716
|
-
});
|
|
717
|
-
if (typeof by !== "string") return { accepted: false, refusal: by };
|
|
718
|
-
const route = await ctx.places.resolve(saved.data.place);
|
|
719
|
-
if (route.kind !== "connected") return { ignored: true, reason: "unconnected" };
|
|
720
|
-
const conversation = ctx.conversation(route.conversation);
|
|
721
|
-
const key2 = saved.data.externalKey;
|
|
722
|
-
if (saved.data.turnId != null && (event.data.event === "MESSAGE_DELETE" || event.data.event === "MESSAGE_UPDATE"))
|
|
723
|
-
return { ignored: true, reason: "provider-reply-mutation" };
|
|
724
|
-
if (event.data.event === "MESSAGE_DELETE") {
|
|
725
|
-
const result2 = await conversation.remove({ externalKey: key2, by });
|
|
726
|
-
return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
|
|
727
|
-
}
|
|
728
|
-
if (event.data.event === "MESSAGE_UPDATE" && change.data.content != null) {
|
|
729
|
-
if (change.data.content.trim().length === 0)
|
|
730
|
-
return { ignored: true, reason: "provider-content-unavailable" };
|
|
731
|
-
const text2 = normalizedText(change.data.content, await botFor(ctx, config));
|
|
732
|
-
if (text2.length === 0) return { ignored: true, reason: "empty-message" };
|
|
733
|
-
const result2 = await conversation.edit({
|
|
734
|
-
externalKey: key2,
|
|
735
|
-
by,
|
|
736
|
-
content: [{ type: "text", text: text2 }]
|
|
737
|
-
});
|
|
738
|
-
return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
|
|
739
|
-
}
|
|
740
|
-
if ((event.data.event === "MESSAGE_REACTION_ADD" || event.data.event === "MESSAGE_REACTION_REMOVE") && change.data.emoji != null) {
|
|
741
|
-
const result2 = await conversation.react({
|
|
742
|
-
target: saved.data.turnId == null ? { externalKey: key2 } : { replyOf: { externalKey: key2, turnId: saved.data.turnId } },
|
|
743
|
-
by,
|
|
744
|
-
emoji: change.data.emoji.id == null ? change.data.emoji.name : `${change.data.emoji.name}:${change.data.emoji.id}`,
|
|
745
|
-
op: event.data.event === "MESSAGE_REACTION_ADD" ? "add" : "remove"
|
|
746
|
-
});
|
|
747
|
-
return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
|
|
748
|
-
}
|
|
749
|
-
return { ignored: true, reason: "unsupported-change" };
|
|
750
|
-
}
|
|
751
|
-
const parsed = messageSchema.safeParse(event.data.data);
|
|
752
|
-
if (!parsed.success || parsed.data.author.bot === true)
|
|
753
|
-
return { ignored: true, reason: "not-human" };
|
|
754
|
-
const message = parsed.data;
|
|
755
|
-
const botId = await botFor(ctx, config);
|
|
756
|
-
if (message.guild_id != null && !message.mentions.some((mention) => mention.id === botId))
|
|
757
|
-
return { ignored: true, reason: "not-mentioned" };
|
|
758
|
-
const text = normalizedText(message.content, botId);
|
|
759
|
-
if (text.length === 0) return { ignored: true, reason: "empty-message" };
|
|
760
|
-
let channelData;
|
|
761
|
-
try {
|
|
762
|
-
channelData = await api(ctx, `/channels/${message.channel_id}`);
|
|
763
|
-
} catch (error) {
|
|
764
|
-
const failed = z.object({ cause: z.object({ status: z.number() }) }).safeParse(error);
|
|
765
|
-
if (failed.success && (failed.data.cause.status === 403 || failed.data.cause.status === 404))
|
|
766
|
-
return { ignored: true, reason: "provider-place-unavailable" };
|
|
767
|
-
throw error;
|
|
768
|
-
}
|
|
769
|
-
const channel = z.object({
|
|
770
|
-
id: z.string(),
|
|
771
|
-
type: z.number(),
|
|
772
|
-
parent_id: z.string().nullish(),
|
|
773
|
-
name: z.string().optional()
|
|
774
|
-
}).parse(channelData);
|
|
775
|
-
const place = {
|
|
776
|
-
kind: message.guild_id == null ? "dm" : [10, 11, 12].includes(channel.type) ? "thread" : "channel",
|
|
777
|
-
id: channel.id,
|
|
778
|
-
...message.guild_id == null ? {} : { tenant: message.guild_id },
|
|
779
|
-
...channel.name == null ? {} : { title: channel.name },
|
|
780
|
-
...channel.parent_id == null || ![10, 11, 12].includes(channel.type) ? {} : {
|
|
781
|
-
parent: {
|
|
782
|
-
kind: "channel",
|
|
783
|
-
id: channel.parent_id,
|
|
784
|
-
...message.guild_id == null ? {} : { tenant: message.guild_id }
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
await api(ctx, `/channels/${channel.id}/typing`, {}).catch(
|
|
789
|
-
() => ctx.log("Discord typing indicator unavailable.")
|
|
790
|
-
);
|
|
791
|
-
const key = externalKey(
|
|
792
|
-
{
|
|
793
|
-
kind: "channel",
|
|
794
|
-
id: channel.id,
|
|
795
|
-
...message.guild_id == null ? {} : { tenant: message.guild_id }
|
|
796
|
-
},
|
|
797
|
-
message.id
|
|
798
|
-
);
|
|
799
|
-
let replyChannel = channel.id;
|
|
800
|
-
const person = await ctx.people.resolve({ id: message.author.id });
|
|
801
|
-
const connected = await ctx.places.resolve(place);
|
|
802
|
-
if (place.kind === "channel" && person.kind === "member" && connected.kind === "connected") {
|
|
803
|
-
const threadKey = stateKey("discord-thread", channel.id, message.id);
|
|
804
|
-
const savedThread = z.string().safeParse(await ctx.store.get(threadKey));
|
|
805
|
-
if (savedThread.success) replyChannel = savedThread.data;
|
|
806
|
-
else {
|
|
807
|
-
try {
|
|
808
|
-
replyChannel = z.object({ id: z.string() }).parse(
|
|
809
|
-
await api(ctx, `/channels/${channel.id}/messages/${message.id}/threads`, {
|
|
810
|
-
name: message.content.replace(/<@!?[0-9A-Za-z_-]+>/g, "").trim().slice(0, 80) || "Final",
|
|
811
|
-
auto_archive_duration: 1440
|
|
812
|
-
})
|
|
813
|
-
).id;
|
|
814
|
-
await ctx.store.set(threadKey, replyChannel, deliveryRetention);
|
|
815
|
-
} catch {
|
|
816
|
-
const existing = await api(ctx, `/channels/${channel.id}/messages/${message.id}`).then((value) => z.object({ thread: z.object({ id: z.string() }) }).safeParse(value)).catch(() => null);
|
|
817
|
-
if (existing?.success === true) {
|
|
818
|
-
replyChannel = existing.data.thread.id;
|
|
819
|
-
await ctx.store.set(threadKey, replyChannel, deliveryRetention);
|
|
820
|
-
} else ctx.log("Discord thread unavailable; replying in the source channel.");
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
const result = await receivePersonMessage({
|
|
825
|
-
ctx,
|
|
826
|
-
principal: { id: message.author.id, displayName: message.author.username },
|
|
827
|
-
place,
|
|
828
|
-
resumeAfterLink: place.kind === "dm",
|
|
829
|
-
text,
|
|
830
|
-
externalKey: key,
|
|
831
|
-
loadContext: async () => config.history === "linked-members" ? await historyFor({
|
|
832
|
-
ctx,
|
|
833
|
-
channel: channel.id,
|
|
834
|
-
currentMessage: message.id,
|
|
835
|
-
bot: botId
|
|
836
|
-
}).catch(() => {
|
|
837
|
-
ctx.log("Discord history is unavailable for this app grant.");
|
|
838
|
-
return [];
|
|
839
|
-
}) : [],
|
|
840
|
-
ref: {
|
|
841
|
-
externalKey: key,
|
|
842
|
-
channel: channel.id,
|
|
843
|
-
replyChannel,
|
|
844
|
-
messageId: message.id,
|
|
845
|
-
requester: message.author.id
|
|
846
|
-
}
|
|
847
|
-
});
|
|
848
|
-
await ctx.store.set(
|
|
849
|
-
stateKey("discord-origin", channel.id, message.id),
|
|
850
|
-
jsonValueSchema.parse({
|
|
851
|
-
principal: { id: message.author.id },
|
|
852
|
-
externalKey: key,
|
|
853
|
-
place
|
|
854
|
-
}),
|
|
855
|
-
deliveryRetention
|
|
856
|
-
);
|
|
857
|
-
if ("accepted" in result && !result.accepted) {
|
|
858
|
-
try {
|
|
859
|
-
const dm = z.object({ id: z.string() }).parse(await api(ctx, "/users/@me/channels", { recipient_id: message.author.id }));
|
|
860
|
-
await api(ctx, `/channels/${dm.id}/messages`, {
|
|
861
|
-
content: `${result.refusal.message} ${result.refusal.linkUrl ?? result.refusal.connectUrl ?? ""}`
|
|
862
|
-
});
|
|
863
|
-
} catch {
|
|
864
|
-
try {
|
|
865
|
-
await api(ctx, `/channels/${replyChannel}/messages`, {
|
|
866
|
-
content: "This request could not be handled here. Open Final to link your account or manage this place.",
|
|
867
|
-
allowed_mentions: { parse: [] }
|
|
868
|
-
});
|
|
869
|
-
} catch {
|
|
870
|
-
ctx.log("Discord requester notice unavailable.");
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return result;
|
|
875
|
-
};
|
|
876
|
-
const historyFor = async (args) => {
|
|
877
|
-
const { ctx, channel, currentMessage, bot } = args;
|
|
878
|
-
const messages = z.array(messageSchema).parse(
|
|
879
|
-
await api(
|
|
880
|
-
ctx,
|
|
881
|
-
`/channels/${channel}/messages?limit=20&before=${encodeURIComponent(currentMessage)}`
|
|
882
|
-
)
|
|
883
|
-
);
|
|
884
|
-
const lines = [];
|
|
885
|
-
const authors = /* @__PURE__ */ new Map();
|
|
886
|
-
for (const message of messages.reverse()) {
|
|
887
|
-
if (message.id === currentMessage || message.author.bot === true || message.content.trim().length === 0)
|
|
888
|
-
continue;
|
|
889
|
-
if (!authors.has(message.author.id)) {
|
|
890
|
-
const person = await ctx.people.resolve({ id: message.author.id });
|
|
891
|
-
authors.set(message.author.id, person.kind === "member" ? person.displayName : null);
|
|
892
|
-
}
|
|
893
|
-
const name = authors.get(message.author.id);
|
|
894
|
-
if (name != null) {
|
|
895
|
-
const text = normalizedText(message.content, bot);
|
|
896
|
-
if (text.length > 0) lines.push(`${name}: ${text}`);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
return lines;
|
|
900
|
-
};
|
|
901
|
-
export {
|
|
902
|
-
discordChannel
|
|
903
|
-
};
|
|
904
|
-
//# sourceMappingURL=discord.mjs.map
|