···11-export { default as example } from "./example";
11+export { default as upload } from "./upload";
22export { default as takes } from "./takes";
+48-29
src/features/takes.ts
···11import type { AnyMessageBlock } from "slack-edge";
22-import { slackApp } from "../index";
22+import { slackApp, slackClient } from "../index";
33import { db } from "../libs/db";
44import { takes as takesTable } from "../libs/schema";
55import { eq, and, desc } from "drizzle-orm";
···102102103103 // Auto-expire paused sessions that exceed the max pause duration
104104 if (pausedDuration > TakesConfig.MAX_PAUSE_DURATION) {
105105- await db
106106- .update(takesTable)
107107- .set({
108108- status: "completed",
109109- completedAt: now,
110110- notes: take.notes
111111- ? `${take.notes} (Automatically completed due to pause timeout)`
112112- : "Automatically completed due to pause timeout",
113113- })
114114- .where(eq(takesTable.id, take.id));
115115-105105+ let ts: string | undefined;
116106 // Notify user that their session was auto-completed
117107 try {
118118- await slackApp.client.chat.postMessage({
108108+ const res = await slackApp.client.chat.postMessage({
119109 channel: take.userId,
120120- text: `⏰ Your paused takes session has been automatically completed because it was paused for more than ${TakesConfig.MAX_PAUSE_DURATION} minutes.`,
110110+ text: `⏰ Your paused takes session has been automatically completed because it was paused for more than ${TakesConfig.MAX_PAUSE_DURATION} minutes.\n\nPlease upload your takes video in this thread within the next 24 hours!`,
121111 });
112112+ ts = res.ts;
122113 } catch (error) {
123114 console.error(
124115 "Failed to notify user of auto-completed session:",
125116 error,
126117 );
127118 }
119119+120120+ await db
121121+ .update(takesTable)
122122+ .set({
123123+ status: "waitingUpload",
124124+ completedAt: now,
125125+ ts,
126126+ notes: take.notes
127127+ ? `${take.notes} (Automatically completed due to pause timeout)`
128128+ : "Automatically completed due to pause timeout",
129129+ })
130130+ .where(eq(takesTable.id, take.id));
128131 }
129132 }
130133 }
···172175 }
173176174177 if (remainingMs <= 0) {
178178+ let ts: string | undefined;
179179+ try {
180180+ const res = await slackApp.client.chat.postMessage({
181181+ channel: take.userId,
182182+ text: "⏰ Your takes session has automatically completed because the time is up. Please upload your takes video in this thread within the next 24 hours!",
183183+ });
184184+185185+ ts = res.ts;
186186+ } catch (error) {
187187+ console.error(
188188+ "Failed to notify user of completed session:",
189189+ error,
190190+ );
191191+ }
192192+175193 await db
176194 .update(takesTable)
177195 .set({
178178- status: "completed",
196196+ status: "waitingUpload",
179197 completedAt: now,
198198+ ts,
180199 notes: take.notes
181200 ? `${take.notes} (Automatically completed - time expired)`
182201 : "Automatically completed - time expired",
183202 })
184203 .where(eq(takesTable.id, take.id));
185185-186186- try {
187187- await slackApp.client.chat.postMessage({
188188- channel: take.userId,
189189- text: "⏰ Your takes session has automatically completed because the time is up.",
190190- });
191191- } catch (error) {
192192- console.error(
193193- "Failed to notify user of completed session:",
194194- error,
195195- );
196196- }
197204 }
198205 }
199206 };
···560567 notes = args.slice(1).join(" ");
561568 }
562569570570+ const res = await slackClient.chat.postMessage({
571571+ channel: userId,
572572+ text: "🎬 Your paused takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
573573+ });
574574+563575 await db
564576 .update(takesTable)
565577 .set({
566566- status: "completed",
578578+ status: "waitingUpload",
579579+ ts: res.ts,
567580 completedAt: new Date(),
568581 ...(notes && { notes }),
569582 })
···581594 notes = args.slice(1).join(" ");
582595 }
583596597597+ const res = await slackClient.chat.postMessage({
598598+ channel: userId,
599599+ text: "🎬 Your takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
600600+ });
601601+584602 await db
585603 .update(takesTable)
586604 .set({
587587- status: "completed",
605605+ status: "waitingUpload",
606606+ ts: res.ts,
588607 completedAt: new Date(),
589608 ...(notes && { notes }),
590609 })
···11+import { db } from "../libs/db";
22+import { takes as takesTable } from "../libs/schema";
33+import { eq, and } from "drizzle-orm";
44+55+export async function getVideo(url: URL): Promise<Response> {
66+ const videoId = url.pathname.split("/")[2];
77+88+ if (!videoId) {
99+ return new Response("Invalid video id", { status: 400 });
1010+ }
1111+1212+ const video = await db
1313+ .select()
1414+ .from(takesTable)
1515+ .where(eq(takesTable.id, videoId));
1616+1717+ if (video.length === 0) {
1818+ return new Response("Video not found", { status: 404 });
1919+ }
2020+2121+ const videoData = video[0];
2222+2323+ return new Response(
2424+ `<!DOCTYPE html>
2525+ <html>
2626+ <head>
2727+ <title>Video Player</title>
2828+ <style>
2929+ body, html {
3030+ margin: 0;
3131+ padding: 0;
3232+ height: 100vh;
3333+ overflow: hidden;
3434+ }
3535+ .video-container {
3636+ position: fixed;
3737+ top: 0;
3838+ left: 0;
3939+ width: 100vw;
4040+ height: 100vh;
4141+ display: flex;
4242+ flex-direction: column;
4343+ justify-content: center;
4444+ background: linear-gradient(180deg, #000000 25%, #ffffff 50%, #000000 75%);
4545+ }
4646+ video {
4747+ width: 100vw;
4848+ height: 100vh;
4949+ object-fit: contain;
5050+ position: absolute;
5151+ bottom: 0;
5252+ }
5353+ </style>
5454+ </head>
5555+ <body>
5656+ <div class="video-container">
5757+ <video autoplay controls>
5858+ <source src="${videoData?.takeUrl}" type="video/mp4">
5959+ Your browser does not support the video tag.
6060+ </video>
6161+ </div>
6262+ </body>
6363+ </html>`,
6464+ {
6565+ headers: {
6666+ "Content-Type": "text/html",
6767+ },
6868+ },
6969+ );
7070+}
+11-2
src/index.ts
···55import { t, t_fetch } from "./libs/template";
66import { blog } from "./libs/Logger";
77import { version, name } from "../package.json";
88+import { getVideo } from "./features/video";
89const environment = process.env.NODE_ENV;
9101011// Check required environment variables
1111-const requiredVars = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"] as const;
1212+const requiredVars = [
1313+ "SLACK_BOT_TOKEN",
1414+ "SLACK_SIGNING_SECRET",
1515+ "SLACK_REVIEW_CHANNEL",
1616+ "SLACK_USER_TOKEN",
1717+ "API_URL",
1818+] as const;
1219const missingVars = requiredVars.filter((varName) => !process.env[varName]);
13201421if (missingVars.length > 0) {
···4653 port: process.env.PORT || 3000,
4754 async fetch(request: Request) {
4855 const url = new URL(request.url);
4949- const path = url.pathname;
5656+ const path = `/${url.pathname.split("/")[1]}`;
50575158 switch (path) {
5259 case "/":
···5562 return new Response("OK");
5663 case "/slack":
5764 return slackApp.run(request);
6565+ case "/video":
6666+ return getVideo(url);
5867 default:
5968 return new Response("404 Not Found", { status: 404 });
6069 }
+1-1
src/libs/config.ts
···2233export const TakesConfig = {
44 // Default takes session length in minutes (should be 90 for production)
55- DEFAULT_SESSION_LENGTH: 2,
55+ DEFAULT_SESSION_LENGTH: 5,
6677 // Maximum time in minutes that a takes session can be paused before automatic expiration
88 MAX_PAUSE_DURATION: 3,