@finalai/final-agent-provider 0.8.0 → 0.10.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.
Files changed (53) hide show
  1. package/HOSTING.md +3 -3
  2. package/README.md +7 -91
  3. package/dist/modules/channel-factories.d.mts +5 -6
  4. package/dist/modules/channel-factories.mjs +7 -11
  5. package/dist/modules/channel-factories.mjs.map +3 -3
  6. package/dist/modules/channels/inbound.mjs +2 -2
  7. package/dist/modules/channels/inbound.mjs.map +2 -2
  8. package/dist/modules/channels/index.d.mts +0 -7
  9. package/dist/modules/channels/index.mjs +0 -7
  10. package/dist/modules/channels/index.mjs.map +2 -2
  11. package/dist/modules/channels/prompt.d.mts +2 -2
  12. package/dist/modules/channels/prompt.mjs +3 -3
  13. package/dist/modules/channels/prompt.mjs.map +2 -2
  14. package/dist/modules/channels/routing.d.mts +1 -1
  15. package/dist/modules/channels/routing.mjs +1 -1
  16. package/dist/modules/channels/routing.mjs.map +1 -1
  17. package/package.json +4 -40
  18. package/dist/modules/channels/channel-utils.d.mts +0 -132
  19. package/dist/modules/channels/channel-utils.mjs +0 -445
  20. package/dist/modules/channels/channel-utils.mjs.map +0 -7
  21. package/dist/modules/channels/discord-dispatch.d.mts +0 -17
  22. package/dist/modules/channels/discord-dispatch.mjs +0 -139
  23. package/dist/modules/channels/discord-dispatch.mjs.map +0 -7
  24. package/dist/modules/channels/discord-gateway.d.mts +0 -19
  25. package/dist/modules/channels/discord-gateway.mjs +0 -233
  26. package/dist/modules/channels/discord-gateway.mjs.map +0 -7
  27. package/dist/modules/channels/discord.d.mts +0 -9
  28. package/dist/modules/channels/discord.mjs +0 -904
  29. package/dist/modules/channels/discord.mjs.map +0 -7
  30. package/dist/modules/channels/github.d.mts +0 -5
  31. package/dist/modules/channels/github.mjs +0 -720
  32. package/dist/modules/channels/github.mjs.map +0 -7
  33. package/dist/modules/channels/linear.d.mts +0 -4
  34. package/dist/modules/channels/linear.mjs +0 -621
  35. package/dist/modules/channels/linear.mjs.map +0 -7
  36. package/dist/modules/channels/phone.d.mts +0 -8
  37. package/dist/modules/channels/phone.mjs +0 -382
  38. package/dist/modules/channels/phone.mjs.map +0 -7
  39. package/dist/modules/channels/reply-receipts.d.mts +0 -31
  40. package/dist/modules/channels/reply-receipts.mjs +0 -63
  41. package/dist/modules/channels/reply-receipts.mjs.map +0 -7
  42. package/dist/modules/channels/slack.d.mts +0 -7
  43. package/dist/modules/channels/slack.mjs +0 -1134
  44. package/dist/modules/channels/slack.mjs.map +0 -7
  45. package/dist/modules/channels/voice-recording.d.mts +0 -7
  46. package/dist/modules/channels/voice-recording.mjs +0 -49
  47. package/dist/modules/channels/voice-recording.mjs.map +0 -7
  48. package/dist/modules/channels/voice.d.mts +0 -33
  49. package/dist/modules/channels/voice.mjs +0 -851
  50. package/dist/modules/channels/voice.mjs.map +0 -7
  51. package/dist/modules/channels/webhook.d.mts +0 -5
  52. package/dist/modules/channels/webhook.mjs +0 -76
  53. package/dist/modules/channels/webhook.mjs.map +0 -7
