a fun bot for the hc slack
at v0.0.3 9.4 kB view raw
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}