alpha
Login
or
Join now
dunkirk.sh
/
smokie
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: move to periods time system
author
Kieran Klukas
date
1 year ago
(Apr 2, 2025, 10:59 PM -0400)
commit
8d2d6475
8d2d647514331c481e47186bb0a17d40bafe5a62
parent
e4347ff7
e4347ff71c8b1b589c9516e15e2f85ea1878a56e
+325
-180
13 changed files
Expand all
Collapse all
Unified
Split
src
features
api
routes
video.ts
takes
handlers
history.ts
pause.ts
resume.ts
start.ts
status.ts
stop.ts
services
database.ts
notifications.ts
upload.ts
types.ts
libs
schema.ts
time-periods.ts
+7
src/features/api/routes/video.ts
Reviewed
···
4
4
5
5
export default async function getVideo(url: URL): Promise<Response> {
6
6
const videoId = url.pathname.split("/")[2];
7
7
+
const thumbnail = url.pathname.split("/")[3] === "thumbnail";
7
8
8
9
if (!videoId) {
9
10
return new Response("Invalid video id", { status: 400 });
···
19
20
}
20
21
21
22
const videoData = video[0];
23
23
+
24
24
+
if (thumbnail) {
25
25
+
return Response.redirect(
26
26
+
`https://cachet.dunkirk.sh/users/${videoData?.userId}/r`,
27
27
+
);
28
28
+
}
22
29
23
30
return new Response(
24
31
`<!DOCTYPE html>
+5
-17
src/features/takes/handlers/history.ts
Reviewed
···
2
2
import TakesConfig from "../../../libs/config";
3
3
import { getCompletedTakes } from "../services/database";
4
4
import type { MessageResponse } from "../types";
5
5
+
import { calculateElapsedTime } from "../../../libs/time-periods";
6
6
+
import { prettyPrintTime } from "../../../libs/time";
5
7
6
8
export async function handleHistory(userId: string): Promise<MessageResponse> {
7
9
// Get completed takes for the user
···
25
27
type: "header",
26
28
text: {
27
29
type: "plain_text",
28
28
-
text: `📋 Your most recent ${completedTakes.length} Takes Sessions`,
30
30
+
text: `📋 Your most recent ${completedTakes.length} Takes sessions`,
29
31
emoji: true,
30
32
},
31
33
},
32
34
];
33
35
34
36
for (const take of completedTakes) {
35
35
-
const startTime = new Date(take.startedAt);
36
36
-
const endTime = take.completedAt || startTime;
37
37
-
38
38
-
// Calculate duration in minutes
39
39
-
const durationMs = endTime.getTime() - startTime.getTime();
40
40
-
const pausedMs = take.pausedTimeMs || 0;
41
41
-
const activeDuration = Math.round((durationMs - pausedMs) / 60000);
42
42
-
43
43
-
// Format dates
44
44
-
const startDate = `<!date^${Math.floor(startTime.getTime() / 1000)}^{date_short_pretty} at {time}|${startTime.toLocaleString()}>`;
45
45
-
const endDate = `<!date^${Math.floor(endTime.getTime() / 1000)}^{date_short_pretty} at {time}|${endTime.toLocaleString()}>`;
37
37
+
const elapsedTime = calculateElapsedTime(JSON.parse(take.periods));
46
38
47
39
const notes = take.notes ? `\n• Notes: ${take.notes}` : "";
48
40
const description = take.description
···
53
45
type: "section",
54
46
text: {
55
47
type: "mrkdwn",
56
56
-
text: `*Take on ${startDate}*\n${description}• Duration: ${activeDuration} minutes${
57
57
-
pausedMs > 0
58
58
-
? ` (+ ${Math.round(pausedMs / 60000)} minutes paused)`
59
59
-
: ""
60
60
-
}\n• Started: ${startDate}\n• Completed: ${endDate}${notes}`,
48
48
+
text: `*Duration:* \`${prettyPrintTime(elapsedTime)}\`\n*Status:* ${take.status}\n${notes ? `*Notes:* ${take.notes}\n` : ""}${description ? `*Description:* ${take.description}\n` : ""}`,
61
49
},
62
50
});
63
51
+22
-12
src/features/takes/handlers/pause.ts
Reviewed
···
4
4
import TakesConfig from "../../../libs/config";
5
5
import { getActiveTake } from "../services/database";
6
6
import type { MessageResponse } from "../types";
7
7
-
import { prettyPrintTime } from "../../../libs/time";
7
7
+
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
8
8
+
import {
9
9
+
addNewPeriod,
10
10
+
getPausedTimeRemaining,
11
11
+
} from "../../../libs/time-periods";
8
12
9
13
export default async function handlePause(
10
14
userId: string,
···
22
26
return;
23
27
}
24
28
29
29
+
const newPeriods = JSON.stringify(
30
30
+
addNewPeriod(takeToUpdate.periods, "paused"),
31
31
+
);
32
32
+
33
33
+
const pausedTime = getPausedTimeRemaining(newPeriods);
34
34
+
35
35
+
if (pausedTime > TakesConfig.MAX_PAUSE_DURATION) {
36
36
+
return {
37
37
+
text: `You can't pause for more than ${TakesConfig.MAX_PAUSE_DURATION} minutes!`,
38
38
+
response_type: "ephemeral",
39
39
+
};
40
40
+
}
41
41
+
25
42
// Update the takes entry to paused status
26
43
await db
27
44
.update(takesTable)
28
45
.set({
29
46
status: "paused",
30
30
-
pausedAt: new Date(),
47
47
+
periods: newPeriods,
31
48
notifiedPauseExpiration: false, // Reset pause expiration notification
32
49
})
33
50
.where(eq(takesTable.id, takeToUpdate.id));
34
51
35
35
-
// Calculate when the pause will expire
36
36
-
const pauseExpires = new Date();
37
37
-
pauseExpires.setMinutes(
38
38
-
pauseExpires.getMinutes() + TakesConfig.MAX_PAUSE_DURATION,
39
39
-
);
40
40
-
const pauseExpiresStr = `<!date^${Math.floor(pauseExpires.getTime() / 1000)}^{date_short_pretty} at {time}|${pauseExpires.toLocaleString()}>`;
41
41
-
42
52
return {
43
43
-
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.`,
53
53
+
text: `⏸️ Session paused! You have ${prettyPrintTime(TakesConfig.MAX_PAUSE_DURATION * 60000 - pausedTime)} remaining. It will automatically finish at ${generateSlackDate(new Date(Date.now() + TakesConfig.MAX_PAUSE_DURATION * 60000))}`,
44
54
response_type: "ephemeral",
45
55
blocks: [
46
56
{
47
57
type: "section",
48
58
text: {
49
59
type: "mrkdwn",
50
50
-
text: `⏸️ Session paused! You have ${prettyPrintTime(takeToUpdate.durationMinutes * 60000)} remaining.`,
60
60
+
text: `⏸️ Session paused! You have ${prettyPrintTime(TakesConfig.MAX_PAUSE_DURATION * 60000 - pausedTime)} remaining.`,
51
61
},
52
62
},
53
63
{
···
58
68
elements: [
59
69
{
60
70
type: "mrkdwn",
61
61
-
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
71
71
+
text: `It will automatically finish at ${generateSlackDate(new Date(Date.now() + TakesConfig.MAX_PAUSE_DURATION * 60000))} if not resumed.`,
62
72
},
63
73
],
64
74
},
+19
-25
src/features/takes/handlers/resume.ts
Reviewed
···
4
4
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
5
5
import { getPausedTake } from "../services/database";
6
6
import type { MessageResponse } from "../types";
7
7
+
import { addNewPeriod, getRemainingTime } from "../../../libs/time-periods";
7
8
8
9
export default async function handleResume(
9
10
userId: string,
···
22
23
}
23
24
24
25
const now = new Date();
26
26
+
const newPeriods = JSON.stringify(
27
27
+
addNewPeriod(pausedSession.periods, "active"),
28
28
+
);
25
29
26
26
-
// Calculate paused time
27
27
-
if (pausedSession.pausedAt) {
28
28
-
const pausedTimeMs = now.getTime() - pausedSession.pausedAt.getTime();
29
29
-
const totalPausedTime =
30
30
-
(pausedSession.pausedTimeMs || 0) + pausedTimeMs;
31
31
-
32
32
-
// Update the takes entry to active status
33
33
-
await db
34
34
-
.update(takesTable)
35
35
-
.set({
36
36
-
status: "active",
37
37
-
pausedAt: null,
38
38
-
pausedTimeMs: totalPausedTime,
39
39
-
notifiedLowTime: false, // Reset low time notification
40
40
-
})
41
41
-
.where(eq(takesTable.id, pausedSession.id));
42
42
-
}
30
30
+
// Update the takes entry to active status
31
31
+
await db
32
32
+
.update(takesTable)
33
33
+
.set({
34
34
+
status: "active",
35
35
+
lastResumeAt: now,
36
36
+
periods: newPeriods,
37
37
+
notifiedLowTime: false, // Reset low time notification
38
38
+
})
39
39
+
.where(eq(takesTable.id, pausedSession.id));
43
40
44
44
-
const endTime = new Date(
45
45
-
new Date(pausedSession.startedAt).getTime() +
46
46
-
pausedSession.durationMinutes * 60000 +
47
47
-
(pausedSession.pausedTimeMs || 0),
41
41
+
const endTime = getRemainingTime(
42
42
+
pausedSession.targetDurationMs,
43
43
+
pausedSession.periods,
48
44
);
49
45
50
50
-
const timeRemaining = endTime.getTime() - now.getTime();
51
51
-
52
46
return {
53
53
-
text: `▶️ Takes session resumed! You have ${prettyPrintTime(timeRemaining)} remaining in your session.`,
47
47
+
text: `▶️ Takes session resumed! You have ${prettyPrintTime(endTime.remaining)} remaining in your session.`,
54
48
response_type: "ephemeral",
55
49
blocks: [
56
50
{
···
68
62
elements: [
69
63
{
70
64
type: "mrkdwn",
71
71
-
text: `You have ${prettyPrintTime(timeRemaining)} remaining until ${generateSlackDate(endTime)}.`,
65
65
+
text: `You have ${prettyPrintTime(endTime.remaining)} remaining until ${generateSlackDate(endTime.endTime)}.`,
72
66
},
73
67
],
74
68
},
+15
-8
src/features/takes/handlers/start.ts
Reviewed
···
4
4
import { takes as takesTable } from "../../../libs/schema";
5
5
import TakesConfig from "../../../libs/config";
6
6
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
7
7
+
import { getRemainingTime } from "../../../libs/time-periods";
7
8
8
9
export default async function handleStart(
9
10
userId: string,
10
11
channelId: string,
11
12
description?: string,
12
12
-
durationMinutes?: number,
13
13
): Promise<MessageResponse> {
14
14
const activeTake = await getActiveTake(userId);
15
15
if (activeTake.length > 0) {
···
23
23
const newTake = {
24
24
id: Bun.randomUUIDv7(),
25
25
userId,
26
26
-
channelId,
27
26
status: "active",
28
28
-
startedAt: new Date(),
29
29
-
durationMinutes: durationMinutes || TakesConfig.DEFAULT_SESSION_LENGTH,
27
27
+
targetDurationMs: TakesConfig.DEFAULT_SESSION_LENGTH * 60000,
28
28
+
periods: JSON.stringify([
29
29
+
{
30
30
+
type: "active",
31
31
+
startTime: Date.now(),
32
32
+
endTime: null,
33
33
+
},
34
34
+
]),
35
35
+
elapsedTimeMs: 0,
30
36
description: description || null,
31
37
notifiedLowTime: false,
32
38
notifiedPauseExpiration: false,
···
35
41
await db.insert(takesTable).values(newTake);
36
42
37
43
// Calculate end time for message
38
38
-
const endTime = new Date(
39
39
-
newTake.startedAt.getTime() + newTake.durationMinutes * 60000,
44
44
+
const endTime = getRemainingTime(
45
45
+
TakesConfig.DEFAULT_SESSION_LENGTH * 60000,
46
46
+
newTake.periods,
40
47
);
41
48
42
49
const descriptionText = description
43
50
? `\n\n*Working on:* ${description}`
44
51
: "";
45
52
return {
46
46
-
text: `🎬 Takes session started! You have ${prettyPrintTime(newTake.durationMinutes * 60000)} until ${generateSlackDate(endTime)}.${descriptionText}`,
53
53
+
text: `🎬 Takes session started! You have ${prettyPrintTime(endTime.remaining)} until ${generateSlackDate(endTime.endTime)}.${descriptionText}`,
47
54
response_type: "ephemeral",
48
55
blocks: [
49
56
{
···
61
68
elements: [
62
69
{
63
70
type: "mrkdwn",
64
64
-
text: `You have ${prettyPrintTime(newTake.durationMinutes * 60000)} left until ${generateSlackDate(endTime)}.`,
71
71
+
text: `You have ${prettyPrintTime(endTime.remaining)} left until ${generateSlackDate(endTime.endTime)}.`,
65
72
},
66
73
],
67
74
},
+17
-34
src/features/takes/handlers/status.ts
Reviewed
···
1
1
import TakesConfig from "../../../libs/config";
2
2
import { generateSlackDate, prettyPrintTime } from "../../../libs/time";
3
3
import {
4
4
+
getPausedTimeRemaining,
5
5
+
getRemainingTime,
6
6
+
} from "../../../libs/time-periods";
7
7
+
import {
4
8
getActiveTake,
5
9
getCompletedTakes,
6
10
getPausedTake,
···
22
26
return;
23
27
}
24
28
25
25
-
const startTime = new Date(take.startedAt);
26
26
-
const endTime = new Date(
27
27
-
startTime.getTime() + take.durationMinutes * 60000,
28
28
-
);
29
29
-
30
30
-
// Adjust for paused time
31
31
-
if (take.pausedTimeMs) {
32
32
-
endTime.setTime(endTime.getTime() + take.pausedTimeMs);
33
33
-
}
34
34
-
35
35
-
const now = new Date();
36
36
-
const remainingMs = endTime.getTime() - now.getTime();
29
29
+
const endTime = getRemainingTime(take.targetDurationMs, take.periods);
37
30
38
31
// Add description to display if present
39
32
const descriptionText = take.description
···
41
34
: "";
42
35
43
36
return {
44
44
-
text: `🎬 You have an active takes session with ${prettyPrintTime(remainingMs)} remaining.${descriptionText}`,
37
37
+
text: `🎬 You have an active takes session with ${prettyPrintTime(endTime.remaining)} remaining.${descriptionText}`,
45
38
response_type: "ephemeral",
46
39
blocks: [
47
40
{
···
59
52
elements: [
60
53
{
61
54
type: "mrkdwn",
62
62
-
text: `You have ${prettyPrintTime(remainingMs)} remaining until ${generateSlackDate(endTime)}.`,
55
55
+
text: `You have ${prettyPrintTime(endTime.remaining)} remaining until ${generateSlackDate(endTime.endTime)}.`,
63
56
},
64
57
],
65
58
},
···
119
112
120
113
if (pausedTakeStatus.length > 0) {
121
114
const pausedTake = pausedTakeStatus[0];
122
122
-
if (!pausedTake || !pausedTake.pausedAt) {
115
115
+
if (!pausedTake) {
123
116
return;
124
117
}
125
118
126
119
// Calculate how much time remains before auto-completion
127
127
-
const now = new Date();
128
128
-
const pausedDuration =
129
129
-
(now.getTime() - pausedTake.pausedAt.getTime()) / (60 * 1000); // In minutes
130
130
-
const remainingPauseTime = Math.max(
131
131
-
0,
132
132
-
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
120
120
+
const endTime = getRemainingTime(
121
121
+
pausedTake.targetDurationMs,
122
122
+
pausedTake.periods,
133
123
);
134
134
-
135
135
-
// Format the pause timeout
136
136
-
const pauseExpires = new Date(pausedTake.pausedAt);
137
137
-
pauseExpires.setMinutes(
138
138
-
pauseExpires.getMinutes() + TakesConfig.MAX_PAUSE_DURATION,
139
139
-
);
140
140
-
const pauseExpiresStr = `<!date^${Math.floor(pauseExpires.getTime() / 1000)}^{date_short_pretty} at {time}|${pauseExpires.toLocaleString()}>`;
124
124
+
const pauseExpires = getPausedTimeRemaining(pausedTake.periods);
141
125
142
126
// Add notes to display if present
143
127
const noteText = pausedTake.notes
···
145
129
: "";
146
130
147
131
return {
148
148
-
text: `⏸️ You have a paused takes session. It will auto-complete in ${remainingPauseTime.toFixed(1)} minutes if not resumed.`,
132
132
+
text: `⏸️ You have a paused takes session. It will auto-complete in ${prettyPrintTime(pauseExpires)} if not resumed.`,
149
133
response_type: "ephemeral",
150
134
blocks: [
151
135
{
152
136
type: "section",
153
137
text: {
154
138
type: "mrkdwn",
155
155
-
text: `⏸️ Session paused! You have ${prettyPrintTime(pausedTake.durationMinutes * 60000)} remaining.`,
139
139
+
text: `⏸️ Session paused! You have ${prettyPrintTime(endTime.remaining)} remaining.`,
156
140
},
157
141
},
158
142
{
···
163
147
elements: [
164
148
{
165
149
type: "mrkdwn",
166
166
-
text: `It will automatically finish in ${TakesConfig.MAX_PAUSE_DURATION} minutes (by ${pauseExpiresStr}) if not resumed.`,
150
150
+
text: `It will automatically finish in ${prettyPrintTime(pauseExpires)} (by ${generateSlackDate(new Date(new Date().getTime() - pauseExpires))}) if not resumed.`,
167
151
},
168
152
],
169
153
},
···
214
198
const diffMs =
215
199
new Date().getTime() -
216
200
// @ts-expect-error - TS doesn't know that we are checking the length
217
217
-
completedSessions[
218
218
-
completedSessions.length - 1
219
219
-
].startedAt.getTime();
201
201
+
completedSessions[completedSessions.length - 1]
202
202
+
?.completedAt;
220
203
221
204
const hours = Math.ceil(diffMs / (1000 * 60 * 60));
222
205
if (hours < 24) return `${hours} hours`;
+55
src/features/takes/handlers/stop.ts
Reviewed
···
4
4
import { eq } from "drizzle-orm";
5
5
import { getActiveTake, getPausedTake } from "../services/database";
6
6
import type { MessageResponse } from "../types";
7
7
+
import { prettyPrintTime } from "../../../libs/time";
8
8
+
import {
9
9
+
calculateElapsedTime,
10
10
+
getRemainingTime,
11
11
+
} from "../../../libs/time-periods";
7
12
8
13
export default async function handleStop(
9
14
userId: string,
···
33
38
notes = args.slice(1).join(" ");
34
39
}
35
40
41
41
+
const elapsed = calculateElapsedTime(
42
42
+
JSON.parse(pausedTakeToStop.periods),
43
43
+
);
44
44
+
36
45
const res = await slackClient.chat.postMessage({
37
46
channel: userId,
38
47
text: "🎬 Your paused takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
48
48
+
blocks: [
49
49
+
{
50
50
+
type: "section",
51
51
+
text: {
52
52
+
type: "mrkdwn",
53
53
+
text: "🎬 Your paused takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
54
54
+
},
55
55
+
},
56
56
+
{
57
57
+
type: "divider",
58
58
+
},
59
59
+
{
60
60
+
type: "context",
61
61
+
elements: [
62
62
+
{
63
63
+
type: "mrkdwn",
64
64
+
text: `*Duration:* ${prettyPrintTime(elapsed)} ${notes ? `\n*Notes:* ${notes}` : ""}`,
65
65
+
},
66
66
+
],
67
67
+
},
68
68
+
],
39
69
});
40
70
41
71
await db
···
60
90
notes = args.slice(1).join(" ");
61
91
}
62
92
93
93
+
const elapsed = calculateElapsedTime(
94
94
+
JSON.parse(activeTakeToStop.periods),
95
95
+
);
96
96
+
63
97
const res = await slackClient.chat.postMessage({
64
98
channel: userId,
65
99
text: "🎬 Your takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
100
100
+
blocks: [
101
101
+
{
102
102
+
type: "section",
103
103
+
text: {
104
104
+
type: "mrkdwn",
105
105
+
text: "🎬 Your takes session has been completed. Please upload your takes video in this thread within the next 24 hours!",
106
106
+
},
107
107
+
},
108
108
+
{
109
109
+
type: "divider",
110
110
+
},
111
111
+
{
112
112
+
type: "context",
113
113
+
elements: [
114
114
+
{
115
115
+
type: "mrkdwn",
116
116
+
text: `*Duration:* ${prettyPrintTime(elapsed)} ${notes ? `\n*Notes:* ${notes}` : ""}`,
117
117
+
},
118
118
+
],
119
119
+
},
120
120
+
],
66
121
});
67
122
68
123
await db
+5
-2
src/features/takes/services/database.ts
Reviewed
···
1
1
import { db } from "../../../libs/db";
2
2
import { takes as takesTable } from "../../../libs/schema";
3
3
-
import { eq, and, desc } from "drizzle-orm";
3
3
+
import { eq, and, desc, not } from "drizzle-orm";
4
4
5
5
export async function getActiveTake(userId: string) {
6
6
return db
···
29
29
.where(
30
30
and(
31
31
eq(takesTable.userId, userId),
32
32
-
eq(takesTable.status, "completed"),
32
32
+
and(
33
33
+
not(eq(takesTable.status, "active")),
34
34
+
not(eq(takesTable.status, "paused")),
35
35
+
),
33
36
),
34
37
)
35
38
.orderBy(desc(takesTable.completedAt))
+64
-68
src/features/takes/services/notifications.ts
Reviewed
···
3
3
import { db } from "../../../libs/db";
4
4
import { takes as takesTable } from "../../../libs/schema";
5
5
import { eq } from "drizzle-orm";
6
6
+
import {
7
7
+
getPausedDuration,
8
8
+
getRemainingTime,
9
9
+
} from "../../../libs/time-periods";
6
10
7
11
// Check for paused sessions that have exceeded the max pause duration
8
12
export async function expirePausedSessions() {
···
13
17
.where(eq(takesTable.status, "paused"));
14
18
15
19
for (const take of pausedTakes) {
16
16
-
if (take.pausedAt) {
17
17
-
const pausedDuration =
18
18
-
(now.getTime() - take.pausedAt.getTime()) / (60 * 1000); // Convert to minutes
20
20
+
const pausedDuration = getPausedDuration(take.periods) / 60000; // Convert to minutes
19
21
20
20
-
// Send warning notification when getting close to expiration
21
21
-
if (
22
22
-
pausedDuration >
23
23
-
TakesConfig.MAX_PAUSE_DURATION -
24
24
-
TakesConfig.NOTIFICATIONS.PAUSE_EXPIRATION_WARNING &&
25
25
-
!take.notifiedPauseExpiration
26
26
-
) {
27
27
-
// Update notification flag
28
28
-
await db
29
29
-
.update(takesTable)
30
30
-
.set({
31
31
-
notifiedPauseExpiration: true,
32
32
-
})
33
33
-
.where(eq(takesTable.id, take.id));
22
22
+
// Send warning notification when getting close to expiration
23
23
+
if (
24
24
+
pausedDuration >
25
25
+
TakesConfig.MAX_PAUSE_DURATION -
26
26
+
TakesConfig.NOTIFICATIONS.PAUSE_EXPIRATION_WARNING &&
27
27
+
!take.notifiedPauseExpiration
28
28
+
) {
29
29
+
// Update notification flag
30
30
+
await db
31
31
+
.update(takesTable)
32
32
+
.set({
33
33
+
notifiedPauseExpiration: true,
34
34
+
})
35
35
+
.where(eq(takesTable.id, take.id));
34
36
35
35
-
// Send warning message
36
36
-
try {
37
37
-
const timeRemaining = Math.round(
38
38
-
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
39
39
-
);
40
40
-
await slackApp.client.chat.postMessage({
41
41
-
channel: take.userId,
42
42
-
text: `⚠️ Reminder: Your paused takes session will automatically complete in about ${timeRemaining} minutes if not resumed.`,
43
43
-
});
44
44
-
} catch (error) {
45
45
-
console.error(
46
46
-
"Failed to send pause expiration warning:",
47
47
-
error,
48
48
-
);
49
49
-
}
37
37
+
// Send warning message
38
38
+
try {
39
39
+
const timeRemaining = Math.round(
40
40
+
TakesConfig.MAX_PAUSE_DURATION - pausedDuration,
41
41
+
);
42
42
+
await slackApp.client.chat.postMessage({
43
43
+
channel: take.userId,
44
44
+
text: `⚠️ Reminder: Your paused takes session will automatically complete in about ${timeRemaining} minutes if not resumed.`,
45
45
+
});
46
46
+
} catch (error) {
47
47
+
console.error(
48
48
+
"Failed to send pause expiration warning:",
49
49
+
error,
50
50
+
);
50
51
}
51
51
-
52
52
-
// Auto-expire paused sessions that exceed the max pause duration
53
53
-
if (pausedDuration > TakesConfig.MAX_PAUSE_DURATION) {
54
54
-
let ts: string | undefined;
55
55
-
// Notify user that their session was auto-completed
56
56
-
try {
57
57
-
const res = await slackApp.client.chat.postMessage({
58
58
-
channel: take.userId,
59
59
-
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!`,
60
60
-
});
61
61
-
ts = res.ts;
62
62
-
} catch (error) {
63
63
-
console.error(
64
64
-
"Failed to notify user of auto-completed session:",
65
65
-
error,
66
66
-
);
67
67
-
}
52
52
+
}
68
53
69
69
-
await db
70
70
-
.update(takesTable)
71
71
-
.set({
72
72
-
status: "waitingUpload",
73
73
-
completedAt: now,
74
74
-
ts,
75
75
-
notes: take.notes
76
76
-
? `${take.notes} (Automatically completed due to pause timeout)`
77
77
-
: "Automatically completed due to pause timeout",
78
78
-
})
79
79
-
.where(eq(takesTable.id, take.id));
54
54
+
// Auto-expire paused sessions that exceed the max pause duration
55
55
+
if (pausedDuration > TakesConfig.MAX_PAUSE_DURATION) {
56
56
+
let ts: string | undefined;
57
57
+
// Notify user that their session was auto-completed
58
58
+
try {
59
59
+
const res = await slackApp.client.chat.postMessage({
60
60
+
channel: take.userId,
61
61
+
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!`,
62
62
+
});
63
63
+
ts = res.ts;
64
64
+
} catch (error) {
65
65
+
console.error(
66
66
+
"Failed to notify user of auto-completed session:",
67
67
+
error,
68
68
+
);
80
69
}
70
70
+
71
71
+
await db
72
72
+
.update(takesTable)
73
73
+
.set({
74
74
+
status: "waitingUpload",
75
75
+
completedAt: now,
76
76
+
ts,
77
77
+
notes: take.notes
78
78
+
? `${take.notes} (Automatically completed due to pause timeout)`
79
79
+
: "Automatically completed due to pause timeout",
80
80
+
})
81
81
+
.where(eq(takesTable.id, take.id));
81
82
}
82
83
}
83
84
}
···
91
92
.where(eq(takesTable.status, "active"));
92
93
93
94
for (const take of activeTakes) {
94
94
-
const endTime = new Date(
95
95
-
take.startedAt.getTime() +
96
96
-
take.durationMinutes * 60000 +
97
97
-
(take.pausedTimeMs || 0),
98
98
-
);
95
95
+
const endTime = getRemainingTime(take.targetDurationMs, take.periods);
99
96
100
100
-
const remainingMs = endTime.getTime() - now.getTime();
101
101
-
const remainingMinutes = remainingMs / 60000;
97
97
+
const remainingMinutes = endTime.remaining / 60000;
102
98
103
99
if (
104
100
remainingMinutes <= TakesConfig.NOTIFICATIONS.LOW_TIME_WARNING &&
···
122
118
}
123
119
}
124
120
125
125
-
if (remainingMs <= 0) {
121
121
+
if (endTime.remaining <= 0) {
126
122
let ts: string | undefined;
127
123
try {
128
124
const res = await slackApp.client.chat.postMessage({
+17
-9
src/features/takes/services/upload.ts
Reviewed
···
3
3
import { takes as takesTable } from "../../../libs/schema";
4
4
import { eq, and } from "drizzle-orm";
5
5
import { prettyPrintTime } from "../../../libs/time";
6
6
+
import { calculateElapsedTime } from "../../../libs/time-periods";
6
7
7
8
export default async function upload() {
8
9
slackApp.anyMessage(async ({ payload }) => {
···
58
59
const match = html.match(/src="([^"]*\.mp4[^"]*)"/);
59
60
const takePublicUrl = match?.[1];
60
61
62
62
+
const takeUploadedAt = new Date();
63
63
+
61
64
await db
62
65
.update(takesTable)
63
66
.set({
64
67
status: "uploaded",
65
65
-
takeUploadedAt: new Date(),
68
68
+
takeUploadedAt,
66
69
takeUrl: takePublicUrl,
67
67
-
takeThumbUrl: file?.thumb_video,
68
70
})
69
71
.where(eq(takesTable.id, take.id));
70
72
···
74
76
name: "fire",
75
77
});
76
78
79
79
+
const takeDuration = calculateElapsedTime(JSON.parse(take.periods));
80
80
+
77
81
await slackClient.chat.postMessage({
78
82
channel: payload.channel,
79
83
thread_ts: payload.thread_ts,
···
94
98
elements: [
95
99
{
96
100
type: "mrkdwn",
97
97
-
text: `take by <@${user}> for \`${prettyPrintTime(take.durationMinutes * 60000)}\` working on: *${take.description}*`,
101
101
+
text: `take by <@${user}> for \`${prettyPrintTime(takeDuration)}\` working on: *${take.description}*`,
98
102
},
99
103
],
100
104
},
···
103
107
104
108
await slackClient.chat.postMessage({
105
109
channel: process.env.SLACK_REVIEW_CHANNEL || "",
106
106
-
text: "",
110
110
+
text: ":video_camera: new take uploaded!",
107
111
blocks: [
108
112
{
109
113
type: "section",
110
114
text: {
111
115
type: "mrkdwn",
112
112
-
text: `:video_camera: new take uploaded by <@${user}> for \`${prettyPrintTime(take.durationMinutes * 60000)}\` working on: *${take.description}*`,
116
116
+
text: `:video_camera: new take uploaded by <@${user}> for \`${prettyPrintTime(takeDuration)}\` working on: *${take.description}*`,
113
117
},
114
118
},
115
119
{
···
121
125
title_url: `${process.env.API_URL}/video/${take.id}`,
122
126
title: {
123
127
type: "plain_text",
124
124
-
text: `take on ${take.takeUploadedAt?.toISOString()}`,
128
128
+
text: `takes from ${takeUploadedAt?.toISOString()}`,
125
129
},
126
130
thumbnail_url: `https://cachet.dunkirk.sh/users/${payload.user}/r`,
127
127
-
alt_text: `take on ${take.takeUploadedAt?.toISOString()}`,
131
131
+
alt_text: `takes from ${takeUploadedAt?.toISOString()}`,
128
132
},
129
133
{
130
134
type: "divider",
···
214
218
elements: [
215
219
{
216
220
type: "mrkdwn",
217
217
-
text: `take by <@${user}> for \`${prettyPrintTime(take.durationMinutes * 60000)}\` working on: *${take.description}*`,
221
221
+
text: `take by <@${user}> for \`${prettyPrintTime(takeDuration)}\` working on: *${take.description}*`,
218
222
},
219
223
],
220
224
},
···
248
252
})
249
253
.where(eq(takesTable.id, takeId));
250
254
255
255
+
const takeDuration = calculateElapsedTime(
256
256
+
JSON.parse(take[0]?.periods as string),
257
257
+
);
258
258
+
251
259
await slackClient.chat.postMessage({
252
260
channel: payload.user.id,
253
261
thread_ts: take[0]?.ts as string,
254
254
-
text: `take approved with multiplier \`${multiplier}\` so you have earned *${Number(((take[0]?.durationMinutes as number) * Number(multiplier)) / 60).toFixed(1)} takes*!`,
262
262
+
text: `take approved with multiplier \`${multiplier}\` so you have earned *${Number((takeDuration * Number(multiplier)) / 60).toFixed(1)} takes*!`,
255
263
});
256
264
257
265
// delete the message from the review channel
+20
src/features/takes/types.ts
Reviewed
···
5
5
text: string;
6
6
response_type: "ephemeral" | "in_channel";
7
7
};
8
8
+
9
9
+
export type PeriodType = "active" | "paused";
10
10
+
11
11
+
export interface TimePeriod {
12
12
+
type: PeriodType;
13
13
+
startTime: number; // timestamp
14
14
+
endTime: number | null; // null means ongoing
15
15
+
}
16
16
+
17
17
+
export interface TakeTimeTracking {
18
18
+
periods: TimePeriod[];
19
19
+
elapsedTimeMs: number;
20
20
+
targetDurationMs: number;
21
21
+
}
22
22
+
23
23
+
export interface TakeTimeTrackingString {
24
24
+
periods: string;
25
25
+
elapsedTimeMs: number;
26
26
+
targetDurationMs: number;
27
27
+
}
+4
-5
src/libs/schema.ts
Reviewed
···
6
6
userId: text("user_id").notNull(),
7
7
ts: text("ts"),
8
8
status: text("status").notNull().default("active"), // active, paused, waitingUpload, completed
9
9
-
startedAt: integer("started_at", { mode: "timestamp" }).notNull(),
10
10
-
pausedAt: integer("paused_at", { mode: "timestamp" }),
9
9
+
elapsedTimeMs: integer("elapsed_time_ms").notNull().default(0),
10
10
+
targetDurationMs: integer("target_duration_ms").notNull(),
11
11
+
periods: text("periods").notNull(), // JSON string of time periods
12
12
+
lastResumeAt: integer("last_resume_at", { mode: "timestamp" }),
11
13
completedAt: integer("completed_at", { mode: "timestamp" }),
12
14
takeUploadedAt: integer("take_uploaded_at", { mode: "timestamp" }),
13
15
takeUrl: text("take_url"),
14
14
-
takeThumbUrl: text("take_thumb_url"),
15
16
multiplier: text("multiplier").notNull().default("1.0"),
16
16
-
durationMinutes: integer("duration_minutes").notNull().default(5), // 5 minutes for testing (should be 90)
17
17
-
pausedTimeMs: integer("paused_time_ms").notNull().default(0), // cumulative paused time
18
17
notes: text("notes"),
19
18
description: text("description"),
20
19
notifiedLowTime: integer("notified_low_time", { mode: "boolean" }).default(
+75
src/libs/time-periods.ts
Reviewed
···
1
1
+
import type { PeriodType, TimePeriod } from "../features/takes/types";
2
2
+
import TakesConfig from "./config";
3
3
+
4
4
+
export function calculateElapsedTime(periods: TimePeriod[]): number {
5
5
+
return periods.reduce((total, period) => {
6
6
+
if (period.type !== "active") return total;
7
7
+
8
8
+
const endTime = period.endTime || Date.now();
9
9
+
return total + (endTime - period.startTime);
10
10
+
}, 0);
11
11
+
}
12
12
+
13
13
+
export function addNewPeriod(
14
14
+
periodsString: string,
15
15
+
type: PeriodType,
16
16
+
): TimePeriod[] {
17
17
+
const periods = JSON.parse(periodsString);
18
18
+
19
19
+
// Close previous period if exists
20
20
+
if (periods.length > 0) {
21
21
+
const lastPeriod = periods[periods.length - 1];
22
22
+
if (!lastPeriod.endTime) {
23
23
+
lastPeriod.endTime = Date.now();
24
24
+
}
25
25
+
}
26
26
+
27
27
+
// Add new period
28
28
+
periods.push({
29
29
+
type,
30
30
+
startTime: Date.now(),
31
31
+
endTime: null,
32
32
+
});
33
33
+
34
34
+
return periods;
35
35
+
}
36
36
+
37
37
+
export function getRemainingTime(
38
38
+
targetDurationMs: number,
39
39
+
periods: string,
40
40
+
): {
41
41
+
remaining: number;
42
42
+
endTime: Date;
43
43
+
} {
44
44
+
const elapsedMs = calculateElapsedTime(JSON.parse(periods));
45
45
+
const remaining = Math.max(0, targetDurationMs - elapsedMs);
46
46
+
const endTime = new Date(Date.now() + remaining);
47
47
+
return { remaining, endTime };
48
48
+
}
49
49
+
50
50
+
export function getPausedTimeRemaining(periods: string): number {
51
51
+
const parsedPeriods = JSON.parse(periods);
52
52
+
const currentPeriod = parsedPeriods[parsedPeriods.length - 1];
53
53
+
54
54
+
if (currentPeriod.type !== "paused" || !currentPeriod.startTime) {
55
55
+
return 0;
56
56
+
}
57
57
+
58
58
+
const now = new Date();
59
59
+
const pausedDuration = now.getTime() - currentPeriod.startTime;
60
60
+
61
61
+
return Math.max(
62
62
+
0,
63
63
+
TakesConfig.MAX_PAUSE_DURATION * 60 * 1000 - pausedDuration,
64
64
+
);
65
65
+
}
66
66
+
67
67
+
export function getPausedDuration(periods: string): number {
68
68
+
const parsedPeriods = JSON.parse(periods);
69
69
+
return parsedPeriods.reduce((total: number, period: TimePeriod) => {
70
70
+
if (period.type !== "paused") return total;
71
71
+
72
72
+
const endTime = period.endTime || Date.now();
73
73
+
return total + (endTime - period.startTime);
74
74
+
}, 0);
75
75
+
}