···
import { slackApp } from "../index";
import { db } from "../libs/db";
import { takes as takesTable } from "../libs/schema";
-
import { eq, and, isNull } from "drizzle-orm";
blocks?: AnyMessageBlock[];
···
const takes = async () => {
// Helper functions for command actions
const getActiveTake = async (userId: string) => {
···
-
const getCompletedTakes = async (userId: string) => {
···
eq(takesTable.userId, userId),
eq(takesTable.status, "completed"),
// Command action handlers
const handleStart = async (
): Promise<MessageResponse> => {
const activeTake = await getActiveTake(userId);
if (activeTake.length > 0) {
-
text: `You already have an active takes session! Use \`/takes status\` to check it.`,
response_type: "ephemeral",
···
-
durationMinutes: 5, // 5 minutes for testing (should be 90)
await db.insert(takesTable).values(newTake);
···
const endTimeStr = `<!date^${Math.floor(endTime.getTime() / 1000)}^{time}|${endTime.toLocaleTimeString()}>`;
-
text: `🎬 Takes session started! You have ${newTake.durationMinutes} minutes until ${endTimeStr}.`,
-
response_type: "in_channel",
-
text: `🎬 Takes session started! You have ${newTake.durationMinutes} minutes until ${endTimeStr}.`,
···
···
.where(eq(takesTable.id, takeToUpdate.id));
-
text: `⏸️ Takes session paused! Use \`/takes resume\` to continue.`,
-
response_type: "in_channel",
-
text: `⏸️ Takes session paused!`,
···
···
pausedTimeMs: totalPausedTime,
.where(eq(takesTable.id, pausedSession.id));
-
text: `▶️ Takes session resumed!`,
-
response_type: "in_channel",
-
text: `▶️ Takes session resumed!`,
···
···
const handleStop = async (
): Promise<MessageResponse | undefined> => {
const activeTake = await getActiveTake(userId);
···
.where(eq(takesTable.id, pausedTakeToStop.id));
···
.where(eq(takesTable.id, activeTakeToStop.id));
-
text: `✅ Takes session completed! Thanks for your contribution.`,
-
response_type: "in_channel",
-
text: `✅ Takes session completed! Thanks for your contribution.`,
···
action_id: "takes_start",
···
): Promise<MessageResponse | undefined> => {
const activeTake = await getActiveTake(userId);
if (activeTake.length > 0) {
const take = activeTake[0];
···
endTime.setTime(endTime.getTime() + take.pausedTimeMs);
const remainingMs = endTime.getTime() - now.getTime();
-
if (remainingMs < 120000) {
-
remaining = `${Math.max(0, Math.floor(remainingMs / 1000))} seconds`;
-
remaining = `${Math.max(0, Math.floor(remainingMs / 60000))} minutes`;
-
text: `You have an active takes session with ${remaining} minutes remaining.`,
response_type: "ephemeral",
-
text: `You have an active takes session with *${remaining}* remaining.`,
···
···
const pausedTakeStatus = await getPausedTake(userId);
if (pausedTakeStatus.length > 0) {
-
text: `You have a paused takes session. Use \`/takes resume\` to continue.`,
response_type: "ephemeral",
-
text: `You have a paused takes session.`,
···
···
// Check history of completed sessions
const completedSessions = await getCompletedTakes(userId);
-
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the past.`,
response_type: "ephemeral",
-
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the past.`,
···
action_id: "takes_start",
const handleHelp = async (): Promise<MessageResponse> => {
-
text: `*Takes Commands*\n\n• \`/takes start\` - Start a new takes session\n• \`/takes pause\` - Pause your current session\n• \`/takes resume\` - Resume your paused session\n• \`/takes stop\` - End your current session\n• \`/takes status\` - Check the status of your session`,
response_type: "ephemeral",
···
-
text: "• `/takes start` - Start a new takes session\n• `/takes pause` - Pause your current session\n• `/takes resume` - Resume your paused session\n• `/takes stop` - End your current session\n• `/takes status` - Check the status of your session",
···
action_id: "takes_start",
slackApp.command("/takes", async ({ payload, context }): Promise<void> => {
···
activeTake.length === 0 ? await getPausedTake(userId) : [];
// Default to status if we have an active or paused session and no command specified
···
let response: MessageResponse | undefined;
// Route to the appropriate handler function
···
response = await handleResume(userId);
-
response = await handleStop(userId);
response = await handleStatus(userId);
response = await handleHelp();
···
-
slackApp.action(/^takes_(\w+)$/, async ({ body, context }) => {
-
const userId = body.user.id;
-
const channelId = body.channel?.id || "";
-
const actionId = body.actions[0].action_id;
const command = actionId.replace("takes_", "");
let response: MessageResponse | undefined;
// Route to the appropriate handler function
-
response = await handleStart(userId, channelId);
response = await handlePause(userId);
···
response = await handleStop(userId);
response = await handleStatus(userId);
response = await handleHelp();
-
text: "An error occurred while processing your request.",
···
import { slackApp } 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";
blocks?: AnyMessageBlock[];
···
const takes = async () => {
+
// Helper function for pretty-printing time
+
const prettyPrintTime = (ms: number): string => {
+
const minutes = Math.round(ms / 60000);
+
const seconds = Math.max(0, Math.round(ms / 1000));
+
return `${seconds} seconds`;
+
return `${minutes} minutes`;
// Helper functions for command actions
const getActiveTake = async (userId: string) => {
···
+
const getCompletedTakes = async (userId: string, limit = 5) => {
···
eq(takesTable.userId, userId),
eq(takesTable.status, "completed"),
+
.orderBy(desc(takesTable.completedAt))
+
// Check for paused sessions that have exceeded the max pause duration
+
const expirePausedSessions = async () => {
+
const now = new Date();
+
const pausedTakes = await db
+
.where(eq(takesTable.status, "paused"));
+
for (const take of pausedTakes) {
+
(now.getTime() - take.pausedAt.getTime()) / (60 * 1000); // Convert to minutes
+
// Send warning notification when getting close to expiration
+
TakesConfig.MAX_PAUSE_DURATION -
+
TakesConfig.NOTIFICATIONS
+
.PAUSE_EXPIRATION_WARNING &&
+
!take.notifiedPauseExpiration
+
// Update notification flag
+
notifiedPauseExpiration: true,
+
.where(eq(takesTable.id, take.id));
+
// Send warning message
+
const timeRemaining = Math.round(
+
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
+
await slackApp.client.chat.postMessage({
+
text: `⚠️ Reminder: Your paused takes session will automatically complete in about ${timeRemaining} minutes if not resumed.`,
+
"Failed to send pause expiration warning:",
+
// Auto-expire paused sessions that exceed the max pause duration
+
if (pausedDuration > TakesConfig.MAX_PAUSE_DURATION) {
+
? `${take.notes} (Automatically completed due to pause timeout)`
+
: "Automatically completed due to pause timeout",
+
.where(eq(takesTable.id, take.id));
+
// Notify user that their session was auto-completed
+
await slackApp.client.chat.postMessage({
+
text: `⏰ Your paused takes session has been automatically completed because it was paused for more than ${TakesConfig.MAX_PAUSE_DURATION} minutes.`,
+
"Failed to notify user of auto-completed session:",
+
// Check for active sessions that are almost done
+
const checkActiveSessions = async () => {
+
const now = new Date();
+
const activeTakes = await db
+
.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;
+
TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING &&
+
remainingMinutes > 0 &&
+
.set({ notifiedLowTime: true })
+
.where(eq(takesTable.id, take.id));
+
console.log("Sending low time warning to user");
+
await slackApp.client.chat.postMessage({
+
text: `⏱️ Your takes session has less than ${TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING} minutes remaining.`,
+
console.error("Failed to send low time warning:", error);
+
if (remainingMs <= 0) {
+
? `${take.notes} (Automatically completed - time expired)`
+
: "Automatically completed - time expired",
+
.where(eq(takesTable.id, take.id));
+
await slackApp.client.chat.postMessage({
+
text: "⏰ Your takes session has automatically completed because the time is up.",
+
"Failed to notify user of completed session:",
// Command action handlers
const handleStart = async (
+
durationMinutes?: number,
): Promise<MessageResponse> => {
const activeTake = await getActiveTake(userId);
if (activeTake.length > 0) {
+
text: "You already have an active takes session! Use `/takes status` to check it.",
response_type: "ephemeral",
···
+
durationMinutes || TakesConfig.DEFAULT_SESSION_LENGTH,
+
description: description || null,
+
notifiedLowTime: false,
+
notifiedPauseExpiration: false,
await db.insert(takesTable).values(newTake);
···
const endTimeStr = `<!date^${Math.floor(endTime.getTime() / 1000)}^{time}|${endTime.toLocaleTimeString()}>`;
+
const descriptionText = description
+
? `\n\n*Working on:* ${description}`
+
text: `🎬 Takes session started! You have ${prettyPrintTime(newTake.durationMinutes * 60000)} until ${endTimeStr}.${descriptionText}`,
+
response_type: "ephemeral",
+
text: `🎬 Takes session started!${descriptionText}`,
+
text: `You have ${prettyPrintTime(newTake.durationMinutes * 60000)} left until ${endTimeStr}.`,
+
action_id: "takes_edit",
···
+
action_id: "takes_status",
···
+
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()}>`;
+
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",
+
text: `⏸️ Session paused! You have ${prettyPrintTime(takeToUpdate.durationMinutes * 60000)} remaining.`,
+
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
+
action_id: "takes_edit",
···
+
action_id: "takes_status",
···
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 endTimeStr = `<!date^${Math.floor(endTime.getTime() / 1000)}^{time}|${endTime.toLocaleTimeString()}>`;
+
text: `▶️ Takes session resumed! You have ${prettyPrintTime(pausedSession.durationMinutes * 60000)} remaining in your session.`,
+
response_type: "ephemeral",
+
text: "▶️ Takes session resumed!",
+
text: `You have ${prettyPrintTime(pausedSession.durationMinutes * 60000)} remaining until ${endTimeStr}.`,
+
action_id: "takes_edit",
···
+
action_id: "takes_status",
···
const handleStop = async (
): Promise<MessageResponse | undefined> => {
const activeTake = await getActiveTake(userId);
···
+
// Extract notes if provided
+
if (args && args.length > 1) {
+
notes = args.slice(1).join(" ");
+
...(notes && { notes }),
.where(eq(takesTable.id, pausedTakeToStop.id));
···
+
// Extract notes if provided
+
if (args && args.length > 1) {
+
notes = args.slice(1).join(" ");
+
...(notes && { notes }),
.where(eq(takesTable.id, activeTakeToStop.id));
+
text: "✅ Takes session completed! I hope you had fun!",
+
response_type: "ephemeral",
+
text: "✅ Takes session completed! I hope you had fun!",
···
action_id: "takes_start",
+
action_id: "takes_history",
···
): Promise<MessageResponse | undefined> => {
const activeTake = await getActiveTake(userId);
+
// First, check for expired paused sessions
+
await expirePausedSessions();
if (activeTake.length > 0) {
const take = activeTake[0];
···
endTime.setTime(endTime.getTime() + take.pausedTimeMs);
+
const endTimeStr = `<!date^${Math.floor(endTime.getTime() / 1000)}^{time}|${endTime.toLocaleTimeString()}>`;
const remainingMs = endTime.getTime() - now.getTime();
+
// Add description to display if present
+
const descriptionText = take.description
+
? `\n\n*Working on:* ${take.description}`
+
text: `🎬 You have an active takes session with ${prettyPrintTime(remainingMs)} remaining.${descriptionText}`,
response_type: "ephemeral",
+
text: `🎬 You have an active takes session${descriptionText}`,
+
text: `You have ${prettyPrintTime(remainingMs)} remaining until ${endTimeStr}.`,
+
action_id: "takes_edit",
···
+
action_id: "takes_status",
···
const pausedTakeStatus = await getPausedTake(userId);
if (pausedTakeStatus.length > 0) {
+
const pausedTake = pausedTakeStatus[0];
+
if (!pausedTake || !pausedTake.pausedAt) {
+
// Calculate how much time remains before auto-completion
+
const now = new Date();
+
(now.getTime() - pausedTake.pausedAt.getTime()) / (60 * 1000); // In minutes
+
const remainingPauseTime = Math.max(
+
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}`
+
text: `⏸️ You have a paused takes session. It will auto-complete in ${remainingPauseTime.toFixed(1)} minutes if not resumed.`,
response_type: "ephemeral",
+
text: `⏸️ Session paused! You have ${prettyPrintTime(pausedTake.durationMinutes * 60000)} remaining.`,
+
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
···
+
action_id: "takes_status",
···
// Check history of completed sessions
const completedSessions = await getCompletedTakes(userId);
+
const takeTime = completedSessions.length
+
// @ts-expect-error - TS doesn't know that we are checking the length
+
completedSessions.length - 1
+
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`;
+
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the last ${takeTime}.`,
response_type: "ephemeral",
+
text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the last ${takeTime}.`,
···
action_id: "takes_start",
+
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)
+
(b.completedAt?.getTime() ?? 0) -
+
(a.completedAt?.getTime() ?? 0),
+
if (completedTakes.length === 0) {
+
text: "You haven't completed any takes sessions yet.",
+
response_type: "ephemeral",
+
// Create blocks for each completed take
+
const historyBlocks: AnyMessageBlock[] = [
+
text: `📋 Your most recent ${completedTakes.length} Takes Sessions`,
+
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);
+
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`
+
text: `*Take on ${startDate}*\n${description}• Duration: ${activeDuration} minutes${
+
? ` (+ ${Math.round(pausedMs / 60000)} minutes paused)`
+
}\n• Started: ${startDate}\n• Completed: ${endDate}${notes}`,
+
// Add a divider between entries
+
if (take !== completedTakes[completedTakes.length - 1]) {
+
text: "🎬 Start New Session",
+
action_id: "takes_start",
+
action_id: "takes_status",
+
action_id: "takes_history",
+
text: `Your recent takes history (${completedTakes.length} sessions)`,
+
response_type: "ephemeral",
const handleHelp = async (): Promise<MessageResponse> => {
+
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",
···
+
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`,
···
action_id: "takes_start",
+
action_id: "takes_history",
+
const getDescriptionBlocks = (error?: string): MessageResponse => {
+
const blocks: AnyMessageBlock[] = [
+
block_id: "note_block",
+
type: "plain_text_input",
+
action_id: "note_input",
+
text: "Enter a note for your session",
+
text: "🎬 Start Session",
+
action_id: "takes_start",
+
action_id: "takes_status",
+
text: "Please enter a note for your session:",
+
response_type: "ephemeral",
+
const getEditDescriptionBlocks = (
+
): MessageResponse => {
+
const blocks: AnyMessageBlock[] = [
+
block_id: "note_block",
+
type: "plain_text_input",
+
action_id: "note_input",
+
text: "Enter a note for your session",
+
initial_value: description,
+
text: "✍️ Update Note",
+
action_id: "takes_edit",
+
action_id: "takes_status",
+
text: "Please enter a note for your session:",
+
response_type: "ephemeral",
slackApp.command("/takes", async ({ payload, context }): Promise<void> => {
···
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
···
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
···
response = await handleResume(userId);
+
response = await handleStop(userId, args);
+
response = getEditDescriptionBlocks(
+
activeTake[0]?.description || "",
response = await handleStatus(userId);
+
response = await handleHistory(userId);
+
response = await handleHelp();
response = await handleHelp();
···
+
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
+
if (activeTake.length > 0) {
+
response = await handleStatus(userId);
+
if (!descriptionInput?.value?.trim()) {
+
response = getDescriptionBlocks(
+
"Please enter a note for your session.",
+
response = await handleStart(
+
descriptionInput?.value?.trim(),
response = await handlePause(userId);
···
response = await handleStop(userId);
+
if (!activeTake.length && context.respond) {
+
await context.respond({
+
text: "You don't have an active takes session to edit!",
+
response_type: "ephemeral",
+
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);
+
response = getEditDescriptionBlocks(
+
"Please enter a note for your session.",
response = await handleStatus(userId);
+
response = await handleHistory(userId);
response = await handleHelp();
+
if (response && context.respond) {
+
await context.respond(response);
+
// Setup scheduled tasks
+
const notificationInterval = TakesConfig.NOTIFICATIONS.CHECK_INTERVAL;
+
setInterval(async () => {
+
await checkActiveSessions();
+
await expirePausedSessions();
+
}, notificationInterval);