···33import { db } from "../../../libs/db";
44import { takes as takesTable, users as usersTable } from "../../../libs/schema";
55import * as Sentry from "@sentry/bun";
66+import {
77+ fetchHackatimeSummary,
88+ type HackatimeVersion,
99+} from "../../../libs/hackatime";
1010+import { prettyPrintTime } from "../../../libs/time";
611712export default async function upload() {
813 slackApp.anyMessage(async ({ payload, context }) => {
···2025 const userInDB = await db
2126 .select()
2227 .from(usersTable)
2323- .where(eq(usersTable.id, user));
2828+ .where(eq(usersTable.id, user))
2929+ .then((users) => users[0] || null);
24302525- if (userInDB.length === 0) {
3131+ if (!userInDB) {
2632 await slackClient.chat.postMessage({
2733 channel: payload.channel,
2834 thread_ts: payload.ts,
···129135 }
130136131137 // fetch time spent on project via hackatime
132132- const timeSpentMs = 60000;
138138+ const timeSpent = await fetchHackatimeSummary(
139139+ user,
140140+ userInDB.hackatimeVersion as HackatimeVersion,
141141+ JSON.parse(userInDB.hackatimeKeys),
142142+ new Date(userInDB.lastTakeUploadDate),
143143+ new Date(),
144144+ ).then((res) => res.total_categories_sum || 0);
133145134146 await db.insert(takesTable).values({
135147 id: Bun.randomUUIDv7(),
···137149 ts: payload.ts,
138150 notes: markdownText,
139151 media: JSON.stringify(mediaUrls),
140140- elapsedTime: timeSpentMs / 1000,
152152+ elapsedTime: timeSpent,
141153 });
142154143155 await slackClient.reactions.add({
···155167 type: "section",
156168 text: {
157169 type: "mrkdwn",
158158- text: `:inbox_tray: ${mediaUrls.length > 0 ? "uploaded media and " : ""}saved your notes!`,
170170+ text: `:inbox_tray: ${mediaUrls.length > 0 ? "uploaded media and " : ""}saved your notes! That's \`${prettyPrintTime(timeSpent * 1000)}\`!`,
159171 },
160172 },
161173 ],
+54-11
src/libs/hackatime.ts
···99 },
1010 v2: {
1111 id: "v2",
1212- name: "Hackatime v2",
1313- apiUrl: "https://hackatime.hackclub.com/api",
1212+ name: "Hackatime V2 (broken don't use)",
1313+ apiUrl: "https://waka.hackclub.com/api",
1414 },
1515} as const;
1616···18181919/**
2020 * Converts a Hackatime version identifier to its full API URL
2121- * @param version The version identifier (v1 or v2)
2121+ * @param version The version identifier (v1 or v2 soon)
2222 * @returns The corresponding API URL
2323 */
2424export function getHackatimeApiUrl(version: HackatimeVersion): string {
···27272828/**
2929 * Gets the fancy name for a Hackatime version
3030- * @param version The version identifier (v1 or v2)
3030+ * @param version The version identifier (v1 or v2 soon)
3131 * @returns The fancy display name for the version
3232 */
3333export function getHackatimeName(version: HackatimeVersion): string {
···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
4040+ * @returns The version identifier (v1 or v2 soon), defaulting to v1 if not recognized
4141 */
4242export function getHackatimeVersion(apiUrl: string): HackatimeVersion {
4343 for (const [version, data] of Object.entries(HACKATIME_VERSIONS)) {
···4545 return version as HackatimeVersion;
4646 }
4747 }
4848- return "v2";
4848+ return "v1";
4949+}
5050+5151+/**
5252+ * Type definition for Hackatime summary response
5353+ */
5454+export interface HackatimeSummaryResponse {
5555+ categories?: Array<{
5656+ name: string;
5757+ total: number;
5858+ percent?: number;
5959+ }>;
6060+ projects?: Array<{
6161+ key: string;
6262+ name: string;
6363+ total: number;
6464+ percent?: number;
6565+ last_used_at: string;
6666+ }>;
6767+ languages?: Array<{
6868+ name: string;
6969+ total: number;
7070+ percent?: number;
7171+ }>;
7272+ editors?: Array<{
7373+ name: string;
7474+ total: number;
7575+ percent?: number;
7676+ }>;
7777+ operating_systems?: Array<{
7878+ name: string;
7979+ total: number;
8080+ percent?: number;
8181+ }>;
8282+ range?: {
8383+ start: string;
8484+ end: string;
8585+ timezone: string;
8686+ };
8787+ total_categories_sum?: number;
8888+ total_categories_human_readable?: string;
8989+ projectsKeys?: string[];
4990}
50915192/**
5293 * Fetches a user's Hackatime summary
5394 * @param userId The user ID to fetch the summary for
5454- * @param version The Hackatime version to use (defaults to v2)
9595+ * @param version The Hackatime version to use (defaults to v1)
5596 * @param projectKeys Optional array of project keys to filter results by
9797+ * @param from Optional start date for the summary
9898+ * @param to Optional end date for the summary
5699 * @returns A promise that resolves to the summary data
57100 */
58101export async function fetchHackatimeSummary(
59102 userId: string,
6060- version: HackatimeVersion = "v2",
103103+ version: HackatimeVersion = "v1",
61104 projectKeys?: string[],
62105 from?: Date,
63106 to?: Date,
6464-) {
107107+): Promise<HackatimeSummaryResponse> {
65108 const apiUrl = getHackatimeApiUrl(version);
66109 const params = new URLSearchParams({
67110 user: userId,
···124167 * Fetches the most recent project keys from a user's Hackatime data
125168 * @param userId The user ID to fetch the project keys for
126169 * @param limit The maximum number of projects to return (defaults to 10)
127127- * @param version The Hackatime version to use (defaults to v2)
170170+ * @param version The Hackatime version to use (defaults to v1)
128171 * @returns A promise that resolves to an array of recent project keys
129172 */
130173export async function fetchRecentProjectKeys(
131174 userId: string,
132175 limit = 10,
133133- version: HackatimeVersion = "v2",
176176+ version: HackatimeVersion = "v1",
134177): Promise<string[]> {
135178 const summary = await fetchHackatimeSummary(userId, version);
136179
+39-33
src/libs/schema.ts
···11import { pgTable, text, integer, boolean } from "drizzle-orm/pg-core";
22import type { Pool } from "pg";
33+import TakesConfig from "./config";
3445// Define the takes table
56export const takes = pgTable("takes", {
···2627 projectName: text("project_name").notNull().default(""),
2728 projectDescription: text("project_description").notNull().default(""),
2829 projectBannerUrl: text("project_banner_url").notNull().default(""),
2929- hackatimeVersion: text("hackatime_version").notNull().default("v2"),
3030+ hackatimeVersion: text("hackatime_version").notNull().default("v1"),
3131+ lastTakeUploadDate: text("last_take_upload_date")
3232+ .notNull()
3333+ .default(TakesConfig.START_DATE.toISOString()),
3034 repoLink: text("repo_link"),
3135 demoLink: text("demo_link"),
3236});
33373438export async function setupTriggers(pool: Pool) {
3539 await pool.query(`
3636- CREATE INDEX IF NOT EXISTS idx_takes_user_id ON takes(user_id);
4040+ CREATE INDEX IF NOT EXISTS idx_takes_user_id ON takes(user_id);
37413838- CREATE OR REPLACE FUNCTION update_user_total_time()
3939- RETURNS TRIGGER AS $$
4040- BEGIN
4141- IF TG_OP = 'INSERT' THEN
4242- UPDATE users
4343- SET total_takes_time = COALESCE(total_takes_time, 0) + NEW.elapsed_time
4444- WHERE id = NEW.user_id;
4545- RETURN NEW;
4646- ELSIF TG_OP = 'DELETE' THEN
4747- UPDATE users
4848- SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time
4949- WHERE id = OLD.user_id;
5050- RETURN OLD;
5151- ELSIF TG_OP = 'UPDATE' THEN
5252- UPDATE users
5353- SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time + NEW.elapsed_time
5454- WHERE id = NEW.user_id;
5555- RETURN NEW;
5656- END IF;
4242+ CREATE OR REPLACE FUNCTION update_user_total_time()
4343+ RETURNS TRIGGER AS $$
4444+ BEGIN
4545+ IF TG_OP = 'INSERT' THEN
4646+ UPDATE users
4747+ SET total_takes_time = COALESCE(total_takes_time, 0) + NEW.elapsed_time,
4848+ last_take_upload_date = NEW.created_at
4949+ WHERE id = NEW.user_id;
5050+ RETURN NEW;
5151+ ELSIF TG_OP = 'DELETE' THEN
5252+ UPDATE users
5353+ SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time
5454+ WHERE id = OLD.user_id;
5555+ RETURN OLD;
5656+ ELSIF TG_OP = 'UPDATE' THEN
5757+ UPDATE users
5858+ SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time + NEW.elapsed_time,
5959+ last_take_upload_date = CASE WHEN NEW.created_at > OLD.created_at THEN NEW.created_at ELSE users.last_take_upload_date END
6060+ WHERE id = NEW.user_id;
6161+ RETURN NEW;
6262+ END IF;
57635858- RETURN NULL; -- Default return for unexpected operations
6464+ RETURN NULL; -- Default return for unexpected operations
59656060- EXCEPTION WHEN OTHERS THEN
6161- RAISE NOTICE 'Error updating user total time: %', SQLERRM;
6262- RETURN NULL;
6363- END;
6464- $$ LANGUAGE plpgsql;
6666+ EXCEPTION WHEN OTHERS THEN
6767+ RAISE NOTICE 'Error updating user total time: %', SQLERRM;
6868+ RETURN NULL;
6969+ END;
7070+ $$ LANGUAGE plpgsql;
65716666- DROP TRIGGER IF EXISTS update_user_total_time_trigger ON takes;
7272+ DROP TRIGGER IF EXISTS update_user_total_time_trigger ON takes;
67736868- CREATE TRIGGER update_user_total_time_trigger
6969- AFTER INSERT OR UPDATE OR DELETE ON takes
7070- FOR EACH ROW
7171- EXECUTE FUNCTION update_user_total_time();
7272- `);
7474+ CREATE TRIGGER update_user_total_time_trigger
7575+ AFTER INSERT OR UPDATE OR DELETE ON takes
7676+ FOR EACH ROW
7777+ EXECUTE FUNCTION update_user_total_time();
7878+ `);
7379}