a fun bot for the hc slack

feat: migrate to distributed multi file approach

dunkirk.sh e4347ff7 a7fc0cfe

verified
+3 -2
manifest.dev.yaml
···
- chat:write.public
- commands
- im:history
+
- links:read
+
- reactions:write
- users:read
-
- reactions:write
-
- links:read
+
- links.embed:write
settings:
event_subscriptions:
request_url: https://casual-renewing-reptile.ngrok-free.app/slack
+3 -2
manifest.prod.yaml
···
- chat:write.public
- commands
- im:history
+
- links:read
+
- reactions:write
- users:read
-
- reactions:write
-
- links:read
+
- links.embed:write
settings:
event_subscriptions:
request_url: https://takes.dunkirk.sh/slack
+1 -1
package.json
···
{
"name": "takes",
"description": "smokey says hi!",
-
"version": "0.0.1",
+
"version": "0.0.2",
"module": "src/index.ts",
"type": "module",
"private": true,
+1
src/features/api/index.ts
···
+
export { default as video } from "./routes/video";
-9
src/features/example.ts
···
-
import { slackApp } from "../index";
-
-
const example = async () => {
-
slackApp.action("example_action", async ({ context, payload }) => {
-
console.log("Example Action", payload);
-
});
-
};
-
-
export default example;
-1
src/features/index.ts
···
-
export { default as upload } from "./upload";
export { default as takes } from "./takes";
-1405
src/features/takes.ts
···
-
import type { AnyMessageBlock } from "slack-edge";
-
import { environment, slackApp, slackClient } from "../index";
-
import { db } from "../libs/db";
-
import { takes as takesTable } from "../libs/schema";
-
import { eq, and, desc } from "drizzle-orm";
-
import TakesConfig from "../libs/config";
-
import { generateSlackDate, prettyPrintTime } from "../libs/time";
-
-
type MessageResponse = {
-
blocks?: AnyMessageBlock[];
-
text: string;
-
response_type: "ephemeral" | "in_channel";
-
};
-
-
const takes = async () => {
-
// Helper functions for command actions
-
const getActiveTake = async (userId: string) => {
-
return db
-
.select()
-
.from(takesTable)
-
.where(
-
and(
-
eq(takesTable.userId, userId),
-
eq(takesTable.status, "active"),
-
),
-
)
-
.limit(1);
-
};
-
-
const getPausedTake = async (userId: string) => {
-
return db
-
.select()
-
.from(takesTable)
-
.where(
-
and(
-
eq(takesTable.userId, userId),
-
eq(takesTable.status, "paused"),
-
),
-
)
-
.limit(1);
-
};
-
-
const getCompletedTakes = async (userId: string, limit = 5) => {
-
return db
-
.select()
-
.from(takesTable)
-
.where(
-
and(
-
eq(takesTable.userId, userId),
-
eq(takesTable.status, "completed"),
-
),
-
)
-
.orderBy(desc(takesTable.completedAt))
-
.limit(limit);
-
};
-
-
// Check for paused sessions that have exceeded the max pause duration
-
const expirePausedSessions = async () => {
-
const now = new Date();
-
const pausedTakes = await db
-
.select()
-
.from(takesTable)
-
.where(eq(takesTable.status, "paused"));
-
-
for (const take of pausedTakes) {
-
if (take.pausedAt) {
-
const pausedDuration =
-
(now.getTime() - take.pausedAt.getTime()) / (60 * 1000); // Convert to minutes
-
-
// Send warning notification when getting close to expiration
-
if (
-
pausedDuration >
-
TakesConfig.MAX_PAUSE_DURATION -
-
TakesConfig.NOTIFICATIONS
-
.PAUSE_EXPIRATION_WARNING &&
-
!take.notifiedPauseExpiration
-
) {
-
// Update notification flag
-
await db
-
.update(takesTable)
-
.set({
-
notifiedPauseExpiration: true,
-
})
-
.where(eq(takesTable.id, take.id));
-
-
// Send warning message
-
try {
-
const timeRemaining = Math.round(
-
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
-
);
-
await slackApp.client.chat.postMessage({
-
channel: take.userId,
-
text: `โš ๏ธ Reminder: Your paused takes session will automatically complete in about ${timeRemaining} minutes if not resumed.`,
-
});
-
} catch (error) {
-
console.error(
-
"Failed to send pause expiration warning:",
-
error,
-
);
-
}
-
}
-
-
// Auto-expire paused sessions that exceed the max pause duration
-
if (pausedDuration > TakesConfig.MAX_PAUSE_DURATION) {
-
let ts: string | undefined;
-
// Notify user that their session was auto-completed
-
try {
-
const res = await slackApp.client.chat.postMessage({
-
channel: take.userId,
-
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!`,
-
});
-
ts = res.ts;
-
} catch (error) {
-
console.error(
-
"Failed to notify user of auto-completed session:",
-
error,
-
);
-
}
-
-
await db
-
.update(takesTable)
-
.set({
-
status: "waitingUpload",
-
completedAt: now,
-
ts,
-
notes: take.notes
-
? `${take.notes} (Automatically completed due to pause timeout)`
-
: "Automatically completed due to pause timeout",
-
})
-
.where(eq(takesTable.id, take.id));
-
}
-
}
-
}
-
};
-
-
// Check for active sessions that are almost done
-
const checkActiveSessions = async () => {
-
const now = new Date();
-
const activeTakes = await db
-
.select()
-
.from(takesTable)
-
.where(eq(takesTable.status, "active"));
-
-
for (const take of activeTakes) {
-
const endTime = new Date(
-
take.startedAt.getTime() +
-
take.durationMinutes * 60000 +
-
(take.pausedTimeMs || 0),
-
);
-
-
const remainingMs = endTime.getTime() - now.getTime();
-
const remainingMinutes = remainingMs / 60000;
-
-
if (
-
remainingMinutes <=
-
TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING &&
-
remainingMinutes > 0 &&
-
!take.notifiedLowTime
-
) {
-
await db
-
.update(takesTable)
-
.set({ notifiedLowTime: true })
-
.where(eq(takesTable.id, take.id));
-
-
console.log("Sending low time warning to user");
-
-
try {
-
await slackApp.client.chat.postMessage({
-
channel: take.userId,
-
text: `โฑ๏ธ Your takes session has less than ${TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING} minutes remaining.`,
-
});
-
} catch (error) {
-
console.error("Failed to send low time warning:", error);
-
}
-
}
-
-
if (remainingMs <= 0) {
-
let ts: string | undefined;
-
try {
-
const res = await slackApp.client.chat.postMessage({
-
channel: take.userId,
-
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!",
-
});
-
-
ts = res.ts;
-
} catch (error) {
-
console.error(
-
"Failed to notify user of completed session:",
-
error,
-
);
-
}
-
-
await db
-
.update(takesTable)
-
.set({
-
status: "waitingUpload",
-
completedAt: now,
-
ts,
-
notes: take.notes
-
? `${take.notes} (Automatically completed - time expired)`
-
: "Automatically completed - time expired",
-
})
-
.where(eq(takesTable.id, take.id));
-
}
-
}
-
};
-
-
// Command action handlers
-
const handleStart = async (
-
userId: string,
-
channelId: string,
-
description?: string,
-
durationMinutes?: number,
-
): Promise<MessageResponse> => {
-
const activeTake = await getActiveTake(userId);
-
if (activeTake.length > 0) {
-
return {
-
text: "You already have an active takes session! Use `/takes status` to check it.",
-
response_type: "ephemeral",
-
};
-
}
-
-
// Create new takes session
-
const newTake = {
-
id: Bun.randomUUIDv7(),
-
userId,
-
channelId,
-
status: "active",
-
startedAt: new Date(),
-
durationMinutes:
-
durationMinutes || TakesConfig.DEFAULT_SESSION_LENGTH,
-
description: description || null,
-
notifiedLowTime: false,
-
notifiedPauseExpiration: false,
-
};
-
-
await db.insert(takesTable).values(newTake);
-
-
// Calculate end time for message
-
const endTime = new Date(
-
newTake.startedAt.getTime() + newTake.durationMinutes * 60000,
-
);
-
-
const descriptionText = description
-
? `\n\n*Working on:* ${description}`
-
: "";
-
return {
-
text: `๐ŸŽฌ Takes session started! You have ${prettyPrintTime(newTake.durationMinutes * 60000)} until ${generateSlackDate(endTime)}.${descriptionText}`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `๐ŸŽฌ Takes session started!${descriptionText}`,
-
},
-
},
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `You have ${prettyPrintTime(newTake.durationMinutes * 60000)} left until ${generateSlackDate(endTime)}.`,
-
},
-
],
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โœ๏ธ edit",
-
emoji: true,
-
},
-
value: "edit",
-
action_id: "takes_edit",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โธ๏ธ Pause",
-
emoji: true,
-
},
-
value: "pause",
-
action_id: "takes_pause",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โน๏ธ Stop",
-
emoji: true,
-
},
-
value: "stop",
-
action_id: "takes_stop",
-
style: "danger",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ”„ Refresh",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_status",
-
},
-
],
-
},
-
],
-
};
-
};
-
-
const handlePause = async (
-
userId: string,
-
): Promise<MessageResponse | undefined> => {
-
const activeTake = await getActiveTake(userId);
-
if (activeTake.length === 0) {
-
return {
-
text: `You don't have an active takes session! Use \`/takes start\` to begin.`,
-
response_type: "ephemeral",
-
};
-
}
-
-
const takeToUpdate = activeTake[0];
-
if (!takeToUpdate) {
-
return;
-
}
-
-
// Update the takes entry to paused status
-
await db
-
.update(takesTable)
-
.set({
-
status: "paused",
-
pausedAt: new Date(),
-
notifiedPauseExpiration: false, // Reset pause expiration notification
-
})
-
.where(eq(takesTable.id, takeToUpdate.id));
-
-
// Calculate when the pause will expire
-
const pauseExpires = new Date();
-
pauseExpires.setMinutes(
-
pauseExpires.getMinutes() + TakesConfig.MAX_PAUSE_DURATION,
-
);
-
const pauseExpiresStr = `<!date^${Math.floor(pauseExpires.getTime() / 1000)}^{date_short_pretty} at {time}|${pauseExpires.toLocaleString()}>`;
-
-
return {
-
text: `โธ๏ธ Session paused! You have ${prettyPrintTime(takeToUpdate.durationMinutes * 60000)} remaining. It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `โธ๏ธ Session paused! You have ${prettyPrintTime(takeToUpdate.durationMinutes * 60000)} remaining.`,
-
},
-
},
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
-
},
-
],
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โœ๏ธ edit",
-
emoji: true,
-
},
-
value: "edit",
-
action_id: "takes_edit",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โ–ถ๏ธ Resume",
-
emoji: true,
-
},
-
value: "resume",
-
action_id: "takes_resume",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โน๏ธ Stop",
-
emoji: true,
-
},
-
value: "stop",
-
action_id: "takes_stop",
-
style: "danger",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ”„ Refresh",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_status",
-
},
-
],
-
},
-
],
-
};
-
};
-
-
const handleResume = async (
-
userId: string,
-
): Promise<MessageResponse | undefined> => {
-
const pausedTake = await getPausedTake(userId);
-
if (pausedTake.length === 0) {
-
return {
-
text: `You don't have a paused takes session!`,
-
response_type: "ephemeral",
-
};
-
}
-
-
const pausedSession = pausedTake[0];
-
if (!pausedSession) {
-
return;
-
}
-
-
const now = new Date();
-
-
// Calculate paused time
-
if (pausedSession.pausedAt) {
-
const pausedTimeMs =
-
now.getTime() - pausedSession.pausedAt.getTime();
-
const totalPausedTime =
-
(pausedSession.pausedTimeMs || 0) + pausedTimeMs;
-
-
// Update the takes entry to active status
-
await db
-
.update(takesTable)
-
.set({
-
status: "active",
-
pausedAt: null,
-
pausedTimeMs: totalPausedTime,
-
notifiedLowTime: false, // Reset low time notification
-
})
-
.where(eq(takesTable.id, pausedSession.id));
-
}
-
-
const endTime = new Date(
-
new Date(pausedSession.startedAt).getTime() +
-
pausedSession.durationMinutes * 60000 +
-
(pausedSession.pausedTimeMs || 0),
-
);
-
-
const timeRemaining = endTime.getTime() - now.getTime();
-
-
return {
-
text: `โ–ถ๏ธ Takes session resumed! You have ${prettyPrintTime(timeRemaining)} remaining in your session.`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: "โ–ถ๏ธ Takes session resumed!",
-
},
-
},
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `You have ${prettyPrintTime(timeRemaining)} remaining until ${generateSlackDate(endTime)}.`,
-
},
-
],
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โœ๏ธ edit",
-
emoji: true,
-
},
-
value: "edit",
-
action_id: "takes_edit",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โธ๏ธ Pause",
-
emoji: true,
-
},
-
value: "pause",
-
action_id: "takes_pause",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โน๏ธ Stop",
-
emoji: true,
-
},
-
value: "stop",
-
action_id: "takes_stop",
-
style: "danger",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ”„ Refresh",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_status",
-
},
-
],
-
},
-
],
-
};
-
};
-
-
const handleStop = async (
-
userId: string,
-
args?: string[],
-
): Promise<MessageResponse | undefined> => {
-
const activeTake = await getActiveTake(userId);
-
-
if (activeTake.length === 0) {
-
const pausedTake = await getPausedTake(userId);
-
-
if (pausedTake.length === 0) {
-
return {
-
text: `You don't have an active or paused takes session!`,
-
response_type: "ephemeral",
-
};
-
}
-
-
// Mark the paused session as completed
-
const pausedTakeToStop = pausedTake[0];
-
if (!pausedTakeToStop) {
-
return;
-
}
-
-
// Extract notes if provided
-
let notes = undefined;
-
if (args && args.length > 1) {
-
notes = args.slice(1).join(" ");
-
}
-
-
const res = await slackClient.chat.postMessage({
-
channel: userId,
-
text: "๐ŸŽฌ Your paused takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
-
});
-
-
await db
-
.update(takesTable)
-
.set({
-
status: "waitingUpload",
-
ts: res.ts,
-
completedAt: new Date(),
-
...(notes && { notes }),
-
})
-
.where(eq(takesTable.id, pausedTakeToStop.id));
-
} else {
-
// Mark the active session as completed
-
const activeTakeToStop = activeTake[0];
-
if (!activeTakeToStop) {
-
return;
-
}
-
-
// Extract notes if provided
-
let notes = undefined;
-
if (args && args.length > 1) {
-
notes = args.slice(1).join(" ");
-
}
-
-
const res = await slackClient.chat.postMessage({
-
channel: userId,
-
text: "๐ŸŽฌ Your takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
-
});
-
-
await db
-
.update(takesTable)
-
.set({
-
status: "waitingUpload",
-
ts: res.ts,
-
completedAt: new Date(),
-
...(notes && { notes }),
-
})
-
.where(eq(takesTable.id, activeTakeToStop.id));
-
}
-
-
return {
-
text: "โœ… Takes session completed! I hope you had fun!",
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: "โœ… Takes session completed! I hope you had fun!",
-
},
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐ŸŽฌ Start New Session",
-
emoji: true,
-
},
-
value: "start",
-
action_id: "takes_start",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ“‹ History",
-
emoji: true,
-
},
-
value: "history",
-
action_id: "takes_history",
-
},
-
],
-
},
-
],
-
};
-
};
-
-
const handleStatus = async (
-
userId: string,
-
): Promise<MessageResponse | undefined> => {
-
const activeTake = await getActiveTake(userId);
-
-
// First, check for expired paused sessions
-
await expirePausedSessions();
-
-
if (activeTake.length > 0) {
-
const take = activeTake[0];
-
if (!take) {
-
return;
-
}
-
-
const startTime = new Date(take.startedAt);
-
const endTime = new Date(
-
startTime.getTime() + take.durationMinutes * 60000,
-
);
-
-
// Adjust for paused time
-
if (take.pausedTimeMs) {
-
endTime.setTime(endTime.getTime() + take.pausedTimeMs);
-
}
-
-
const now = new Date();
-
const remainingMs = endTime.getTime() - now.getTime();
-
-
// Add description to display if present
-
const descriptionText = take.description
-
? `\n\n*Working on:* ${take.description}`
-
: "";
-
-
return {
-
text: `๐ŸŽฌ You have an active takes session with ${prettyPrintTime(remainingMs)} remaining.${descriptionText}`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `๐ŸŽฌ You have an active takes session${descriptionText}`,
-
},
-
},
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `You have ${prettyPrintTime(remainingMs)} remaining until ${generateSlackDate(endTime)}.`,
-
},
-
],
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โœ๏ธ edit",
-
emoji: true,
-
},
-
value: "edit",
-
action_id: "takes_edit",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โธ๏ธ Pause",
-
emoji: true,
-
},
-
value: "pause",
-
action_id: "takes_pause",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โน๏ธ Stop",
-
emoji: true,
-
},
-
value: "stop",
-
action_id: "takes_stop",
-
style: "danger",
-
},
-
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ”„ Refresh",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_status",
-
},
-
],
-
},
-
],
-
};
-
}
-
-
// Check for paused session
-
const pausedTakeStatus = await getPausedTake(userId);
-
-
if (pausedTakeStatus.length > 0) {
-
const pausedTake = pausedTakeStatus[0];
-
if (!pausedTake || !pausedTake.pausedAt) {
-
return;
-
}
-
-
// Calculate how much time remains before auto-completion
-
const now = new Date();
-
const pausedDuration =
-
(now.getTime() - pausedTake.pausedAt.getTime()) / (60 * 1000); // In minutes
-
const remainingPauseTime = Math.max(
-
0,
-
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
-
);
-
-
// Format the pause timeout
-
const pauseExpires = new Date(pausedTake.pausedAt);
-
pauseExpires.setMinutes(
-
pauseExpires.getMinutes() + TakesConfig.MAX_PAUSE_DURATION,
-
);
-
const pauseExpiresStr = `<!date^${Math.floor(pauseExpires.getTime() / 1000)}^{date_short_pretty} at {time}|${pauseExpires.toLocaleString()}>`;
-
-
// Add notes to display if present
-
const noteText = pausedTake.notes
-
? `\n\n*Working on:* ${pausedTake.notes}`
-
: "";
-
-
return {
-
text: `โธ๏ธ You have a paused takes session. It will auto-complete in ${remainingPauseTime.toFixed(1)} minutes if not resumed.`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `โธ๏ธ Session paused! You have ${prettyPrintTime(pausedTake.durationMinutes * 60000)} remaining.`,
-
},
-
},
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
-
},
-
],
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โ–ถ๏ธ Resume",
-
emoji: true,
-
},
-
value: "resume",
-
action_id: "takes_resume",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โน๏ธ Stop",
-
emoji: true,
-
},
-
value: "stop",
-
action_id: "takes_stop",
-
style: "danger",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ”„ Refresh",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_status",
-
},
-
],
-
},
-
],
-
};
-
}
-
-
// Check history of completed sessions
-
const completedSessions = await getCompletedTakes(userId);
-
const takeTime = completedSessions.length
-
? (() => {
-
const diffMs =
-
new Date().getTime() -
-
// @ts-expect-error - TS doesn't know that we are checking the length
-
completedSessions[
-
completedSessions.length - 1
-
].startedAt.getTime();
-
-
const hours = Math.ceil(diffMs / (1000 * 60 * 60));
-
if (hours < 24) return `${hours} hours`;
-
-
const weeks = Math.floor(
-
diffMs / (1000 * 60 * 60 * 24 * 7),
-
);
-
if (weeks > 0 && weeks < 4) return `${weeks} weeks`;
-
-
const months = Math.floor(
-
diffMs / (1000 * 60 * 60 * 24 * 30),
-
);
-
return `${months} months`;
-
})()
-
: 0;
-
-
return {
-
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the last ${takeTime}.`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the last ${takeTime}.`,
-
},
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐ŸŽฌ Start New Session",
-
emoji: true,
-
},
-
value: "start",
-
action_id: "takes_start",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ“‹ History",
-
emoji: true,
-
},
-
value: "history",
-
action_id: "takes_history",
-
},
-
],
-
},
-
],
-
};
-
};
-
-
const handleHistory = async (userId: string): Promise<MessageResponse> => {
-
// Get completed takes for the user
-
const completedTakes = (
-
await getCompletedTakes(userId, TakesConfig.MAX_HISTORY_ITEMS)
-
).sort(
-
(a, b) =>
-
(b.completedAt?.getTime() ?? 0) -
-
(a.completedAt?.getTime() ?? 0),
-
);
-
-
if (completedTakes.length === 0) {
-
return {
-
text: "You haven't completed any takes sessions yet.",
-
response_type: "ephemeral",
-
};
-
}
-
-
// Create blocks for each completed take
-
const historyBlocks: AnyMessageBlock[] = [
-
{
-
type: "header",
-
text: {
-
type: "plain_text",
-
text: `๐Ÿ“‹ Your most recent ${completedTakes.length} Takes Sessions`,
-
emoji: true,
-
},
-
},
-
];
-
-
for (const take of completedTakes) {
-
const startTime = new Date(take.startedAt);
-
const endTime = take.completedAt || startTime;
-
-
// Calculate duration in minutes
-
const durationMs = endTime.getTime() - startTime.getTime();
-
const pausedMs = take.pausedTimeMs || 0;
-
const activeDuration = Math.round((durationMs - pausedMs) / 60000);
-
-
// Format dates
-
const startDate = `<!date^${Math.floor(startTime.getTime() / 1000)}^{date_short_pretty} at {time}|${startTime.toLocaleString()}>`;
-
const endDate = `<!date^${Math.floor(endTime.getTime() / 1000)}^{date_short_pretty} at {time}|${endTime.toLocaleString()}>`;
-
-
const notes = take.notes ? `\nโ€ข Notes: ${take.notes}` : "";
-
const description = take.description
-
? `\nโ€ข Description: ${take.description}\n`
-
: "";
-
-
historyBlocks.push({
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `*Take on ${startDate}*\n${description}โ€ข Duration: ${activeDuration} minutes${
-
pausedMs > 0
-
? ` (+ ${Math.round(pausedMs / 60000)} minutes paused)`
-
: ""
-
}\nโ€ข Started: ${startDate}\nโ€ข Completed: ${endDate}${notes}`,
-
},
-
});
-
-
// Add a divider between entries
-
if (take !== completedTakes[completedTakes.length - 1]) {
-
historyBlocks.push({
-
type: "divider",
-
});
-
}
-
}
-
-
// Add actions block
-
historyBlocks.push({
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐ŸŽฌ Start New Session",
-
emoji: true,
-
},
-
value: "start",
-
action_id: "takes_start",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ‘๏ธ Status",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_status",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ”„ Refresh",
-
emoji: true,
-
},
-
value: "status",
-
action_id: "takes_history",
-
},
-
],
-
});
-
-
return {
-
text: `Your recent takes history (${completedTakes.length} sessions)`,
-
response_type: "ephemeral",
-
blocks: historyBlocks,
-
};
-
};
-
-
const handleHelp = async (): Promise<MessageResponse> => {
-
return {
-
text: `*Takes Commands*\n\nโ€ข \`/takes start [minutes]\` - Start a new takes session, optionally specifying duration\nโ€ข \`/takes pause\` - Pause your current session (max ${TakesConfig.MAX_PAUSE_DURATION} min)\nโ€ข \`/takes resume\` - Resume your paused session\nโ€ข \`/takes stop [notes]\` - End your current session with optional notes\nโ€ข \`/takes status\` - Check the status of your session\nโ€ข \`/takes history\` - View your past takes sessions`,
-
response_type: "ephemeral",
-
blocks: [
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: "*Takes Commands*",
-
},
-
},
-
{
-
type: "section",
-
text: {
-
type: "mrkdwn",
-
text: `โ€ข \`/takes start [minutes]\` - Start a new session (default: ${TakesConfig.DEFAULT_SESSION_LENGTH} min)\nโ€ข \`/takes pause\` - Pause your session (max ${TakesConfig.MAX_PAUSE_DURATION} min)\nโ€ข \`/takes resume\` - Resume your paused session\nโ€ข \`/takes stop [notes]\` - End session with optional notes\nโ€ข \`/takes status\` - Check status\nโ€ข \`/takes history\` - View past sessions`,
-
},
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐ŸŽฌ Start New Session",
-
emoji: true,
-
},
-
value: "start",
-
action_id: "takes_start",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐Ÿ“‹ History",
-
emoji: true,
-
},
-
value: "history",
-
action_id: "takes_history",
-
},
-
],
-
},
-
],
-
};
-
};
-
const getDescriptionBlocks = (error?: string): MessageResponse => {
-
const blocks: AnyMessageBlock[] = [
-
{
-
type: "input",
-
block_id: "note_block",
-
element: {
-
type: "plain_text_input",
-
action_id: "note_input",
-
placeholder: {
-
type: "plain_text",
-
text: "Enter a note for your session",
-
},
-
multiline: true,
-
},
-
label: {
-
type: "plain_text",
-
text: "Note",
-
},
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "๐ŸŽฌ Start Session",
-
emoji: true,
-
},
-
value: "start",
-
action_id: "takes_start",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โ›” Cancel",
-
emoji: true,
-
},
-
value: "cancel",
-
action_id: "takes_status",
-
style: "danger",
-
},
-
],
-
},
-
];
-
-
if (error) {
-
blocks.push(
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `โš ๏ธ ${error}`,
-
},
-
],
-
},
-
);
-
}
-
-
return {
-
text: "Please enter a note for your session:",
-
response_type: "ephemeral",
-
blocks,
-
};
-
};
-
-
const getEditDescriptionBlocks = (
-
description: string,
-
error?: string,
-
): MessageResponse => {
-
const blocks: AnyMessageBlock[] = [
-
{
-
type: "input",
-
block_id: "note_block",
-
element: {
-
type: "plain_text_input",
-
action_id: "note_input",
-
placeholder: {
-
type: "plain_text",
-
text: "Enter a note for your session",
-
},
-
multiline: true,
-
initial_value: description,
-
},
-
label: {
-
type: "plain_text",
-
text: "Note",
-
},
-
},
-
{
-
type: "actions",
-
elements: [
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โœ๏ธ Update Note",
-
emoji: true,
-
},
-
value: "start",
-
action_id: "takes_edit",
-
},
-
{
-
type: "button",
-
text: {
-
type: "plain_text",
-
text: "โ›” Cancel",
-
emoji: true,
-
},
-
value: "cancel",
-
action_id: "takes_status",
-
style: "danger",
-
},
-
],
-
},
-
];
-
-
if (error) {
-
blocks.push(
-
{
-
type: "divider",
-
},
-
{
-
type: "context",
-
elements: [
-
{
-
type: "mrkdwn",
-
text: `โš ๏ธ ${error}`,
-
},
-
],
-
},
-
);
-
}
-
-
return {
-
text: "Please enter a note for your session:",
-
response_type: "ephemeral",
-
blocks,
-
};
-
};
-
-
// Main command handler
-
slackApp.command(
-
environment === "dev" ? "/takes-dev" : "/takes",
-
async ({ payload, context }): Promise<void> => {
-
const userId = payload.user_id;
-
const channelId = payload.channel_id;
-
const text = payload.text || "";
-
const args = text.trim().split(/\s+/);
-
let subcommand = args[0]?.toLowerCase() || "";
-
-
// Check for active takes session
-
const activeTake = await getActiveTake(userId);
-
-
// Check for paused session if no active one
-
const pausedTakeCheck =
-
activeTake.length === 0 ? await getPausedTake(userId) : [];
-
-
// Run checks for expired or about-to-expire sessions
-
await expirePausedSessions();
-
await checkActiveSessions();
-
-
// Default to status if we have an active or paused session and no command specified
-
if (
-
subcommand === "" &&
-
(activeTake.length > 0 || pausedTakeCheck.length > 0)
-
) {
-
subcommand = "status";
-
} else if (subcommand === "") {
-
subcommand = "help";
-
}
-
-
let response: MessageResponse | undefined;
-
-
// Special handling for start command to show modal
-
if (subcommand === "start" && !activeTake.length) {
-
response = getDescriptionBlocks();
-
}
-
-
// Route to the appropriate handler function
-
switch (subcommand) {
-
case "start":
-
response = await handleStart(userId, channelId);
-
break;
-
case "pause":
-
response = await handlePause(userId);
-
break;
-
case "resume":
-
response = await handleResume(userId);
-
break;
-
case "stop":
-
response = await handleStop(userId, args);
-
break;
-
case "edit":
-
response = getEditDescriptionBlocks(
-
activeTake[0]?.description || "",
-
);
-
break;
-
case "status":
-
response = await handleStatus(userId);
-
break;
-
case "history":
-
response = await handleHistory(userId);
-
break;
-
case "help":
-
response = await handleHelp();
-
break;
-
default:
-
response = await handleHelp();
-
break;
-
}
-
-
if (context.respond)
-
await context.respond(
-
response || {
-
text: "An error occurred while processing your request.",
-
response_type: "ephemeral",
-
},
-
);
-
},
-
);
-
-
// Handle button actions
-
slackApp.action(/^takes_(\w+)$/, async ({ payload, context }) => {
-
const userId = payload.user.id;
-
const channelId = context.channelId || "";
-
const actionId = payload.actions[0]?.action_id as string;
-
const command = actionId.replace("takes_", "");
-
const descriptionInput = payload.state.values.note_block?.note_input;
-
-
let response: MessageResponse | undefined;
-
-
const activeTake = await getActiveTake(userId);
-
-
// Route to the appropriate handler function
-
switch (command) {
-
case "start": {
-
if (activeTake.length > 0) {
-
if (context.respond) {
-
response = await handleStatus(userId);
-
}
-
} else {
-
if (!descriptionInput?.value?.trim()) {
-
response = getDescriptionBlocks(
-
"Please enter a note for your session.",
-
);
-
} else {
-
response = await handleStart(
-
userId,
-
channelId,
-
descriptionInput?.value?.trim(),
-
);
-
}
-
}
-
break;
-
}
-
case "pause":
-
response = await handlePause(userId);
-
break;
-
case "resume":
-
response = await handleResume(userId);
-
break;
-
case "stop":
-
response = await handleStop(userId);
-
break;
-
case "edit": {
-
if (!activeTake.length && context.respond) {
-
await context.respond({
-
text: "You don't have an active takes session to edit!",
-
response_type: "ephemeral",
-
});
-
return;
-
}
-
-
if (!descriptionInput) {
-
response = getEditDescriptionBlocks(
-
activeTake[0]?.description || "",
-
);
-
} else if (descriptionInput.value?.trim()) {
-
const takeToUpdate = activeTake[0];
-
if (!takeToUpdate) return;
-
-
// Update the note for the active session
-
await db.update(takesTable).set({
-
description: descriptionInput.value.trim(),
-
});
-
-
response = await handleStatus(userId);
-
} else {
-
response = getEditDescriptionBlocks(
-
"",
-
"Please enter a note for your session.",
-
);
-
}
-
break;
-
}
-
-
case "status":
-
response = await handleStatus(userId);
-
break;
-
case "history":
-
response = await handleHistory(userId);
-
break;
-
default:
-
response = await handleHelp();
-
break;
-
}
-
-
// Send the response
-
if (response && context.respond) {
-
await context.respond(response);
-
}
-
});
-
-
// Setup scheduled tasks
-
const notificationInterval = TakesConfig.NOTIFICATIONS.CHECK_INTERVAL;
-
setInterval(async () => {
-
await checkActiveSessions();
-
await expirePausedSessions();
-
}, notificationInterval);
-
};
-
-
export default takes;
+50
src/features/takes/handlers/help.ts
···
+
import TakesConfig from "../../../libs/config";
+
import type { MessageResponse } from "../types";
+
+
export default async function handleHelp(): Promise<MessageResponse> {
+
return {
+
text: `*Takes Commands*\n\nโ€ข \`/takes start [minutes]\` - Start a new takes session, optionally specifying duration\nโ€ข \`/takes pause\` - Pause your current session (max ${TakesConfig.MAX_PAUSE_DURATION} min)\nโ€ข \`/takes resume\` - Resume your paused session\nโ€ข \`/takes stop [notes]\` - End your current session with optional notes\nโ€ข \`/takes status\` - Check the status of your session\nโ€ข \`/takes history\` - View your past takes sessions`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: "*Takes Commands*",
+
},
+
},
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `โ€ข \`/takes start [minutes]\` - Start a new session (default: ${TakesConfig.DEFAULT_SESSION_LENGTH} min)\nโ€ข \`/takes pause\` - Pause your session (max ${TakesConfig.MAX_PAUSE_DURATION} min)\nโ€ข \`/takes resume\` - Resume your paused session\nโ€ข \`/takes stop [notes]\` - End session with optional notes\nโ€ข \`/takes status\` - Check status\nโ€ข \`/takes history\` - View past sessions`,
+
},
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐ŸŽฌ Start New Session",
+
emoji: true,
+
},
+
value: "start",
+
action_id: "takes_start",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ“‹ History",
+
emoji: true,
+
},
+
value: "history",
+
action_id: "takes_history",
+
},
+
],
+
},
+
],
+
};
+
}
+114
src/features/takes/handlers/history.ts
···
+
import type { AnyMessageBlock } from "slack-edge";
+
import TakesConfig from "../../../libs/config";
+
import { getCompletedTakes } from "../services/database";
+
import type { MessageResponse } from "../types";
+
+
export async function handleHistory(userId: string): Promise<MessageResponse> {
+
// Get completed takes for the user
+
const completedTakes = (
+
await getCompletedTakes(userId, TakesConfig.MAX_HISTORY_ITEMS)
+
).sort(
+
(a, b) =>
+
(b.completedAt?.getTime() ?? 0) - (a.completedAt?.getTime() ?? 0),
+
);
+
+
if (completedTakes.length === 0) {
+
return {
+
text: "You haven't completed any takes sessions yet.",
+
response_type: "ephemeral",
+
};
+
}
+
+
// Create blocks for each completed take
+
const historyBlocks: AnyMessageBlock[] = [
+
{
+
type: "header",
+
text: {
+
type: "plain_text",
+
text: `๐Ÿ“‹ Your most recent ${completedTakes.length} Takes Sessions`,
+
emoji: true,
+
},
+
},
+
];
+
+
for (const take of completedTakes) {
+
const startTime = new Date(take.startedAt);
+
const endTime = take.completedAt || startTime;
+
+
// Calculate duration in minutes
+
const durationMs = endTime.getTime() - startTime.getTime();
+
const pausedMs = take.pausedTimeMs || 0;
+
const activeDuration = Math.round((durationMs - pausedMs) / 60000);
+
+
// Format dates
+
const startDate = `<!date^${Math.floor(startTime.getTime() / 1000)}^{date_short_pretty} at {time}|${startTime.toLocaleString()}>`;
+
const endDate = `<!date^${Math.floor(endTime.getTime() / 1000)}^{date_short_pretty} at {time}|${endTime.toLocaleString()}>`;
+
+
const notes = take.notes ? `\nโ€ข Notes: ${take.notes}` : "";
+
const description = take.description
+
? `\nโ€ข Description: ${take.description}\n`
+
: "";
+
+
historyBlocks.push({
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `*Take on ${startDate}*\n${description}โ€ข Duration: ${activeDuration} minutes${
+
pausedMs > 0
+
? ` (+ ${Math.round(pausedMs / 60000)} minutes paused)`
+
: ""
+
}\nโ€ข Started: ${startDate}\nโ€ข Completed: ${endDate}${notes}`,
+
},
+
});
+
+
// Add a divider between entries
+
if (take !== completedTakes[completedTakes.length - 1]) {
+
historyBlocks.push({
+
type: "divider",
+
});
+
}
+
}
+
+
// Add actions block
+
historyBlocks.push({
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐ŸŽฌ Start New Session",
+
emoji: true,
+
},
+
value: "start",
+
action_id: "takes_start",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ‘๏ธ Status",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_status",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ”„ Refresh",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_history",
+
},
+
],
+
});
+
+
return {
+
text: `Your recent takes history (${completedTakes.length} sessions)`,
+
response_type: "ephemeral",
+
blocks: historyBlocks,
+
};
+
}
+113
src/features/takes/handlers/pause.ts
···
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import { eq } from "drizzle-orm";
+
import TakesConfig from "../../../libs/config";
+
import { getActiveTake } from "../services/database";
+
import type { MessageResponse } from "../types";
+
import { prettyPrintTime } from "../../../libs/time";
+
+
export default async function handlePause(
+
userId: string,
+
): Promise<MessageResponse | undefined> {
+
const activeTake = await getActiveTake(userId);
+
if (activeTake.length === 0) {
+
return {
+
text: `You don't have an active takes session! Use \`/takes start\` to begin.`,
+
response_type: "ephemeral",
+
};
+
}
+
+
const takeToUpdate = activeTake[0];
+
if (!takeToUpdate) {
+
return;
+
}
+
+
// Update the takes entry to paused status
+
await db
+
.update(takesTable)
+
.set({
+
status: "paused",
+
pausedAt: new Date(),
+
notifiedPauseExpiration: false, // Reset pause expiration notification
+
})
+
.where(eq(takesTable.id, takeToUpdate.id));
+
+
// Calculate when the pause will expire
+
const pauseExpires = new Date();
+
pauseExpires.setMinutes(
+
pauseExpires.getMinutes() + TakesConfig.MAX_PAUSE_DURATION,
+
);
+
const pauseExpiresStr = `<!date^${Math.floor(pauseExpires.getTime() / 1000)}^{date_short_pretty} at {time}|${pauseExpires.toLocaleString()}>`;
+
+
return {
+
text: `โธ๏ธ Session paused! You have ${prettyPrintTime(takeToUpdate.durationMinutes * 60000)} remaining. It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `โธ๏ธ Session paused! You have ${prettyPrintTime(takeToUpdate.durationMinutes * 60000)} remaining.`,
+
},
+
},
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
+
},
+
],
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โœ๏ธ edit",
+
emoji: true,
+
},
+
value: "edit",
+
action_id: "takes_edit",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โ–ถ๏ธ Resume",
+
emoji: true,
+
},
+
value: "resume",
+
action_id: "takes_resume",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โน๏ธ Stop",
+
emoji: true,
+
},
+
value: "stop",
+
action_id: "takes_stop",
+
style: "danger",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ”„ Refresh",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_status",
+
},
+
],
+
},
+
],
+
};
+
}
+123
src/features/takes/handlers/resume.ts
···
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import { eq } from "drizzle-orm";
+
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
+
import { getPausedTake } from "../services/database";
+
import type { MessageResponse } from "../types";
+
+
export default async function handleResume(
+
userId: string,
+
): Promise<MessageResponse | undefined> {
+
const pausedTake = await getPausedTake(userId);
+
if (pausedTake.length === 0) {
+
return {
+
text: `You don't have a paused takes session!`,
+
response_type: "ephemeral",
+
};
+
}
+
+
const pausedSession = pausedTake[0];
+
if (!pausedSession) {
+
return;
+
}
+
+
const now = new Date();
+
+
// Calculate paused time
+
if (pausedSession.pausedAt) {
+
const pausedTimeMs = now.getTime() - pausedSession.pausedAt.getTime();
+
const totalPausedTime =
+
(pausedSession.pausedTimeMs || 0) + pausedTimeMs;
+
+
// Update the takes entry to active status
+
await db
+
.update(takesTable)
+
.set({
+
status: "active",
+
pausedAt: null,
+
pausedTimeMs: totalPausedTime,
+
notifiedLowTime: false, // Reset low time notification
+
})
+
.where(eq(takesTable.id, pausedSession.id));
+
}
+
+
const endTime = new Date(
+
new Date(pausedSession.startedAt).getTime() +
+
pausedSession.durationMinutes * 60000 +
+
(pausedSession.pausedTimeMs || 0),
+
);
+
+
const timeRemaining = endTime.getTime() - now.getTime();
+
+
return {
+
text: `โ–ถ๏ธ Takes session resumed! You have ${prettyPrintTime(timeRemaining)} remaining in your session.`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: "โ–ถ๏ธ Takes session resumed!",
+
},
+
},
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `You have ${prettyPrintTime(timeRemaining)} remaining until ${generateSlackDate(endTime)}.`,
+
},
+
],
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โœ๏ธ edit",
+
emoji: true,
+
},
+
value: "edit",
+
action_id: "takes_edit",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โธ๏ธ Pause",
+
emoji: true,
+
},
+
value: "pause",
+
action_id: "takes_pause",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โน๏ธ Stop",
+
emoji: true,
+
},
+
value: "stop",
+
action_id: "takes_stop",
+
style: "danger",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ”„ Refresh",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_status",
+
},
+
],
+
},
+
],
+
};
+
}
+116
src/features/takes/handlers/start.ts
···
+
import type { MessageResponse } from "../types";
+
import { getActiveTake } from "../services/database";
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import TakesConfig from "../../../libs/config";
+
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
+
+
export default async function handleStart(
+
userId: string,
+
channelId: string,
+
description?: string,
+
durationMinutes?: number,
+
): Promise<MessageResponse> {
+
const activeTake = await getActiveTake(userId);
+
if (activeTake.length > 0) {
+
return {
+
text: "You already have an active takes session! Use `/takes status` to check it.",
+
response_type: "ephemeral",
+
};
+
}
+
+
// Create new takes session
+
const newTake = {
+
id: Bun.randomUUIDv7(),
+
userId,
+
channelId,
+
status: "active",
+
startedAt: new Date(),
+
durationMinutes: durationMinutes || TakesConfig.DEFAULT_SESSION_LENGTH,
+
description: description || null,
+
notifiedLowTime: false,
+
notifiedPauseExpiration: false,
+
};
+
+
await db.insert(takesTable).values(newTake);
+
+
// Calculate end time for message
+
const endTime = new Date(
+
newTake.startedAt.getTime() + newTake.durationMinutes * 60000,
+
);
+
+
const descriptionText = description
+
? `\n\n*Working on:* ${description}`
+
: "";
+
return {
+
text: `๐ŸŽฌ Takes session started! You have ${prettyPrintTime(newTake.durationMinutes * 60000)} until ${generateSlackDate(endTime)}.${descriptionText}`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `๐ŸŽฌ Takes session started!${descriptionText}`,
+
},
+
},
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `You have ${prettyPrintTime(newTake.durationMinutes * 60000)} left until ${generateSlackDate(endTime)}.`,
+
},
+
],
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โœ๏ธ edit",
+
emoji: true,
+
},
+
value: "edit",
+
action_id: "takes_edit",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โธ๏ธ Pause",
+
emoji: true,
+
},
+
value: "pause",
+
action_id: "takes_pause",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โน๏ธ Stop",
+
emoji: true,
+
},
+
value: "stop",
+
action_id: "takes_stop",
+
style: "danger",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ”„ Refresh",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_status",
+
},
+
],
+
},
+
],
+
};
+
}
+270
src/features/takes/handlers/status.ts
···
+
import TakesConfig from "../../../libs/config";
+
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
+
import {
+
getActiveTake,
+
getCompletedTakes,
+
getPausedTake,
+
} from "../services/database";
+
import { expirePausedSessions } from "../services/notifications";
+
import type { MessageResponse } from "../types";
+
+
export default async function handleStatus(
+
userId: string,
+
): Promise<MessageResponse | undefined> {
+
const activeTake = await getActiveTake(userId);
+
+
// First, check for expired paused sessions
+
await expirePausedSessions();
+
+
if (activeTake.length > 0) {
+
const take = activeTake[0];
+
if (!take) {
+
return;
+
}
+
+
const startTime = new Date(take.startedAt);
+
const endTime = new Date(
+
startTime.getTime() + take.durationMinutes * 60000,
+
);
+
+
// Adjust for paused time
+
if (take.pausedTimeMs) {
+
endTime.setTime(endTime.getTime() + take.pausedTimeMs);
+
}
+
+
const now = new Date();
+
const remainingMs = endTime.getTime() - now.getTime();
+
+
// Add description to display if present
+
const descriptionText = take.description
+
? `\n\n*Working on:* ${take.description}`
+
: "";
+
+
return {
+
text: `๐ŸŽฌ You have an active takes session with ${prettyPrintTime(remainingMs)} remaining.${descriptionText}`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `๐ŸŽฌ You have an active takes session${descriptionText}`,
+
},
+
},
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `You have ${prettyPrintTime(remainingMs)} remaining until ${generateSlackDate(endTime)}.`,
+
},
+
],
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โœ๏ธ edit",
+
emoji: true,
+
},
+
value: "edit",
+
action_id: "takes_edit",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โธ๏ธ Pause",
+
emoji: true,
+
},
+
value: "pause",
+
action_id: "takes_pause",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โน๏ธ Stop",
+
emoji: true,
+
},
+
value: "stop",
+
action_id: "takes_stop",
+
style: "danger",
+
},
+
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ”„ Refresh",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_status",
+
},
+
],
+
},
+
],
+
};
+
}
+
+
// Check for paused session
+
const pausedTakeStatus = await getPausedTake(userId);
+
+
if (pausedTakeStatus.length > 0) {
+
const pausedTake = pausedTakeStatus[0];
+
if (!pausedTake || !pausedTake.pausedAt) {
+
return;
+
}
+
+
// Calculate how much time remains before auto-completion
+
const now = new Date();
+
const pausedDuration =
+
(now.getTime() - pausedTake.pausedAt.getTime()) / (60 * 1000); // In minutes
+
const remainingPauseTime = Math.max(
+
0,
+
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
+
);
+
+
// Format the pause timeout
+
const pauseExpires = new Date(pausedTake.pausedAt);
+
pauseExpires.setMinutes(
+
pauseExpires.getMinutes() + TakesConfig.MAX_PAUSE_DURATION,
+
);
+
const pauseExpiresStr = `<!date^${Math.floor(pauseExpires.getTime() / 1000)}^{date_short_pretty} at {time}|${pauseExpires.toLocaleString()}>`;
+
+
// Add notes to display if present
+
const noteText = pausedTake.notes
+
? `\n\n*Working on:* ${pausedTake.notes}`
+
: "";
+
+
return {
+
text: `โธ๏ธ You have a paused takes session. It will auto-complete in ${remainingPauseTime.toFixed(1)} minutes if not resumed.`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `โธ๏ธ Session paused! You have ${prettyPrintTime(pausedTake.durationMinutes * 60000)} remaining.`,
+
},
+
},
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
+
},
+
],
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โ–ถ๏ธ Resume",
+
emoji: true,
+
},
+
value: "resume",
+
action_id: "takes_resume",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โน๏ธ Stop",
+
emoji: true,
+
},
+
value: "stop",
+
action_id: "takes_stop",
+
style: "danger",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ”„ Refresh",
+
emoji: true,
+
},
+
value: "status",
+
action_id: "takes_status",
+
},
+
],
+
},
+
],
+
};
+
}
+
+
// Check history of completed sessions
+
const completedSessions = await getCompletedTakes(userId);
+
const takeTime = completedSessions.length
+
? (() => {
+
const diffMs =
+
new Date().getTime() -
+
// @ts-expect-error - TS doesn't know that we are checking the length
+
completedSessions[
+
completedSessions.length - 1
+
].startedAt.getTime();
+
+
const hours = Math.ceil(diffMs / (1000 * 60 * 60));
+
if (hours < 24) return `${hours} hours`;
+
+
const weeks = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 7));
+
if (weeks > 0 && weeks < 4) return `${weeks} weeks`;
+
+
const months = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 30));
+
return `${months} months`;
+
})()
+
: 0;
+
+
return {
+
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the last ${takeTime}.`,
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the last ${takeTime}.`,
+
},
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐ŸŽฌ Start New Session",
+
emoji: true,
+
},
+
value: "start",
+
action_id: "takes_start",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ“‹ History",
+
emoji: true,
+
},
+
value: "history",
+
action_id: "takes_history",
+
},
+
],
+
},
+
],
+
};
+
}
+117
src/features/takes/handlers/stop.ts
···
+
import { slackClient } from "../../../index";
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import { eq } from "drizzle-orm";
+
import { getActiveTake, getPausedTake } from "../services/database";
+
import type { MessageResponse } from "../types";
+
+
export default async function handleStop(
+
userId: string,
+
args?: string[],
+
): Promise<MessageResponse | undefined> {
+
const activeTake = await getActiveTake(userId);
+
+
if (activeTake.length === 0) {
+
const pausedTake = await getPausedTake(userId);
+
+
if (pausedTake.length === 0) {
+
return {
+
text: `You don't have an active or paused takes session!`,
+
response_type: "ephemeral",
+
};
+
}
+
+
// Mark the paused session as completed
+
const pausedTakeToStop = pausedTake[0];
+
if (!pausedTakeToStop) {
+
return;
+
}
+
+
// Extract notes if provided
+
let notes = undefined;
+
if (args && args.length > 1) {
+
notes = args.slice(1).join(" ");
+
}
+
+
const res = await slackClient.chat.postMessage({
+
channel: userId,
+
text: "๐ŸŽฌ Your paused takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
+
});
+
+
await db
+
.update(takesTable)
+
.set({
+
status: "waitingUpload",
+
ts: res.ts,
+
completedAt: new Date(),
+
...(notes && { notes }),
+
})
+
.where(eq(takesTable.id, pausedTakeToStop.id));
+
} else {
+
// Mark the active session as completed
+
const activeTakeToStop = activeTake[0];
+
if (!activeTakeToStop) {
+
return;
+
}
+
+
// Extract notes if provided
+
let notes = undefined;
+
if (args && args.length > 1) {
+
notes = args.slice(1).join(" ");
+
}
+
+
const res = await slackClient.chat.postMessage({
+
channel: userId,
+
text: "๐ŸŽฌ Your takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
+
});
+
+
await db
+
.update(takesTable)
+
.set({
+
status: "waitingUpload",
+
ts: res.ts,
+
completedAt: new Date(),
+
...(notes && { notes }),
+
})
+
.where(eq(takesTable.id, activeTakeToStop.id));
+
}
+
+
return {
+
text: "โœ… Takes session completed! I hope you had fun!",
+
response_type: "ephemeral",
+
blocks: [
+
{
+
type: "section",
+
text: {
+
type: "mrkdwn",
+
text: "โœ… Takes session completed! I hope you had fun!",
+
},
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐ŸŽฌ Start New Session",
+
emoji: true,
+
},
+
value: "start",
+
action_id: "takes_start",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐Ÿ“‹ History",
+
emoji: true,
+
},
+
value: "history",
+
action_id: "takes_history",
+
},
+
],
+
},
+
],
+
};
+
}
+11
src/features/takes/index.ts
···
+
import setupCommands from "./setup/commands";
+
import setupActions from "./setup/actions";
+
import setupNotifications from "./setup/notifications";
+
+
const takes = async () => {
+
setupCommands();
+
setupActions();
+
setupNotifications();
+
};
+
+
export default takes;
+37
src/features/takes/services/database.ts
···
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import { eq, and, desc } from "drizzle-orm";
+
+
export async function getActiveTake(userId: string) {
+
return db
+
.select()
+
.from(takesTable)
+
.where(
+
and(eq(takesTable.userId, userId), eq(takesTable.status, "active")),
+
)
+
.limit(1);
+
}
+
+
export async function getPausedTake(userId: string) {
+
return db
+
.select()
+
.from(takesTable)
+
.where(
+
and(eq(takesTable.userId, userId), eq(takesTable.status, "paused")),
+
)
+
.limit(1);
+
}
+
+
export async function getCompletedTakes(userId: string, limit = 5) {
+
return db
+
.select()
+
.from(takesTable)
+
.where(
+
and(
+
eq(takesTable.userId, userId),
+
eq(takesTable.status, "completed"),
+
),
+
)
+
.orderBy(desc(takesTable.completedAt))
+
.limit(limit);
+
}
+154
src/features/takes/services/notifications.ts
···
+
import { slackApp } from "../../../index";
+
import TakesConfig from "../../../libs/config";
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import { eq } from "drizzle-orm";
+
+
// Check for paused sessions that have exceeded the max pause duration
+
export async function expirePausedSessions() {
+
const now = new Date();
+
const pausedTakes = await db
+
.select()
+
.from(takesTable)
+
.where(eq(takesTable.status, "paused"));
+
+
for (const take of pausedTakes) {
+
if (take.pausedAt) {
+
const pausedDuration =
+
(now.getTime() - take.pausedAt.getTime()) / (60 * 1000); // Convert to minutes
+
+
// Send warning notification when getting close to expiration
+
if (
+
pausedDuration >
+
TakesConfig.MAX_PAUSE_DURATION -
+
TakesConfig.NOTIFICATIONS.PAUSE_EXPIRATION_WARNING &&
+
!take.notifiedPauseExpiration
+
) {
+
// Update notification flag
+
await db
+
.update(takesTable)
+
.set({
+
notifiedPauseExpiration: true,
+
})
+
.where(eq(takesTable.id, take.id));
+
+
// Send warning message
+
try {
+
const timeRemaining = Math.round(
+
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
+
);
+
await slackApp.client.chat.postMessage({
+
channel: take.userId,
+
text: `โš ๏ธ Reminder: Your paused takes session will automatically complete in about ${timeRemaining} minutes if not resumed.`,
+
});
+
} catch (error) {
+
console.error(
+
"Failed to send pause expiration warning:",
+
error,
+
);
+
}
+
}
+
+
// Auto-expire paused sessions that exceed the max pause duration
+
if (pausedDuration > TakesConfig.MAX_PAUSE_DURATION) {
+
let ts: string | undefined;
+
// Notify user that their session was auto-completed
+
try {
+
const res = await slackApp.client.chat.postMessage({
+
channel: take.userId,
+
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!`,
+
});
+
ts = res.ts;
+
} catch (error) {
+
console.error(
+
"Failed to notify user of auto-completed session:",
+
error,
+
);
+
}
+
+
await db
+
.update(takesTable)
+
.set({
+
status: "waitingUpload",
+
completedAt: now,
+
ts,
+
notes: take.notes
+
? `${take.notes} (Automatically completed due to pause timeout)`
+
: "Automatically completed due to pause timeout",
+
})
+
.where(eq(takesTable.id, take.id));
+
}
+
}
+
}
+
}
+
+
// Check for active sessions that are almost done
+
export async function checkActiveSessions() {
+
const now = new Date();
+
const activeTakes = await db
+
.select()
+
.from(takesTable)
+
.where(eq(takesTable.status, "active"));
+
+
for (const take of activeTakes) {
+
const endTime = new Date(
+
take.startedAt.getTime() +
+
take.durationMinutes * 60000 +
+
(take.pausedTimeMs || 0),
+
);
+
+
const remainingMs = endTime.getTime() - now.getTime();
+
const remainingMinutes = remainingMs / 60000;
+
+
if (
+
remainingMinutes <= TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING &&
+
remainingMinutes > 0 &&
+
!take.notifiedLowTime
+
) {
+
await db
+
.update(takesTable)
+
.set({ notifiedLowTime: true })
+
.where(eq(takesTable.id, take.id));
+
+
console.log("Sending low time warning to user");
+
+
try {
+
await slackApp.client.chat.postMessage({
+
channel: take.userId,
+
text: `โฑ๏ธ Your takes session has less than ${TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING} minutes remaining.`,
+
});
+
} catch (error) {
+
console.error("Failed to send low time warning:", error);
+
}
+
}
+
+
if (remainingMs <= 0) {
+
let ts: string | undefined;
+
try {
+
const res = await slackApp.client.chat.postMessage({
+
channel: take.userId,
+
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!",
+
});
+
+
ts = res.ts;
+
} catch (error) {
+
console.error(
+
"Failed to notify user of completed session:",
+
error,
+
);
+
}
+
+
await db
+
.update(takesTable)
+
.set({
+
status: "waitingUpload",
+
completedAt: now,
+
ts,
+
notes: take.notes
+
? `${take.notes} (Automatically completed - time expired)`
+
: "Automatically completed - time expired",
+
})
+
.where(eq(takesTable.id, take.id));
+
}
+
}
+
}
+111
src/features/takes/setup/actions.ts
···
+
import { slackApp } from "../../../index";
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
+
import handleHelp from "../handlers/help";
+
import { handleHistory } from "../handlers/history";
+
import handlePause from "../handlers/pause";
+
import handleResume from "../handlers/resume";
+
import handleStart from "../handlers/start";
+
import handleStatus from "../handlers/status";
+
import handleStop from "../handlers/stop";
+
import { getActiveTake } from "../services/database";
+
import upload from "../services/upload";
+
import type { MessageResponse } from "../types";
+
import { getDescriptionBlocks, getEditDescriptionBlocks } from "../ui/blocks";
+
+
export default function setupActions() {
+
// Handle button actions
+
slackApp.action(/^takes_(\w+)$/, async ({ payload, context }) => {
+
const userId = payload.user.id;
+
const channelId = context.channelId || "";
+
const actionId = payload.actions[0]?.action_id as string;
+
const command = actionId.replace("takes_", "");
+
const descriptionInput = payload.state.values.note_block?.note_input;
+
+
let response: MessageResponse | undefined;
+
+
const activeTake = await getActiveTake(userId);
+
+
// Route to the appropriate handler function
+
switch (command) {
+
case "start": {
+
if (activeTake.length > 0) {
+
if (context.respond) {
+
response = await handleStatus(userId);
+
}
+
} else {
+
if (!descriptionInput?.value?.trim()) {
+
response = getDescriptionBlocks(
+
"Please enter a note for your session.",
+
);
+
} else {
+
response = await handleStart(
+
userId,
+
channelId,
+
descriptionInput?.value?.trim(),
+
);
+
}
+
}
+
break;
+
}
+
case "pause":
+
response = await handlePause(userId);
+
break;
+
case "resume":
+
response = await handleResume(userId);
+
break;
+
case "stop":
+
response = await handleStop(userId);
+
break;
+
case "edit": {
+
if (!activeTake.length && context.respond) {
+
await context.respond({
+
text: "You don't have an active takes session to edit!",
+
response_type: "ephemeral",
+
});
+
return;
+
}
+
+
if (!descriptionInput) {
+
response = getEditDescriptionBlocks(
+
activeTake[0]?.description || "",
+
);
+
} else if (descriptionInput.value?.trim()) {
+
const takeToUpdate = activeTake[0];
+
if (!takeToUpdate) return;
+
+
// Update the note for the active session
+
await db.update(takesTable).set({
+
description: descriptionInput.value.trim(),
+
});
+
+
response = await handleStatus(userId);
+
} else {
+
response = getEditDescriptionBlocks(
+
"",
+
"Please enter a note for your session.",
+
);
+
}
+
break;
+
}
+
+
case "status":
+
response = await handleStatus(userId);
+
break;
+
case "history":
+
response = await handleHistory(userId);
+
break;
+
default:
+
response = await handleHelp();
+
break;
+
}
+
+
// Send the response
+
if (response && context.respond) {
+
await context.respond(response);
+
}
+
});
+
+
// setup the upload actions
+
upload();
+
}
+98
src/features/takes/setup/commands.ts
···
+
import { environment, slackApp } from "../../../index";
+
import handleHelp from "../handlers/help";
+
import { handleHistory } from "../handlers/history";
+
import handlePause from "../handlers/pause";
+
import handleResume from "../handlers/resume";
+
import handleStart from "../handlers/start";
+
import handleStatus from "../handlers/status";
+
import handleStop from "../handlers/stop";
+
import { getActiveTake, getPausedTake } from "../services/database";
+
import {
+
checkActiveSessions,
+
expirePausedSessions,
+
} from "../services/notifications";
+
import type { MessageResponse } from "../types";
+
import { getDescriptionBlocks, getEditDescriptionBlocks } from "../ui/blocks";
+
+
export default function setupCommands() {
+
// Main command handler
+
slackApp.command(
+
environment === "dev" ? "/takes-dev" : "/takes",
+
async ({ payload, context }): Promise<void> => {
+
const userId = payload.user_id;
+
const channelId = payload.channel_id;
+
const text = payload.text || "";
+
const args = text.trim().split(/\s+/);
+
let subcommand = args[0]?.toLowerCase() || "";
+
+
// Check for active takes session
+
const activeTake = await getActiveTake(userId);
+
+
// Check for paused session if no active one
+
const pausedTakeCheck =
+
activeTake.length === 0 ? await getPausedTake(userId) : [];
+
+
// Run checks for expired or about-to-expire sessions
+
await expirePausedSessions();
+
await checkActiveSessions();
+
+
// Default to status if we have an active or paused session and no command specified
+
if (
+
subcommand === "" &&
+
(activeTake.length > 0 || pausedTakeCheck.length > 0)
+
) {
+
subcommand = "status";
+
} else if (subcommand === "") {
+
subcommand = "help";
+
}
+
+
let response: MessageResponse | undefined;
+
+
// Special handling for start command to show modal
+
if (subcommand === "start" && !activeTake.length) {
+
response = getDescriptionBlocks();
+
}
+
+
// Route to the appropriate handler function
+
switch (subcommand) {
+
case "start":
+
response = await handleStart(userId, channelId);
+
break;
+
case "pause":
+
response = await handlePause(userId);
+
break;
+
case "resume":
+
response = await handleResume(userId);
+
break;
+
case "stop":
+
response = await handleStop(userId, args);
+
break;
+
case "edit":
+
response = getEditDescriptionBlocks(
+
activeTake[0]?.description || "",
+
);
+
break;
+
case "status":
+
response = await handleStatus(userId);
+
break;
+
case "history":
+
response = await handleHistory(userId);
+
break;
+
case "help":
+
response = await handleHelp();
+
break;
+
default:
+
response = await handleHelp();
+
break;
+
}
+
+
if (context.respond)
+
await context.respond(
+
response || {
+
text: "An error occurred while processing your request.",
+
response_type: "ephemeral",
+
},
+
);
+
},
+
);
+
}
+13
src/features/takes/setup/notifications.ts
···
+
import TakesConfig from "../../../libs/config";
+
import {
+
checkActiveSessions,
+
expirePausedSessions,
+
} from "../services/notifications";
+
+
export default function setupNotifications() {
+
const notificationInterval = TakesConfig.NOTIFICATIONS.CHECK_INTERVAL;
+
setInterval(async () => {
+
await checkActiveSessions();
+
await expirePausedSessions();
+
}, notificationInterval);
+
}
+7
src/features/takes/types.ts
···
+
import type { AnyMessageBlock } from "slack-edge";
+
+
export type MessageResponse = {
+
blocks?: AnyMessageBlock[];
+
text: string;
+
response_type: "ephemeral" | "in_channel";
+
};
+148
src/features/takes/ui/blocks.ts
···
+
import type { AnyMessageBlock } from "slack-edge";
+
import type { MessageResponse } from "../types";
+
+
export function getDescriptionBlocks(error?: string): MessageResponse {
+
const blocks: AnyMessageBlock[] = [
+
{
+
type: "input",
+
block_id: "note_block",
+
element: {
+
type: "plain_text_input",
+
action_id: "note_input",
+
placeholder: {
+
type: "plain_text",
+
text: "Enter a note for your session",
+
},
+
multiline: true,
+
},
+
label: {
+
type: "plain_text",
+
text: "Note",
+
},
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "๐ŸŽฌ Start Session",
+
emoji: true,
+
},
+
value: "start",
+
action_id: "takes_start",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โ›” Cancel",
+
emoji: true,
+
},
+
value: "cancel",
+
action_id: "takes_status",
+
style: "danger",
+
},
+
],
+
},
+
];
+
+
if (error) {
+
blocks.push(
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `โš ๏ธ ${error}`,
+
},
+
],
+
},
+
);
+
}
+
+
return {
+
text: "Please enter a note for your session:",
+
response_type: "ephemeral",
+
blocks,
+
};
+
}
+
+
export function getEditDescriptionBlocks(
+
description: string,
+
error?: string,
+
): MessageResponse {
+
const blocks: AnyMessageBlock[] = [
+
{
+
type: "input",
+
block_id: "note_block",
+
element: {
+
type: "plain_text_input",
+
action_id: "note_input",
+
placeholder: {
+
type: "plain_text",
+
text: "Enter a note for your session",
+
},
+
multiline: true,
+
initial_value: description,
+
},
+
label: {
+
type: "plain_text",
+
text: "Note",
+
},
+
},
+
{
+
type: "actions",
+
elements: [
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โœ๏ธ Update Note",
+
emoji: true,
+
},
+
value: "start",
+
action_id: "takes_edit",
+
},
+
{
+
type: "button",
+
text: {
+
type: "plain_text",
+
text: "โ›” Cancel",
+
emoji: true,
+
},
+
value: "cancel",
+
action_id: "takes_status",
+
style: "danger",
+
},
+
],
+
},
+
];
+
+
if (error) {
+
blocks.push(
+
{
+
type: "divider",
+
},
+
{
+
type: "context",
+
elements: [
+
{
+
type: "mrkdwn",
+
text: `โš ๏ธ ${error}`,
+
},
+
],
+
},
+
);
+
}
+
+
return {
+
text: "Please enter a note for your session:",
+
response_type: "ephemeral",
+
blocks,
+
};
+
}
+6 -8
src/features/upload.ts src/features/takes/services/upload.ts
···
-
import { slackApp, slackClient } from "../index";
-
import { db } from "../libs/db";
-
import { takes as takesTable } from "../libs/schema";
+
import { slackApp, slackClient } from "../../../index";
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
import { eq, and } from "drizzle-orm";
-
import { prettyPrintTime } from "../libs/time";
+
import { prettyPrintTime } from "../../../libs/time";
-
const upload = async () => {
+
export default async function upload() {
slackApp.anyMessage(async ({ payload }) => {
try {
if (payload.subtype !== "file_share") return;
···
delete_original: true,
});
});
-
};
-
-
export default upload;
+
}
+3 -3
src/features/video.ts src/features/api/routes/video.ts
···
-
import { db } from "../libs/db";
-
import { takes as takesTable } from "../libs/schema";
+
import { db } from "../../../libs/db";
+
import { takes as takesTable } from "../../../libs/schema";
import { eq, and } from "drizzle-orm";
-
export async function getVideo(url: URL): Promise<Response> {
+
export default async function getVideo(url: URL): Promise<Response> {
const videoId = url.pathname.split("/")[2];
if (!videoId) {
+4 -4
src/index.ts
···
import { t } from "./libs/template";
import { blog } from "./libs/Logger";
import { version, name } from "../package.json";
-
import { getVideo } from "./features/video";
+
import { video } from "./features/api";
const environment = process.env.NODE_ENV;
import * as Sentry from "@sentry/bun";
···
console.log(
`----------------------------------\n${name} Server\n----------------------------------\n`,
);
-
console.log(`๐Ÿ—๏ธ Starting ${name}...`);
+
console.log(`๐Ÿ—๏ธ Starting ${name}...`);
console.log("๐Ÿ“ฆ Loading Slack App...");
console.log("๐Ÿ”‘ Loading environment variables...");
···
});
const slackClient = slackApp.client;
-
console.log(`โš’๏ธ Loading ${Object.entries(features).length} features...`);
+
console.log(`โš’๏ธ Loading ${Object.entries(features).length} features...`);
for (const [feature, handler] of Object.entries(features)) {
console.log(`๐Ÿ“ฆ ${feature} loaded`);
if (typeof handler === "function") {
···
case "/slack":
return slackApp.run(request);
case "/video":
-
return getVideo(url);
+
return video(url);
default:
return new Response("404 Not Found", { status: 404 });
}