a fun bot for the hc slack
1import { slackApp, slackClient } from "../../../index";
2import { db } from "../../../libs/db";
3import { takes as takesTable } from "../../../libs/schema";
4import { eq, and } from "drizzle-orm";
5import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
6import * as Sentry from "@sentry/bun";
7
8export default async function upload() {
9 slackApp.anyMessage(async ({ payload }) => {
10 try {
11 if (payload.subtype !== "file_share") return;
12 const user = payload.user;
13
14 if (!user) return;
15
16 if (!payload.channel.startsWith("D")) return;
17
18 const takesNeedUpload = await db
19 .select()
20 .from(takesTable)
21 .where(
22 and(
23 eq(takesTable.userId, payload.user as string),
24 eq(takesTable.ts, payload.thread_ts as string),
25 eq(takesTable.status, "waitingUpload"),
26 ),
27 );
28
29 if (takesNeedUpload.length === 0) return;
30
31 const take = takesNeedUpload[0];
32
33 if (!payload.files || !take) return;
34
35 const file = payload.files[0];
36
37 if (!file || !file.id || !file.thumb_video || !file.mp4) {
38 await slackClient.reactions.add({
39 channel: payload.channel,
40 timestamp: payload.ts as string,
41 name: "no",
42 });
43
44 slackClient.chat.postMessage({
45 channel: payload.channel,
46 thread_ts: payload.thread_ts,
47 text: "that's not a video file? 🤔",
48 });
49 return;
50 }
51
52 const fileres = await slackClient.files.sharedPublicURL({
53 file: file.id,
54 token: process.env.SLACK_USER_TOKEN,
55 });
56
57 const fetchRes = await fetch(
58 fileres.file?.permalink_public as string,
59 );
60 const html = await fetchRes.text();
61 const match = html.match(/src="([^"]*\.mp4[^"]*)"/);
62 const takePublicUrl = match?.[1];
63
64 const takeUploadedAt = new Date();
65
66 await db
67 .update(takesTable)
68 .set({
69 status: "uploaded",
70 takeUploadedAt,
71 takeUrl: takePublicUrl,
72 })
73 .where(eq(takesTable.id, take.id));
74
75 await slackClient.reactions.add({
76 channel: payload.channel,
77 timestamp: payload.ts as string,
78 name: "fire",
79 });
80
81 await slackClient.chat.postMessage({
82 channel: payload.channel,
83 thread_ts: payload.thread_ts,
84 text: ":video_camera: uploaded! leme send this to the team for review real quick",
85 blocks: [
86 {
87 type: "section",
88 text: {
89 type: "mrkdwn",
90 text: ":video_camera: uploaded! leme send this to the team for review real quick",
91 },
92 },
93 {
94 type: "divider",
95 },
96 {
97 type: "context",
98 elements: [
99 {
100 type: "mrkdwn",
101 text: `take by <@${user}> for \`${prettyPrintTime(take.elapsedTimeMs)}\` working on: *${take.description}*`,
102 },
103 ],
104 },
105 ],
106 });
107
108 await slackClient.chat.postMessage({
109 channel: process.env.SLACK_REVIEW_CHANNEL || "",
110 text: ":video_camera: new take uploaded!",
111 blocks: [
112 {
113 type: "section",
114 text: {
115 type: "mrkdwn",
116 text: `:video_camera: new take uploaded by <@${user}> for \`${prettyPrintTime(take.elapsedTimeMs)}\` working on: *${take.description}*`,
117 },
118 },
119 {
120 type: "divider",
121 },
122 {
123 type: "video",
124 video_url: `${process.env.API_URL}/api/video/${take.id}`,
125 title_url: `${process.env.API_URL}/api/video/${take.id}`,
126 title: {
127 type: "plain_text",
128 text: `${take.description} by <@${user}> uploaded at ${generateSlackDate(takeUploadedAt)}`,
129 },
130 thumbnail_url: `https://cachet.dunkirk.sh/users/${payload.user}/r`,
131 alt_text: `takes from ${takeUploadedAt?.toLocaleString("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false })} uploaded with the description: *${take.description}*`,
132 },
133 {
134 type: "divider",
135 },
136 {
137 type: "actions",
138 elements: [
139 {
140 type: "static_select",
141 placeholder: {
142 type: "plain_text",
143 text: "Select multiplier",
144 },
145 options: [
146 {
147 text: {
148 type: "plain_text",
149 text: "0.5x",
150 },
151 value: "0.5",
152 },
153 {
154 text: {
155 type: "plain_text",
156 text: "1x",
157 },
158 value: "1",
159 },
160 {
161 text: {
162 type: "plain_text",
163 text: "1.25x",
164 },
165 value: "1.25",
166 },
167 {
168 text: {
169 type: "plain_text",
170 text: "1.5x",
171 },
172 value: "1.5",
173 },
174 {
175 text: {
176 type: "plain_text",
177 text: "2x",
178 },
179 value: "2",
180 },
181 {
182 text: {
183 type: "plain_text",
184 text: "3x",
185 },
186 value: "2.5",
187 },
188 ],
189 action_id: "select_multiplier",
190 },
191 {
192 type: "button",
193 text: {
194 type: "plain_text",
195 text: "approve",
196 },
197 style: "primary",
198 value: take.id,
199 action_id: "approve",
200 },
201 {
202 type: "button",
203 text: {
204 type: "plain_text",
205 text: "reject",
206 },
207 style: "danger",
208 value: take.id,
209 action_id: "reject",
210 },
211 ],
212 },
213 ],
214 });
215 } catch (error) {
216 console.error("Error handling file message:", error);
217 await slackClient.chat.postMessage({
218 channel: payload.channel,
219 thread_ts: payload.thread_ts,
220 text: ":warning: there was an error processing your upload",
221 });
222
223 Sentry.captureException(error, {
224 extra: {
225 channel: payload.channel,
226 user: payload.user,
227 thread_ts: payload.thread_ts,
228 },
229 tags: {
230 type: "file_upload_error",
231 },
232 });
233 }
234 });
235
236 slackApp.action("select_multiplier", async () => {});
237 slackApp.action("dismiss_message", async ({ payload, context }) => {
238 try {
239 if (context.respond)
240 await context.respond({
241 delete_original: true,
242 });
243 } catch (error) {
244 console.error("Error dismissing message:", error);
245 Sentry.captureException(error, {
246 extra: {
247 payload,
248 context,
249 },
250 tags: {
251 type: "dismiss_message_error",
252 },
253 });
254 }
255 });
256
257 slackApp.action("approve", async ({ payload, context }) => {
258 try {
259 const multiplier = Object.values(payload.state.values)[0]
260 ?.select_multiplier?.selected_option?.value;
261
262 // @ts-expect-error
263 const takeId = payload.actions[0]?.value;
264
265 const take = await db
266 .select()
267 .from(takesTable)
268 .where(eq(takesTable.id, takeId));
269 if (take.length === 0) {
270 return;
271 }
272
273 const takeToApprove = take[0];
274 if (!takeToApprove) return;
275
276 if (!multiplier || Number.isNaN(Number(multiplier))) {
277 await slackClient.chat.postEphemeral({
278 channel: process.env.SLACK_REVIEW_CHANNEL || "",
279 user: payload.user.id,
280 text: ":warning: please select a multiplier",
281 blocks: [
282 {
283 type: "actions",
284 elements: [
285 {
286 type: "button",
287 text: {
288 type: "plain_text",
289 text: "⚠️ I'll select a multiplier",
290 },
291 action_id: "dismiss_message",
292 },
293 ],
294 },
295 ],
296 });
297 return;
298 }
299
300 await db
301 .update(takesTable)
302 .set({
303 status: "approved",
304 multiplier: multiplier,
305 })
306 .where(eq(takesTable.id, takeId));
307
308 await slackClient.chat.postMessage({
309 channel: payload.user.id,
310 thread_ts: take[0]?.ts as string,
311 text: `take approved with multiplier \`${multiplier}\` so you have earned *${Number((takeToApprove.elapsedTimeMs * Number(multiplier)) / 1000 / 360).toFixed(1)} takes*!`,
312 });
313
314 // delete the message from the review channel
315 if (context.respond)
316 await context.respond({
317 delete_original: true,
318 });
319 } catch (error) {
320 console.error("Error approving take:", error);
321
322 await slackClient.chat.postEphemeral({
323 channel: process.env.SLACK_REVIEW_CHANNEL || "",
324 user: payload.user.id,
325 text: ":warning: there was an error approving the take",
326 });
327
328 Sentry.captureException(error, {
329 extra: {
330 // @ts-expect-error
331 take: payload.actions[0]?.value,
332 userApproving: payload.user,
333 },
334 tags: {
335 type: "take_approve_error",
336 },
337 });
338 }
339 });
340
341 slackApp.action("reject", async ({ payload, context }) => {
342 try {
343 // @ts-expect-error
344 const takeId = payload.actions[0]?.value;
345
346 const take = await db
347 .select()
348 .from(takesTable)
349 .where(eq(takesTable.id, takeId));
350 if (take.length === 0) {
351 return;
352 }
353 await db
354 .update(takesTable)
355 .set({
356 status: "rejected",
357 multiplier: "0",
358 })
359 .where(eq(takesTable.id, takeId));
360
361 await slackClient.chat.postMessage({
362 channel: payload.user.id,
363 thread_ts: take[0]?.ts as string,
364 text: "take rejected :(",
365 });
366
367 // delete the message from the review channel
368 if (context.respond)
369 await context.respond({
370 delete_original: true,
371 });
372 } catch (error) {
373 console.error("Error rejecting take:", error);
374
375 await slackClient.chat.postEphemeral({
376 channel: process.env.SLACK_REVIEW_CHANNEL || "",
377 user: payload.user.id,
378 text: ":warning: there was an error rejecting the take",
379 });
380
381 Sentry.captureException(error, {
382 extra: {
383 // @ts-expect-error
384 take: payload.actions[0]?.value,
385 userRejecting: payload.user,
386 },
387 tags: {
388 type: "take_reject_error",
389 },
390 });
391 }
392 });
393}