···2525 await slackClient.chat.postMessage({
2626 channel: payload.channel,
2727 thread_ts: payload.ts,
2828- text: "we don't have a project for you; set one up in the web ui or by running `/takes`",
2828+ text: "We don't have a project for you; set one up by clicking the button below or by running `/takes`",
2929+ blocks: [
3030+ {
3131+ type: "section",
3232+ text: {
3333+ type: "mrkdwn",
3434+ text: "We don't have a project for you; set one up by clicking the button below or by running `/takes`",
3535+ },
3636+ },
3737+ {
3838+ type: "actions",
3939+ elements: [
4040+ {
4141+ type: "button",
4242+ text: {
4343+ type: "plain_text",
4444+ text: "setup your project",
4545+ },
4646+ action_id: "takes_setup",
4747+ },
4848+ ],
4949+ },
5050+ {
5151+ type: "context",
5252+ elements: [
5353+ {
5454+ type: "plain_text",
5555+ text: "don't forget to resend your update after setting up your project!",
5656+ },
5757+ ],
5858+ },
5959+ ],
2960 });
3061 return;
3162 }
···100131 const timeSpentMs = 60000;
101132102133 await db.insert(takesTable).values({
103103- id: payload.ts,
134134+ id: Bun.randomUUIDv7(),
104135 userId: user,
105136 ts: payload.ts,
106137 notes: markdownText,
···123154 type: "section",
124155 text: {
125156 type: "mrkdwn",
126126- text: `:inbox_tray: saved! ${mediaUrls.length > 0 ? "uploaded media and " : ""}saved your notes`,
157157+ text: `:inbox_tray: ${mediaUrls.length > 0 ? "uploaded media and " : ""}saved your notes!`,
127158 },
128159 },
129160 ],
+68-49
src/features/takes/setup/actions.ts
···33import handleHelp from "../handlers/help";
44import { handleHistory } from "../handlers/history";
55import handleHome from "../handlers/home";
66-import { setupSubmitListener } from "../handlers/setup";
77-import upload from "../services/upload";
66+import { handleSettings, setupSubmitListener } from "../handlers/settings";
77+import upload from "../handlers/upload";
88import type { MessageResponse } from "../types";
99import * as Sentry from "@sentry/bun";
10101111export default function setupActions() {
1212 // Handle button actions
1313- slackApp.action(/^takes_(\w+)$/, async ({ payload, context }) => {
1414- try {
1515- const userId = payload.user.id;
1616- const actionId = payload.actions[0]?.action_id as string;
1717- const command = actionId.replace("takes_", "");
1313+ slackApp.action(
1414+ /^takes_(\w+)$/,
1515+ async () => Promise.resolve(),
1616+ async ({ payload, context }) => {
1717+ try {
1818+ const userId = payload.user.id;
1919+ const actionId = payload.actions[0]?.action_id as string;
2020+ const command = actionId.replace("takes_", "");
18211919- let response: MessageResponse | undefined;
2222+ let response: MessageResponse | undefined;
20232121- // Route to the appropriate handler function
2222- switch (command) {
2323- case "history":
2424- response = await handleHistory(userId);
2525- break;
2626- case "help":
2727- response = await handleHelp();
2828- break;
2929- case "home":
3030- response = await handleHome(userId);
3131- break;
3232- default:
3333- response = await handleHome(userId);
3434- break;
3535- }
2424+ // Route to the appropriate handler function
2525+ switch (command) {
2626+ case "history":
2727+ response = await handleHistory(userId);
2828+ break;
2929+ case "help":
3030+ response = await handleHelp();
3131+ break;
3232+ case "home":
3333+ response = await handleHome(userId);
3434+ break;
3535+ case "settings":
3636+ await handleSettings(
3737+ context.triggerId as string,
3838+ userId,
3939+ true,
4040+ );
4141+ if (context.respond)
4242+ await context.respond({ delete_original: true });
4343+ return;
4444+ case "setup":
4545+ await handleSettings(
4646+ context.triggerId as string,
4747+ userId,
4848+ );
4949+ return;
5050+ default:
5151+ response = await handleHome(userId);
5252+ break;
5353+ }
36543737- // Send the response
3838- if (response && context.respond) {
3939- await context.respond(response);
4040- }
4141- } catch (error) {
4242- if (error instanceof Error)
4343- blog(
4444- `Error in \`${payload.actions[0]?.action_id}\` action: ${error.message}`,
4545- "error",
4646- );
4747-4848- // Capture the error in Sentry
4949- Sentry.captureException(error, {
5050- extra: {
5151- actionId: payload.actions[0]?.action_id,
5252- userId: payload.user.id,
5353- channelId: context.channelId,
5454- },
5555- });
5555+ // Send the response
5656+ if (response && context.respond) {
5757+ await context.respond(response);
5858+ }
5959+ } catch (error) {
6060+ if (error instanceof Error)
6161+ blog(
6262+ `Error in \`${payload.actions[0]?.action_id}\` action: ${error.message}`,
6363+ "error",
6464+ );
56655757- // Respond with error message to user
5858- if (context.respond) {
5959- await context.respond({
6060- text: "An error occurred while processing your request. Please stand by while we try to put out the fire.",
6161- response_type: "ephemeral",
6666+ // Capture the error in Sentry
6767+ Sentry.captureException(error, {
6868+ extra: {
6969+ actionId: payload.actions[0]?.action_id,
7070+ userId: payload.user.id,
7171+ channelId: context.channelId,
7272+ },
6273 });
7474+7575+ // Respond with error message to user
7676+ if (context.respond) {
7777+ await context.respond({
7878+ text: "An error occurred while processing your request. Please stand by while we try to put out the fire.",
7979+ response_type: "ephemeral",
8080+ });
8181+ }
6382 }
6464- }
6565- });
8383+ },
8484+ );
66856786 // setup the upload actions
6887 try {
+10-2
src/features/takes/setup/commands.ts
···88import { db } from "../../../libs/db";
99import { users as usersTable } from "../../../libs/schema";
1010import { eq } from "drizzle-orm";
1111-import { handleSetup } from "../handlers/setup";
1111+import { handleSettings } from "../handlers/settings";
12121313export default function setupCommands() {
1414 // Main command handler
1515 slackApp.command(
1616 environment === "dev" ? "/takes-dev" : "/takes",
1717+ async () => Promise.resolve(),
1718 async ({ payload, context }): Promise<void> => {
1819 try {
1920 const userId = payload.user_id;
···3031 .where(eq(usersTable.id, userId));
31323233 if (userFromDB.length === 0) {
3333- await handleSetup(context.triggerId as string);
3434+ await handleSettings(context.triggerId as string, userId);
3435 return;
3536 }
3637···4243 case "help":
4344 response = await handleHelp();
4445 break;
4646+ case "settings":
4747+ await handleSettings(
4848+ context.triggerId as string,
4949+ userId,
5050+ true,
5151+ );
5252+ return;
4553 default:
4654 response = await handleHome(userId);
4755 break;
+49
src/libs/hackatime.ts
···11+/**
22+ * Maps Hackatime version identifiers to their corresponding data
33+ */
44+export const HACKATIME_VERSIONS = {
55+ v1: {
66+ id: "v1",
77+ name: "Hackatime",
88+ apiUrl: "https://waka.hackclub.com/api",
99+ },
1010+ v2: {
1111+ id: "v2",
1212+ name: "Hackatime v2",
1313+ apiUrl: "https://hackatime.hackclub.com/api",
1414+ },
1515+} as const;
1616+1717+export type HackatimeVersion = keyof typeof HACKATIME_VERSIONS;
1818+1919+/**
2020+ * Converts a Hackatime version identifier to its full API URL
2121+ * @param version The version identifier (v1 or v2)
2222+ * @returns The corresponding API URL
2323+ */
2424+export function getHackatimeApiUrl(version: HackatimeVersion): string {
2525+ return HACKATIME_VERSIONS[version].apiUrl;
2626+}
2727+2828+/**
2929+ * Gets the fancy name for a Hackatime version
3030+ * @param version The version identifier (v1 or v2)
3131+ * @returns The fancy display name for the version
3232+ */
3333+export function getHackatimeName(version: HackatimeVersion): string {
3434+ return HACKATIME_VERSIONS[version].name;
3535+}
3636+3737+/**
3838+ * Determines which Hackatime version is being used based on the API URL
3939+ * @param apiUrl The full Hackatime API URL
4040+ * @returns The version identifier (v1 or v2), defaulting to v2 if not recognized
4141+ */
4242+export function getHackatimeVersion(apiUrl: string): HackatimeVersion {
4343+ for (const [version, data] of Object.entries(HACKATIME_VERSIONS)) {
4444+ if (apiUrl === data.apiUrl) {
4545+ return version as HackatimeVersion;
4646+ }
4747+ }
4848+ return "v2";
4949+}