@@ -1,1134 +0,0 @@
1
- import {
2
- defineChannel,
3
- packageRef,
4
- jsonValueSchema,
5
- principalRefSchema
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 { emojiToName, nameToEmoji } from "gemoji";
14
- import {
15
- channelBaseUrl,
16
- addSharedReaction,
17
- removeSharedReaction,
18
- providerPlaceKey,
19
- confirmOAuth,
20
- deliveryRetention,
21
- decisionChoices,
22
- decisionSummary,
23
- externalKey,
24
- freshTimestamp,
25
- inTenant,
26
- providerOriginSchema,
27
- parseJson,
28
- personFor,
29
- receivePersonMessage,
30
- receipt,
31
- refString,
32
- responseJson,
33
- startOAuth,
34
- stateKey,
35
- takeOAuth,
36
- textOf,
37
- completionText,
38
- verifyHmac
39
- } from "./channel-utils.mjs";
40
- const configSchema = z.object({
41
- baseUrl: z.string().url().optional(),
42
- history: z.union([z.literal("none"), z.literal("linked-members")]).default("none")
43
- });
44
- const envelopeSchema = z.object({
45
- type: z.string().optional(),
46
- team_id: z.string().optional(),
47
- enterprise_id: z.string().nullish(),
48
- event_id: z.string().optional(),
49
- event: z.record(z.string(), jsonValueSchema).optional(),
50
- challenge: z.string().optional()
51
- });
52
- const messageSchema = z.object({
53
- type: z.string(),
54
- subtype: z.string().optional(),
55
- user: z.string().optional(),
56
- channel: z.string().optional(),
57
- channel_type: z.string().optional(),
58
- text: z.string().optional(),
59
- ts: z.string().optional(),
60
- thread_ts: z.string().optional(),
61
- bot_id: z.string().optional()
62
- });
63
- const slackEmoji = (emoji) => {
64
- const name = emoji.replace(/^:|:$/g, "");
65
- if (/^[a-z0-9_+-]+(?:::skin-tone-[2-6])?$/i.test(name)) return name;
66
- const modifiers = [...emoji].filter((character) => /\p{Emoji_Modifier}/u.test(character));
67
- if (modifiers.length > 1) return null;
68
- const base = emoji.replace(/\p{Emoji_Modifier}/gu, "");
69
- const alias = (Object.hasOwn(emojiToName, base) ? emojiToName[base] : void 0) ?? emojiToName[base + "\uFE0F"] ?? emojiToName[base.replace(/\ufe0f/g, "")];
70
- if (typeof alias !== "string") return null;
71
- const modifier = modifiers[0]?.codePointAt(0);
72
- return alias + (modifier == null ? "" : `::skin-tone-${modifier - 127993}`);
73
- };
74
- const finalEmoji = (name) => {
75
- const match = /^(.+?)(?:::skin-tone-([2-6]))?$/.exec(name);
76
- if (match == null || match[1] == null) return `:${name}:`;
77
- const emoji = Object.hasOwn(nameToEmoji, match[1]) ? nameToEmoji[match[1]] : void 0;
78
- if (emoji == null) return `:${name}:`;
79
- if (match[2] == null) return emoji;
80
- const characters = [...emoji];
81
- characters.splice(1, 0, String.fromCodePoint(127993 + Number(match[2])));
82
- return characters.join("");
83
- };
84
- const safeSlackText = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
85
- const slackResponseSchema = z.looseObject({ ok: z.boolean(), error: z.string().optional() });
86
- const api = async (ctx, method, body) => {
87
- const app = await ctx.credentials.app({ tenant: ctx.origin?.place.tenant });
88
- const read = ["users.info", "conversations.history", "conversations.replies"].includes(method);
89
- const url = new URL(`https://slack.com/api/${method}`);
90
- if (read) {
91
- for (const [key, value] of Object.entries(body))
92
- if (value != null)
93
- url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
94
- }
95
- const data = await responseJson(
96
- await ctx.credentials.fetch(url, {
97
- method: read ? "GET" : "POST",
98
- headers: {
99
- authorization: `Bearer ${app.accessToken}`,
100
- ...!read ? { "content-type": "application/json" } : {}
101
- },
102
- ...read ? {} : {
103
- body: JSON.stringify({
104
- ...body,
105
- ...typeof body.text === "string" ? { text: safeSlackText(body.text) } : {}
106
- })
107
- }
108
- })
109
- );
110
- const status = slackResponseSchema.parse(data);
111
- const repeated = method === "reactions.add" && status.error === "already_reacted" || method === "reactions.remove" && status.error === "no_reaction" || method === "chat.delete" && status.error === "message_not_found" || method === "chat.update" && (status.error === "message_not_modified" || status.error === "message_not_found");
112
- if (!status.ok && !repeated)
113
- throw new Error(`Slack ${method}: ${status.error ?? "unknown error"}`, {
114
- cause: { code: status.error ?? "unknown_error" }
115
- });
116
- return data;
117
- };
118
- const classifyMirrorReaction = async (cause) => {
119
- const error = z.object({ code: z.string() }).safeParse(cause instanceof Error ? cause.cause : null);
120
- if (!error.success) return "retry";
121
- if (error.data.code === "message_not_found") return "missing";
122
- return [
123
- "missing_scope",
124
- "not_in_channel",
125
- "channel_not_found",
126
- "no_permission",
127
- "restricted_action",
128
- "not_allowed_token_type",
129
- "invalid_name",
130
- "thread_locked",
131
- "not_reactable",
132
- "is_archived",
133
- "team_access_not_granted",
134
- "no_access",
135
- "too_many_reactions",
136
- "too_many_emoji",
137
- "bad_timestamp"
138
- ].includes(error.data.code) ? "unavailable" : "retry";
139
- };
140
- const responseUrl = (value) => {
141
- if (value == null) return null;
142
- try {
143
- const url = new URL(value);
144
- return url.protocol === "https:" && url.hostname === "hooks.slack.com" && url.port === "" && url.username === "" && url.password === "" && /^\/(?:actions|commands|services)\//.test(url.pathname) ? url.href : null;
145
- } catch {
146
- return null;
147
- }
148
- };
149
- const confirmDecision = async (args) => {
150
- const { ctx, decisionId } = args;
151
- const key = stateKey("slack-decision-confirmed", decisionId);
152
- const prior = await ctx.store.get(key);
153
- const metadata = z.object({ url: z.string() }).safeParse(await ctx.store.get(stateKey("slack-decision-message", decisionId)));
154
- const url = metadata.success ? responseUrl(metadata.data.url) : null;
155
- if (url != null && prior !== "replaced") {
156
- try {
157
- const response = await ctx.credentials.fetch(url, {
158
- method: "POST",
159
- redirect: "error",
160
- headers: { "content-type": "application/json" },
161
- body: JSON.stringify({
162
- replace_original: true,
163
- text: args.message ?? "This decision has been answered in Final.",
164
- blocks: []
165
- })
166
- });
167
- await response.text();
168
- if (!response.ok) throw new Error("Slack approval confirmation unavailable.");
169
- await ctx.store.set(key, "replaced", { ttlSeconds: 3600 });
170
- return;
171
- } catch {
172
- ctx.log("Slack approval replacement unavailable.");
173
- }
174
- }
175
- if (prior != null || args.channel == null || args.user == null) return;
176
- try {
177
- await api(ctx, "chat.postEphemeral", {
178
- channel: args.channel,
179
- user: args.user,
180
- ...args.threadTs == null ? {} : { thread_ts: args.threadTs },
181
- text: args.message ?? "This decision has been answered in Final."
182
- });
183
- await ctx.store.set(key, "notified", { ttlSeconds: 3600 });
184
- } catch {
185
- ctx.log("Slack approval confirmation unavailable.");
186
- }
187
- };
188
- const slackUserSchema = z.object({
189
- user: z.object({
190
- id: z.string(),
191
- name: z.string().optional(),
192
- deleted: z.boolean().optional(),
193
- enterprise_user: z.object({ id: z.string(), enterprise_id: z.string() }).optional(),
194
- profile: z.object({ display_name: z.string().optional(), real_name: z.string().optional() }).optional()
195
- })
196
- });
197
- const enterpriseTeamsSchema = z.object({
198
- teams: z.array(z.string()).max(256),
199
- overflow: z.boolean()
200
- });
201
- const proveTeam = async (ctx, team, enterprise) => {
202
- await ctx.store.set(stateKey("slack-enterprise", team), enterprise);
203
- const key = stateKey("slack-enterprise-teams", enterprise);
204
- const saved = enterpriseTeamsSchema.safeParse(await ctx.store.get(key));
205
- if (saved.success && (saved.data.overflow || saved.data.teams.includes(team))) return;
206
- const teams = [...saved.success ? saved.data.teams : [], team];
207
- await ctx.store.set(
208
- key,
209
- teams.length > 256 ? { teams: [], overflow: true } : { teams, overflow: false }
210
- );
211
- };
212
- const revokeTeam = async (args) => {
213
- const { ctx, team, reason } = args;
214
- const alias = z.string().safeParse(await ctx.store.get(stateKey("slack-enterprise", team)));
215
- const enterprise = args.enterprise ?? (alias.success ? alias.data : void 0);
216
- await ctx.people.revoke({ tenant: team, reason });
217
- if (enterprise != null && enterprise !== team) {
218
- const key = stateKey("slack-enterprise-teams", enterprise);
219
- const saved = enterpriseTeamsSchema.safeParse(await ctx.store.get(key));
220
- const teams = saved.success ? saved.data.teams.filter((known) => known !== team) : [];
221
- if (!saved.success || saved.data.overflow || teams.length === 0) {
222
- await ctx.people.revoke({ tenant: enterprise, reason });
223
- await ctx.store.set(key, { teams: [], overflow: false });
224
- } else await ctx.store.set(key, { teams, overflow: false });
225
- }
226
- await ctx.store.set(stateKey("slack-enterprise", team), null);
227
- await ctx.store.set(stateKey("slack-bot-user", team), null);
228
- };
229
- const principalFor = async (ctx, team, user) => {
230
- const profile = slackUserSchema.parse(
231
- await api(inTenant(ctx, team), "users.info", { user })
232
- ).user;
233
- if (profile.enterprise_user != null)
234
- await proveTeam(ctx, team, profile.enterprise_user.enterprise_id);
235
- if (profile.deleted === true)
236
- await ctx.people.revoke({
237
- principal: {
238
- id: profile.enterprise_user?.id ?? profile.id,
239
- tenant: profile.enterprise_user?.enterprise_id ?? team
240
- },
241
- reason: "deactivated"
242
- });
243
- return {
244
- id: profile.enterprise_user?.id ?? profile.id,
245
- tenant: profile.enterprise_user?.enterprise_id ?? team,
246
- displayName: [profile.profile?.display_name, profile.profile?.real_name, profile.name, profile.id].find(
247
- (name) => name != null && name.trim().length > 0
248
- ) ?? profile.id
249
- };
250
- };
251
- const placeFor = (message, tenant) => {
252
- if (message.channel == null) return null;
253
- const channel = {
254
- kind: message.channel_type === "im" || message.channel.startsWith("D") ? "dm" : "channel",
255
- id: message.channel,
256
- tenant,
257
- title: message.channel
258
- };
259
- return message.thread_ts == null ? channel : {
260
- kind: "thread",
261
- id: `${message.channel}:${message.thread_ts}`,
262
- tenant,
263
- parent: channel,
264
- title: message.channel
265
- };
266
- };
267
- const location = (ctx) => ({
268
- channel: refString(ctx.origin.ref, "channel") ?? (ctx.origin.place.kind === "thread" ? ctx.origin.place.parent?.id ?? ctx.origin.place.id.split(":")[0] : ctx.origin.place.id),
269
- thread_ts: refString(ctx.origin.ref, "threadTs") ?? refString(ctx.origin.ref, "ts") ?? (ctx.origin.place.kind === "thread" ? ctx.origin.place.id.split(":").at(-1) : null)
270
- });
271
- const botFor = async (ctx, tenant) => {
272
- const key = stateKey("slack-bot-user", tenant);
273
- const cached = z.string().safeParse(await ctx.store.get(key));
274
- if (cached.success) return cached.data;
275
- const bot = z.object({ user_id: z.string().optional() }).parse(await api(ctx, "auth.test", {})).user_id;
276
- if (bot != null) await ctx.store.set(key, bot, deliveryRetention);
277
- return bot;
278
- };
279
- const normalizedText = async (ctx, text, tenant) => {
280
- const bot = await botFor(ctx, tenant);
281
- return text.replace(/<@([A-Z0-9]+)>/g, (_match, id) => id === bot ? "" : `@${id}`).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").trim();
282
- };
283
- const replySchema = z.object({ ts: z.string() });
284
- const replyKey = (ctx, replyOf) => stateKey(
285
- "slack-reply",
286
- replyOf?.externalKey ?? refString(ctx.origin.ref, "externalKey") ?? ctx.origin.place.id,
287
- replyOf?.turnId ?? ctx.turn.id
288
- );
289
- const postReply = async (ctx, text, final, key = replyKey(ctx)) => {
290
- const { channel, thread_ts } = location(ctx);
291
- if (channel == null) return;
292
- const saved = z.object({
293
- ts: z.string(),
294
- updatedAt: z.number(),
295
- final: z.boolean(),
296
- text: z.string().optional()
297
- }).safeParse(await ctx.store.get(key));
298
- if (saved.success && saved.data.final) return;
299
- if (saved.success && !final && Date.now() - saved.data.updatedAt < 1e3) {
300
- await ctx.store.set(key, { ...saved.data, text: text.slice(0, 11500) }, deliveryRetention);
301
- return;
302
- }
303
- const body = text.slice(0, 11500) + (text.length > 11500 ? "\nContinue in Final \u2192" : "");
304
- if (body.length === 0) return;
305
- const posted = await api(ctx, saved.success ? "chat.update" : "chat.postMessage", {
306
- channel,
307
- ...saved.success ? { ts: saved.data.ts } : thread_ts == null ? {} : { thread_ts },
308
- text: body
309
- });
310
- if (saved.success && slackResponseSchema.parse(posted).error === "message_not_found") {
311
- await ctx.store.set(key, null);
312
- await postReply(ctx, text, final, key);
313
- return;
314
- }
315
- const ts = saved.success ? saved.data.ts : replySchema.parse(posted).ts;
316
- await ctx.store.set(
317
- key,
318
- { ts, updatedAt: Date.now(), final, text: text.slice(0, 11500) },
319
- deliveryRetention
320
- );
321
- await ctx.store.set(
322
- stateKey("slack-origin", ctx.origin.place.tenant ?? null, channel, ts),
323
- jsonValueSchema.parse({
324
- place: ctx.origin.place,
325
- externalKey: refString(ctx.origin.ref, "externalKey") ?? ctx.origin.place.id,
326
- turnId: ctx.turn.id
327
- }),
328
- deliveryRetention
329
- );
330
- };
331
- const savedPartial = async (ctx) => {
332
- const receipt2 = z.object({ text: z.string() }).safeParse(await ctx.store.get(replyKey(ctx)));
333
- return receipt2.success ? receipt2.data.text : "";
334
- };
335
- const sharedReply = async (ctx, args) => {
336
- const { channel, thread_ts } = location(ctx);
337
- await shareReplyReceipt({
338
- bindingId: ctx.binding.id,
339
- store: ctx.store,
340
- destination: stateKey(
341
- "slack",
342
- ctx.origin.place.tenant ?? null,
343
- channel ?? null,
344
- thread_ts ?? null
345
- ),
346
- messageIds: args.messageIds,
347
- originKey: replyKey(ctx),
348
- send: (key) => postReply(ctx, args.text, args.final, key),
349
- removeDuplicate: async (receipt2, canonical) => {
350
- const saved = replySchema.safeParse(receipt2);
351
- const target = replySchema.safeParse(canonical);
352
- if (saved.success && target.success && saved.data.ts !== target.data.ts && channel != null)
353
- await api(ctx, "chat.delete", { channel, ts: saved.data.ts });
354
- }
355
- });
356
- };
357
- const slackChannel = (options = {}) => {
358
- const config = configSchema.parse(options);
359
- return defineChannel({
360
- name: "slack",
361
- config,
362
- installed: packageRef("@finalai/final-agent-provider/channels/slack"),
363
- credentials: { app: "slack" },
364
- identity: {
365
- kind: "redirect",
366
- start: async (args, ctx) => {
367
- const app = await ctx.credentials.appInfo();
368
- return startOAuth(
369
- {
370
- ...args,
371
- authorizeUrl: "https://slack.com/openid/connect/authorize",
372
- clientId: app.clientId,
373
- redirectUri: `${channelBaseUrl(config)}/b/${ctx.binding.id}/oauth/callback`,
374
- scope: "openid profile"
375
- },
376
- ctx
377
- );
378
- }
379
- },
380
- places: {
381
- kinds: ["dm", "channel", "thread"],
382
- defaults: (place) => place.kind === "dm" ? { create: "direct-with-agent", replies: "origin", audience: "members" } : null
383
- },
384
- routes: [
385
- {
386
- method: "GET",
387
- path: "/oauth/callback",
388
- handler: async (ctx) => {
389
- const saved = await takeOAuth(ctx.request, ctx);
390
- if (saved == null)
391
- return new Response("Invalid or expired link request", { status: 400 });
392
- const app = await ctx.credentials.appInfo();
393
- const token = z.object({ ok: z.literal(true), access_token: z.string() }).parse(
394
- await responseJson(
395
- await ctx.credentials.fetch("https://slack.com/api/openid.connect.token", {
396
- method: "POST",
397
- body: new URLSearchParams({
398
- client_id: app.clientId,
399
- client_secret: app.clientSecret,
400
- code: saved.code,
401
- redirect_uri: saved.redirectUri,
402
- grant_type: "authorization_code"
403
- })
404
- })
405
- )
406
- );
407
- const identity = z.object({
408
- ok: z.literal(true),
409
- sub: z.string(),
410
- "https://slack.com/team_id": z.string(),
411
- "https://slack.com/user_id": z.string().optional()
412
- }).parse(
413
- await responseJson(
414
- await ctx.credentials.fetch("https://slack.com/api/openid.connect.userInfo", {
415
- headers: { authorization: `Bearer ${token.access_token}` }
416
- })
417
- )
418
- );
419
- const actual = await principalFor(
420
- ctx,
421
- identity["https://slack.com/team_id"],
422
- identity["https://slack.com/user_id"] ?? identity.sub
423
- );
424
- const response = await confirmOAuth(
425
- { ticket: saved.ticket, expected: saved.principal, actual },
426
- ctx
427
- );
428
- if (response.ok)
429
- await ctx.store.set(
430
- stateKey(
431
- "slack-principal",
432
- identity["https://slack.com/team_id"],
433
- identity["https://slack.com/user_id"] ?? identity.sub
434
- ),
435
- jsonValueSchema.parse(actual),
436
- deliveryRetention
437
- );
438
- return response;
439
- }
440
- },
441
- {
442
- method: "POST",
443
- path: "/webhook",
444
- handler: async (ctx) => {
445
- const body = await ctx.request.text();
446
- let raw = null;
447
- let completeJson = false;
448
- try {
449
- raw = JSON.parse(body);
450
- completeJson = true;
451
- } catch {
452
- }
453
- if (!completeJson && ctx.request.headers.get("content-type")?.includes("application/x-www-form-urlencoded")) {
454
- const form = new URLSearchParams(body);
455
- raw = form.has("payload") ? parseJson(form.get("payload") ?? "") : Object.fromEntries(
456
- [...new Set(form.keys())].flatMap((key) => {
457
- const value = form.get(key);
458
- return value == null ? [] : [[key, value]];
459
- })
460
- );
461
- }
462
- const app = await ctx.credentials.appInfo();
463
- const timestamp = ctx.request.headers.get("x-slack-request-timestamp");
464
- const signature = ctx.request.headers.get("x-slack-signature");
465
- if (app.signingSecret == null || !freshTimestamp(timestamp) || signature == null || !signature.startsWith("v0=") || !await verifyHmac({
466
- secret: app.signingSecret,
467
- body: `v0:${timestamp}:${body}`,
468
- signature: signature.slice(3)
469
- }))
470
- return new Response(null, { status: 401 });
471
- const envelope = envelopeSchema.safeParse(raw);
472
- if (envelope.success && envelope.data.type === "url_verification" && envelope.data.challenge != null)
473
- return Response.json({ challenge: envelope.data.challenge });
474
- const slash = z.object({ command: z.literal("/final"), trigger_id: z.string() }).safeParse(raw);
475
- if (slash.success) {
476
- try {
477
- await ctx.receive(jsonValueSchema.parse(raw));
478
- const key = stateKey("slack-command-response", slash.data.trigger_id);
479
- const text = await ctx.store.get(key);
480
- await ctx.store.set(key, null);
481
- return Response.json({
482
- response_type: "ephemeral",
483
- text: typeof text === "string" ? text : "Open Final to check this command."
484
- });
485
- } catch {
486
- return Response.json({
487
- response_type: "ephemeral",
488
- text: "This command could not be completed. Open Final to check its state."
489
- });
490
- }
491
- }
492
- await ctx.receive(jsonValueSchema.parse(raw));
493
- return new Response(null, { status: 200 });
494
- }
495
- }
496
- ],
497
- receive: async (input, baseContext) => {
498
- const tenant = z.object({ team_id: z.string().optional(), team: z.object({ id: z.string() }).optional() }).safeParse(input);
499
- const teamId = tenant.success ? tenant.data.team_id ?? tenant.data.team?.id : void 0;
500
- const ctx = teamId == null ? baseContext : inTenant(baseContext, teamId);
501
- const command = z.object({
502
- command: z.literal("/final"),
503
- trigger_id: z.string().optional(),
504
- text: z.string().default(""),
505
- user_id: z.string(),
506
- team_id: z.string(),
507
- channel_id: z.string()
508
- }).safeParse(input);
509
- if (command.success) {
510
- const principal2 = await principalFor(ctx, command.data.team_id, command.data.user_id);
511
- const by = await personFor(ctx, principal2);
512
- const place2 = {
513
- kind: command.data.channel_id.startsWith("D") ? "dm" : "channel",
514
- id: command.data.channel_id,
515
- tenant: command.data.team_id
516
- };
517
- const verb = command.data.text.trim().split(/\s+/)[0];
518
- let text2;
519
- if (typeof by !== "string") text2 = `${by.message} ${by.linkUrl ?? ""}`;
520
- else if (verb === "disconnect") {
521
- try {
522
- await ctx.places.disconnect(place2, by);
523
- text2 = "Disconnected this place from Final.";
524
- } catch {
525
- text2 = "Could not disconnect this place. Open Final to manage its connection or disconnect its connected parent place.";
526
- }
527
- } else if (verb === "unlink") {
528
- await ctx.people.revoke({ principal: principal2, reason: "provider-revoked" });
529
- text2 = "Unlinked your Slack account.";
530
- } else {
531
- const route = await ctx.places.resolve(place2);
532
- text2 = route.kind === "connected" ? "This place is connected to Final. Use /final disconnect to disconnect it." : `Connect this place in Final: ${route.connectUrl}`;
533
- }
534
- if (command.data.trigger_id != null)
535
- await ctx.store.set(stateKey("slack-command-response", command.data.trigger_id), text2, {
536
- ttlSeconds: 60
537
- });
538
- else
539
- await api(ctx, place2.kind === "dm" ? "chat.postMessage" : "chat.postEphemeral", {
540
- channel: command.data.channel_id,
541
- user: command.data.user_id,
542
- text: text2
543
- }).catch(() => ctx.log("Slack command response unavailable."));
544
- return { accepted: true };
545
- }
546
- const interaction = z.object({
547
- type: z.literal("block_actions"),
548
- user: z.object({ id: z.string() }),
549
- team: z.object({ id: z.string() }),
550
- actions: z.array(z.object({ value: z.string() })),
551
- channel: z.object({ id: z.string() }).optional(),
552
- message: z.object({ thread_ts: z.string().optional(), ts: z.string().optional() }).optional(),
553
- response_url: z.string().optional()
554
- }).safeParse(input);
555
- if (interaction.success) {
556
- const refuse = async (reason) => {
557
- if (interaction.data.channel != null)
558
- await api(ctx, "chat.postEphemeral", {
559
- channel: interaction.data.channel.id,
560
- user: interaction.data.user.id,
561
- ...interaction.data.message?.thread_ts == null ? {} : { thread_ts: interaction.data.message.thread_ts },
562
- text: `${reason.message} ${reason.linkUrl ?? reason.connectUrl ?? ""}`
563
- }).catch(() => ctx.log("Slack approval response unavailable."));
564
- return { accepted: false, refusal: reason };
565
- };
566
- const principal2 = await principalFor(
567
- ctx,
568
- interaction.data.team.id,
569
- interaction.data.user.id
570
- );
571
- const by = await personFor(ctx, principal2);
572
- if (typeof by !== "string") return refuse(by);
573
- const value = interaction.data.actions[0]?.value;
574
- if (value == null) return { ignored: true, reason: "no-action" };
575
- const saved = z.object({
576
- decisionId: z.string(),
577
- principal: z.object({ id: z.string(), tenant: z.string().optional() }),
578
- outcome: jsonValueSchema
579
- }).safeParse(await ctx.store.get(stateKey("slack-decision", value)));
580
- if (!saved.success || saved.data.principal.id !== principal2.id || saved.data.principal.tenant !== principal2.tenant)
581
- return refuse({
582
- refused: "no-access",
583
- message: saved.success ? "Only the requester can answer this decision." : "This approval expired. Answer in Final."
584
- });
585
- const url = responseUrl(interaction.data.response_url);
586
- if (url != null)
587
- await ctx.store.set(
588
- stateKey("slack-decision-message", saved.data.decisionId),
589
- { url },
590
- { ttlSeconds: 1800 }
591
- );
592
- const confirm = (message2 = "This decision is no longer waiting in Final.") => confirmDecision({
593
- ctx,
594
- decisionId: saved.data.decisionId,
595
- message: message2,
596
- channel: interaction.data.channel?.id,
597
- user: interaction.data.user.id
598
- });
599
- const completed = await ctx.store.get(
600
- stateKey("slack-decision-resolved", saved.data.decisionId)
601
- );
602
- if (completed != null) {
603
- await confirm(
604
- completed === "answered" ? "This decision has been answered in Final." : void 0
605
- );
606
- return { accepted: true };
607
- }
608
- const result2 = await ctx.decisions.resolve({
609
- decisionId: saved.data.decisionId,
610
- outcome: saved.data.outcome,
611
- by
612
- });
613
- if ("refused" in result2) return refuse(result2);
614
- await ctx.store.set(
615
- stateKey("slack-decision-resolved", saved.data.decisionId),
616
- result2.resolved ? "answered" : "closed",
617
- {
618
- ttlSeconds: 3600
619
- }
620
- );
621
- await confirm(result2.resolved ? "This decision has been answered in Final." : void 0);
622
- return result2.resolved ? { accepted: true } : {
623
- accepted: false,
624
- refusal: {
625
- refused: "no-access",
626
- message: "This decision is no longer waiting in Final."
627
- }
628
- };
629
- }
630
- const envelope = envelopeSchema.safeParse(input);
631
- if (!envelope.success || envelope.data.event == null || envelope.data.team_id == null)
632
- return { ignored: true, reason: "not-an-event" };
633
- const event = envelope.data.event;
634
- if (event.type === "app_uninstalled" || event.type === "tokens_revoked") {
635
- if (event.type === "app_uninstalled") {
636
- await revokeTeam({
637
- ctx,
638
- team: envelope.data.team_id,
639
- enterprise: envelope.data.enterprise_id ?? void 0,
640
- reason: "uninstalled"
641
- });
642
- } else {
643
- const tokens = z.object({
644
- oauth: z.array(z.string()).default([]),
645
- bot: z.array(z.string()).default([])
646
- }).safeParse(event.tokens);
647
- if (!tokens.success) return { ignored: true, reason: "invalid-revocation" };
648
- if (tokens.data.bot.length > 0)
649
- await revokeTeam({
650
- ctx,
651
- team: envelope.data.team_id,
652
- enterprise: envelope.data.enterprise_id ?? void 0,
653
- reason: "provider-revoked"
654
- });
655
- else
656
- for (const user of tokens.data.oauth) {
657
- const cached = principalRefSchema.safeParse(
658
- await ctx.store.get(stateKey("slack-principal", envelope.data.team_id, user))
659
- );
660
- const principal2 = cached.success ? cached.data : await principalFor(ctx, envelope.data.team_id, user);
661
- await ctx.people.revoke({ principal: principal2, reason: "provider-revoked" });
662
- await ctx.store.set(stateKey("slack-principal", envelope.data.team_id, user), null);
663
- }
664
- }
665
- return { accepted: true };
666
- }
667
- if (event.type === "user_change") {
668
- const changed = slackUserSchema.shape.user.safeParse(event.user);
669
- if (changed.success && changed.data.deleted === true)
670
- await ctx.people.revoke({
671
- principal: {
672
- id: changed.data.enterprise_user?.id ?? changed.data.id,
673
- tenant: changed.data.enterprise_user?.enterprise_id ?? envelope.data.team_id
674
- },
675
- reason: "deactivated"
676
- });
677
- return { ignored: true, reason: "lifecycle" };
678
- }
679
- if (event.type === "message" && (event.subtype === "message_changed" || event.subtype === "message_deleted")) {
680
- const deleted = event.subtype === "message_deleted" || z.object({ subtype: z.literal("tombstone") }).safeParse(event.message).success;
681
- const original = messageSchema.safeParse(deleted ? event.previous_message : event.message);
682
- if (!original.success || original.data.user == null || typeof event.channel !== "string")
683
- return { ignored: true, reason: "missing-original" };
684
- if (original.data.bot_id != null || original.data.subtype === "bot_message")
685
- return { ignored: true, reason: "own-message" };
686
- const timestamp = original.data.ts ?? (typeof event.deleted_ts === "string" ? event.deleted_ts : null);
687
- if (timestamp == null) return { ignored: true, reason: "missing-original" };
688
- const mapped = providerOriginSchema.safeParse(
689
- await ctx.store.get(
690
- stateKey("slack-origin", envelope.data.team_id, event.channel, timestamp)
691
- )
692
- );
693
- if (!mapped.success) return { ignored: true, reason: "unknown-origin" };
694
- const place2 = mapped.data.place;
695
- const by = await personFor(
696
- ctx,
697
- await principalFor(ctx, envelope.data.team_id, original.data.user)
698
- );
699
- if (typeof by !== "string") return { accepted: false, refusal: by };
700
- const route = await ctx.places.resolve(place2);
701
- if (route.kind !== "connected") return { ignored: true, reason: "unconnected" };
702
- const key2 = mapped.data.externalKey;
703
- const editedText = !deleted ? await normalizedText(ctx, original.data.text ?? "", envelope.data.team_id) : null;
704
- if (editedText != null && editedText.length === 0)
705
- return { ignored: true, reason: "empty-message" };
706
- return receipt(
707
- !deleted ? await ctx.conversation(route.conversation).edit({
708
- externalKey: key2,
709
- by,
710
- content: [
711
- {
712
- type: "text",
713
- text: editedText ?? ""
714
- }
715
- ]
716
- }) : await ctx.conversation(route.conversation).remove({ externalKey: key2, by }).then((result2) => "refused" in result2 ? result2 : { messageId: key2 })
717
- );
718
- }
719
- if (event.type === "reaction_added" || event.type === "reaction_removed") {
720
- const parsed2 = z.object({
721
- user: z.string(),
722
- reaction: z.string(),
723
- item: z.object({ channel: z.string(), ts: z.string() })
724
- }).safeParse(event);
725
- if (!parsed2.success) return { ignored: true, reason: "unsupported-reaction" };
726
- const mapped = providerOriginSchema.safeParse(
727
- await ctx.store.get(
728
- stateKey(
729
- "slack-origin",
730
- envelope.data.team_id,
731
- parsed2.data.item.channel,
732
- parsed2.data.item.ts
733
- )
734
- )
735
- );
736
- if (!mapped.success) return { ignored: true, reason: "unknown-origin" };
737
- if (parsed2.data.user === await botFor(ctx, envelope.data.team_id))
738
- return { ignored: true, reason: "own-reaction" };
739
- const by = await personFor(
740
- ctx,
741
- await principalFor(ctx, envelope.data.team_id, parsed2.data.user)
742
- );
743
- if (typeof by !== "string") return { accepted: false, refusal: by };
744
- const route = await ctx.places.resolve(mapped.data.place);
745
- if (route.kind !== "connected") return { ignored: true, reason: "unconnected" };
746
- const target = mapped.data.turnId != null ? { replyOf: { externalKey: mapped.data.externalKey, turnId: mapped.data.turnId } } : {
747
- externalKey: mapped.data.externalKey
748
- };
749
- const result2 = await ctx.conversation(route.conversation).react({
750
- target,
751
- emoji: finalEmoji(parsed2.data.reaction),
752
- by,
753
- op: event.type === "reaction_added" ? "add" : "remove"
754
- });
755
- return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
756
- }
757
- const parsed = messageSchema.safeParse(event);
758
- if (!parsed.success || parsed.data.user == null || parsed.data.ts == null || parsed.data.text == null || parsed.data.bot_id != null || parsed.data.type !== "app_mention" && !(parsed.data.type === "message" && parsed.data.channel_type === "im"))
759
- return { ignored: true, reason: "not-a-human-mention" };
760
- const message = messageSchema.extend({ user: z.string(), ts: z.string(), text: z.string(), channel: z.string() }).parse(parsed.data);
761
- const place = placeFor(message, envelope.data.team_id);
762
- if (place == null) return { ignored: true, reason: "no-place" };
763
- const text = await normalizedText(ctx, message.text, envelope.data.team_id);
764
- if (text.length === 0) return { ignored: true, reason: "empty-message" };
765
- const principal = await principalFor(ctx, envelope.data.team_id, message.user);
766
- await api(ctx, "reactions.add", {
767
- channel: message.channel,
768
- timestamp: message.ts,
769
- name: "eyes"
770
- }).catch(() => ctx.log("Slack acknowledgement unavailable."));
771
- const key = externalKey(place.parent ?? place, message.ts);
772
- const historyTeam = envelope.data.team_id;
773
- const loadContext = async () => config.history === "linked-members" ? historyFor({ ctx, message, team: historyTeam }).catch(() => {
774
- ctx.log("Slack history is unavailable for this app grant.");
775
- return [];
776
- }) : [];
777
- await ctx.store.set(
778
- stateKey("slack-origin", envelope.data.team_id, message.channel, message.ts),
779
- jsonValueSchema.parse({ place, externalKey: key }),
780
- deliveryRetention
781
- );
782
- const result = await receivePersonMessage({
783
- ctx,
784
- principal,
785
- place,
786
- resumeAfterLink: place.kind === "dm",
787
- text,
788
- externalKey: key,
789
- loadContext,
790
- ref: {
791
- externalKey: key,
792
- channel: message.channel,
793
- ts: message.ts,
794
- threadTs: message.thread_ts ?? message.ts,
795
- requester: principal.id,
796
- user: message.user,
797
- tenant: principal.tenant ?? envelope.data.team_id
798
- }
799
- });
800
- if ("accepted" in result && !result.accepted)
801
- await api(ctx, place.kind === "dm" ? "chat.postMessage" : "chat.postEphemeral", {
802
- channel: message.channel,
803
- user: message.user,
804
- ...message.thread_ts == null ? {} : { thread_ts: message.thread_ts },
805
- text: `${result.refusal.message} ${result.refusal.linkUrl ?? result.refusal.connectUrl ?? ""}`
806
- }).catch(() => ctx.log("Slack requester notice unavailable."));
807
- return result;
808
- },
809
- events: {
810
- "message.posted": async (data, ctx) => {
811
- const key = stateKey("slack-mirror", providerPlaceKey(ctx.origin.place), data.message.id);
812
- if (await ctx.store.get(key) != null) return;
813
- const { channel, thread_ts } = location(ctx);
814
- if (channel == null) return;
815
- const result = replySchema.parse(
816
- await api(ctx, "chat.postMessage", {
817
- channel,
818
- ...thread_ts == null ? {} : { thread_ts },
819
- text: `From ${data.author.displayName} in Final
820
- ${textOf(data.message.content)}`.slice(
821
- 0,
822
- 11500
823
- )
824
- })
825
- );
826
- await ctx.store.set(key, result.ts, deliveryRetention);
827
- },
828
- "message.delta": async (data, ctx) => {
829
- const prior = ctx.state.get()?.text ?? await savedPartial(ctx);
830
- const delta = data.delta.type === "text" && typeof data.delta.text === "string" ? data.delta.text : "";
831
- const text = prior + delta;
832
- ctx.state.set({ text });
833
- await sharedReply(ctx, { text, final: false, messageIds: [data.messageId] });
834
- },
835
- "turn.completed": async (data, ctx) => {
836
- await sharedReply(ctx, {
837
- text: completionText(data, ctx.state.get()?.text ?? await savedPartial(ctx)),
838
- final: true,
839
- messageIds: data.messages?.map((message) => message.id) ?? []
840
- });
841
- },
842
- "decision.created": async (data, ctx) => {
843
- const deliveredKey = stateKey("slack-decision-delivered", data.decisionId);
844
- if (await ctx.store.get(deliveredKey) != null) return;
845
- const { channel, thread_ts } = location(ctx);
846
- const user = refString(ctx.origin.ref, "user") ?? refString(ctx.origin.ref, "requester");
847
- const principalId = refString(ctx.origin.ref, "requester") ?? user;
848
- const tenant = refString(ctx.origin.ref, "tenant");
849
- if (channel == null || user == null) return;
850
- const choices = decisionChoices(data.request);
851
- const elements = [];
852
- for (const choice of choices) {
853
- const value = crypto.randomUUID();
854
- await ctx.store.set(
855
- stateKey("slack-decision", value),
856
- {
857
- decisionId: data.decisionId,
858
- principal: { id: principalId ?? user, ...tenant == null ? {} : { tenant } },
859
- outcome: choice.outcome
860
- },
861
- { ttlSeconds: 3600 }
862
- );
863
- elements.push({
864
- type: "button",
865
- text: { type: "plain_text", text: choice.label },
866
- action_id: `final_decision_${elements.length}`,
867
- value
868
- });
869
- }
870
- try {
871
- await api(ctx, "chat.postEphemeral", {
872
- channel,
873
- user,
874
- ...thread_ts == null ? {} : { thread_ts },
875
- text: `${decisionSummary(data.request)}
876
- ${elements.length === 0 ? "Answer in Final \u2192" : "Choose an option below."}`,
877
- ...elements.length === 0 ? {} : {
878
- blocks: [
879
- {
880
- type: "section",
881
- text: { type: "plain_text", text: decisionSummary(data.request) }
882
- },
883
- { type: "actions", elements }
884
- ]
885
- }
886
- });
887
- } catch {
888
- await api(ctx, "chat.postMessage", {
889
- channel,
890
- ...thread_ts == null ? {} : { thread_ts },
891
- text: "This request needs approval. Answer in Final."
892
- });
893
- }
894
- await ctx.store.set(deliveredKey, true, { ttlSeconds: 3600 });
895
- },
896
- "decision.resolved": async (data, ctx) => {
897
- await ctx.store.set(stateKey("slack-decision-resolved", data.decisionId), "answered", {
898
- ttlSeconds: 3600
899
- });
900
- const { channel, thread_ts } = location(ctx);
901
- await confirmDecision({
902
- ctx,
903
- decisionId: data.decisionId,
904
- channel: channel ?? void 0,
905
- user: refString(ctx.origin.ref, "user") ?? refString(ctx.origin.ref, "requester") ?? void 0,
906
- threadTs: thread_ts ?? void 0
907
- });
908
- },
909
- "message.edited": async (data, ctx) => {
910
- if (data.replyOf == null && data.externalKey.startsWith("final:")) {
911
- const key = mirrorReceiptKey({
912
- provider: "slack",
913
- place: ctx.origin.place,
914
- externalKey: data.externalKey,
915
- messageId: data.message.id
916
- });
917
- if (key == null) return;
918
- const saved2 = z.string().safeParse(await ctx.store.get(key));
919
- const { channel: channel2 } = location(ctx);
920
- if (!saved2.success || channel2 == null) return;
921
- const result = slackResponseSchema.parse(
922
- await api(ctx, "chat.update", {
923
- channel: channel2,
924
- ts: saved2.data,
925
- text: `From ${data.author.displayName} in Final
926
- ${textOf(data.message.content)}`.slice(
927
- 0,
928
- 11500
929
- )
930
- })
931
- );
932
- if (result.error === "message_not_found")
933
- await ctx.store.set(key, { deleted: true }, deliveryRetention);
934
- return;
935
- }
936
- if (data.replyOf == null) {
937
- const { channel: channel2, thread_ts } = location(ctx);
938
- if (channel2 == null) return;
939
- const key = stateKey(
940
- "slack-prompt-update",
941
- providerPlaceKey(ctx.origin.place),
942
- data.externalKey
943
- );
944
- const saved2 = z.object({ ts: z.string(), text: z.string() }).safeParse(await ctx.store.get(key));
945
- const text = `Updated in Final:
946
- ${textOf(data.message.content)}`.slice(0, 11500);
947
- if (saved2.success && saved2.data.text === text) return;
948
- const result = await api(ctx, saved2.success ? "chat.update" : "chat.postMessage", {
949
- channel: channel2,
950
- ...saved2.success ? { ts: saved2.data.ts } : thread_ts == null ? {} : { thread_ts },
951
- text
952
- });
953
- const missing = saved2.success && slackResponseSchema.parse(result).error === "message_not_found";
954
- const current = missing ? await api(ctx, "chat.postMessage", {
955
- channel: channel2,
956
- ...thread_ts == null ? {} : { thread_ts },
957
- text
958
- }) : result;
959
- await ctx.store.set(
960
- key,
961
- { ts: saved2.success && !missing ? saved2.data.ts : replySchema.parse(current).ts, text },
962
- deliveryRetention
963
- );
964
- return;
965
- }
966
- const { channel } = location(ctx);
967
- const saved = z.object({ ts: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
968
- const ts = saved.success ? saved.data.ts : null;
969
- if (channel != null && ts != null)
970
- await api(ctx, "chat.update", {
971
- channel,
972
- ts,
973
- text: textOf(data.message.content).slice(0, 11500)
974
- });
975
- },
976
- "message.removed": async (data, ctx) => {
977
- if (data.replyOf == null && data.externalKey.startsWith("final:")) {
978
- const key = mirrorReceiptKey({
979
- provider: "slack",
980
- place: ctx.origin.place,
981
- externalKey: data.externalKey,
982
- messageId: data.message.id
983
- });
984
- if (key == null) return;
985
- const saved2 = z.string().safeParse(await ctx.store.get(key));
986
- const { channel: channel2 } = location(ctx);
987
- if (!saved2.success || channel2 == null) return;
988
- await api(ctx, "chat.delete", { channel: channel2, ts: saved2.data });
989
- await ctx.store.set(key, { deleted: true }, deliveryRetention);
990
- return;
991
- }
992
- if (data.replyOf == null) {
993
- const key = stateKey(
994
- "slack-prompt-update",
995
- providerPlaceKey(ctx.origin.place),
996
- data.externalKey
997
- );
998
- const saved2 = z.object({ ts: z.string() }).safeParse(await ctx.store.get(key));
999
- const { channel: channel2 } = location(ctx);
1000
- if (saved2.success && channel2 != null) {
1001
- await api(ctx, "chat.delete", { channel: channel2, ts: saved2.data.ts });
1002
- await ctx.store.set(key, null);
1003
- }
1004
- return;
1005
- }
1006
- const { channel } = location(ctx);
1007
- const saved = z.object({ ts: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
1008
- const ts = saved.success ? saved.data.ts : null;
1009
- if (channel != null && ts != null) await api(ctx, "chat.delete", { channel, ts });
1010
- },
1011
- "reaction.added": async (data, ctx) => {
1012
- const { channel } = location(ctx);
1013
- const mirror = data.replyOf == null && data.externalKey.startsWith("final:");
1014
- const mirrorKey = mirrorReceiptKey({
1015
- provider: "slack",
1016
- place: ctx.origin.place,
1017
- externalKey: data.externalKey
1018
- });
1019
- const mirrored = z.string().safeParse(mirrorKey == null ? null : await ctx.store.get(mirrorKey));
1020
- if (mirror && !mirrored.success) return;
1021
- const saved = z.object({ ts: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
1022
- const name = slackEmoji(data.emoji);
1023
- const timestamp = mirror && mirrored.success ? mirrored.data : data.replyOf == null ? refString(ctx.origin.ref, "ts") : saved.success ? saved.data.ts : null;
1024
- if (channel != null && timestamp != null && name != null)
1025
- await settleMirrorReaction({
1026
- ctx,
1027
- key: mirror ? mirrorKey : null,
1028
- classify: classifyMirrorReaction,
1029
- work: () => addSharedReaction({
1030
- store: ctx.store,
1031
- key: stateKey(
1032
- "slack-shared-reaction",
1033
- ctx.origin.place.tenant ?? null,
1034
- channel,
1035
- timestamp,
1036
- name
1037
- ),
1038
- principal: data.principal,
1039
- add: async () => {
1040
- const response = slackResponseSchema.parse(
1041
- await api(ctx, "reactions.add", { channel, timestamp, name })
1042
- );
1043
- return response.error !== "already_reacted";
1044
- }
1045
- })
1046
- });
1047
- },
1048
- "reaction.removed": async (data, ctx) => {
1049
- const { channel } = location(ctx);
1050
- const mirror = data.replyOf == null && data.externalKey.startsWith("final:");
1051
- const mirrorKey = mirrorReceiptKey({
1052
- provider: "slack",
1053
- place: ctx.origin.place,
1054
- externalKey: data.externalKey
1055
- });
1056
- const mirrored = z.string().safeParse(mirrorKey == null ? null : await ctx.store.get(mirrorKey));
1057
- if (mirror && !mirrored.success) return;
1058
- const saved = z.object({ ts: z.string() }).safeParse(await ctx.store.get(replyKey(ctx, data.replyOf)));
1059
- const name = slackEmoji(data.emoji);
1060
- const timestamp = mirror && mirrored.success ? mirrored.data : data.replyOf == null ? refString(ctx.origin.ref, "ts") : saved.success ? saved.data.ts : null;
1061
- if (channel != null && timestamp != null && name != null)
1062
- await settleMirrorReaction({
1063
- ctx,
1064
- key: mirror ? mirrorKey : null,
1065
- classify: classifyMirrorReaction,
1066
- work: () => removeSharedReaction({
1067
- store: ctx.store,
1068
- key: stateKey(
1069
- "slack-shared-reaction",
1070
- ctx.origin.place.tenant ?? null,
1071
- channel,
1072
- timestamp,
1073
- name
1074
- ),
1075
- principal: data.principal,
1076
- remove: async () => {
1077
- await api(ctx, "reactions.remove", { channel, timestamp, name });
1078
- }
1079
- })
1080
- });
1081
- }
1082
- }
1083
- });
1084
- };
1085
- const historyFor = async (args) => {
1086
- const { ctx, message } = args;
1087
- if (message.channel == null) return [];
1088
- const messages = [];
1089
- let cursor;
1090
- for (let page = 0; page < 5; page += 1) {
1091
- const result = z.object({
1092
- messages: z.array(messageSchema),
1093
- response_metadata: z.object({ next_cursor: z.string().optional() }).optional()
1094
- }).parse(
1095
- await api(
1096
- ctx,
1097
- message.thread_ts == null ? "conversations.history" : "conversations.replies",
1098
- {
1099
- channel: message.channel,
1100
- limit: 100,
1101
- ...message.ts == null ? {} : { latest: message.ts, inclusive: false },
1102
- ...message.thread_ts == null ? {} : { ts: message.thread_ts },
1103
- ...cursor == null ? {} : { cursor }
1104
- }
1105
- )
1106
- );
1107
- messages.push(...result.messages);
1108
- cursor = result.response_metadata?.next_cursor;
1109
- if (cursor == null || cursor.length === 0 || message.thread_ts == null) break;
1110
- }
1111
- if (message.thread_ts != null && cursor != null && cursor.length > 0) return [];
1112
- const recent = messages.sort((a, b) => Number(a.ts ?? 0) - Number(b.ts ?? 0)).slice(-20);
1113
- const lines = [];
1114
- const authors = /* @__PURE__ */ new Map();
1115
- for (const earlier of recent) {
1116
- if (earlier.user == null || earlier.text == null || earlier.bot_id != null || earlier.ts === message.ts)
1117
- continue;
1118
- if (!authors.has(earlier.user)) {
1119
- const principal = await principalFor(ctx, args.team, earlier.user);
1120
- const linked = await ctx.people.resolve(principal);
1121
- authors.set(earlier.user, linked.kind === "member" ? linked.displayName : null);
1122
- }
1123
- const name = authors.get(earlier.user);
1124
- if (name != null) {
1125
- const text = await normalizedText(ctx, earlier.text, args.team);
1126
- if (text.length > 0) lines.push(`${name}: ${text}`);
1127
- }
1128
- }
1129
- return lines;
1130
- };
1131
- export {
1132
- slackChannel
1133
- };
1134
- //# sourceMappingURL=slack.mjs.map