a fun bot for the hc slack
1/**
2 * Maps Hackatime version identifiers to their corresponding data
3 */
4export const HACKATIME_VERSIONS = {
5 v1: {
6 id: "v1",
7 name: "Hackatime",
8 apiUrl: "https://waka.hackclub.com/api",
9 },
10 v2: {
11 id: "v2",
12 name: "Hackatime V2 (broken don't use)",
13 apiUrl: "https://waka.hackclub.com/api",
14 },
15} as const;
16
17export type HackatimeVersion = keyof typeof HACKATIME_VERSIONS;
18
19/**
20 * Converts a Hackatime version identifier to its full API URL
21 * @param version The version identifier (v1 or v2 soon)
22 * @returns The corresponding API URL
23 */
24export function getHackatimeApiUrl(version: HackatimeVersion): string {
25 return HACKATIME_VERSIONS[version].apiUrl;
26}
27
28/**
29 * Gets the fancy name for a Hackatime version
30 * @param version The version identifier (v1 or v2 soon)
31 * @returns The fancy display name for the version
32 */
33export function getHackatimeName(version: HackatimeVersion): string {
34 return HACKATIME_VERSIONS[version].name;
35}
36
37/**
38 * Determines which Hackatime version is being used based on the API URL
39 * @param apiUrl The full Hackatime API URL
40 * @returns The version identifier (v1 or v2 soon), defaulting to v1 if not recognized
41 */
42export function getHackatimeVersion(apiUrl: string): HackatimeVersion {
43 for (const [version, data] of Object.entries(HACKATIME_VERSIONS)) {
44 if (apiUrl === data.apiUrl) {
45 return version as HackatimeVersion;
46 }
47 }
48 return "v1";
49}
50
51/**
52 * Type definition for Hackatime summary response
53 */
54export interface HackatimeSummaryResponse {
55 categories?: Array<{
56 name: string;
57 total: number;
58 percent?: number;
59 }>;
60 projects?: Array<{
61 key: string;
62 name: string;
63 total: number;
64 percent?: number;
65 last_used_at: string;
66 }>;
67 languages?: Array<{
68 name: string;
69 total: number;
70 percent?: number;
71 }>;
72 editors?: Array<{
73 name: string;
74 total: number;
75 percent?: number;
76 }>;
77 operating_systems?: Array<{
78 name: string;
79 total: number;
80 percent?: number;
81 }>;
82 range?: {
83 start: string;
84 end: string;
85 timezone: string;
86 };
87 total_categories_sum?: number;
88 total_categories_human_readable?: string;
89 projectsKeys?: string[];
90}
91
92/**
93 * Fetches a user's Hackatime summary
94 * @param userId The user ID to fetch the summary for
95 * @param version The Hackatime version to use (defaults to v1)
96 * @param projectKeys Optional array of project keys to filter results by
97 * @param from Optional start date for the summary
98 * @param to Optional end date for the summary
99 * @returns A promise that resolves to the summary data
100 */
101export async function fetchHackatimeSummary(
102 userId: string,
103 version: HackatimeVersion = "v1",
104 projectKeys?: string[],
105 from?: Date,
106 to?: Date,
107): Promise<HackatimeSummaryResponse> {
108 const apiUrl = getHackatimeApiUrl(version);
109 const params = new URLSearchParams({
110 user: userId,
111 });
112 if (!from || !to) {
113 params.append("interval", "month");
114 } else if (from && to) {
115 params.append("from", from.toISOString());
116 params.append("to", to.toISOString());
117 }
118
119 const response = await fetch(`${apiUrl}/summary?${params.toString()}`, {
120 headers: {
121 accept: "application/json",
122 Authorization: "Bearer 2ce9e698-8a16-46f0-b49a-ac121bcfd608",
123 },
124 });
125
126 if (!response.ok) {
127 throw new Error(
128 `Failed to fetch Hackatime summary: ${response.status} ${response.statusText}: ${await response.text()}`,
129 );
130 }
131
132 const data = await response.json();
133
134 // Add derived properties similar to the shell command
135 const totalCategoriesSum =
136 data.categories?.reduce(
137 (sum: number, category: { total: number }) => sum + category.total,
138 0,
139 ) || 0;
140 const hours = Math.floor(totalCategoriesSum / 3600);
141 const minutes = Math.floor((totalCategoriesSum % 3600) / 60);
142 const seconds = totalCategoriesSum % 60;
143
144 // Get all project keys from the data
145 const allProjectsKeys =
146 data.projects
147 ?.sort(
148 (a: { total: number }, b: { total: number }) =>
149 b.total - a.total,
150 )
151 .map((project: { key: string }) => project.key) || [];
152
153 // Filter by provided project keys if any
154 const projectsKeys = projectKeys
155 ? allProjectsKeys.filter((key: string) => projectKeys.includes(key))
156 : allProjectsKeys;
157
158 return {
159 ...data,
160 total_categories_sum: totalCategoriesSum,
161 total_categories_human_readable: `${hours}h ${minutes}m ${seconds}s`,
162 projectsKeys: projectsKeys,
163 };
164}
165
166/**
167 * Fetches the most recent project keys from a user's Hackatime data
168 * @param userId The user ID to fetch the project keys for
169 * @param limit The maximum number of projects to return (defaults to 10)
170 * @param version The Hackatime version to use (defaults to v1)
171 * @returns A promise that resolves to an array of recent project keys
172 */
173export async function fetchRecentProjectKeys(
174 userId: string,
175 limit = 10,
176 version: HackatimeVersion = "v1",
177): Promise<string[]> {
178 const summary = await fetchHackatimeSummary(userId, version);
179
180 // Extract projects and sort by most recent
181 const sortedProjects =
182 summary.projects?.sort(
183 (a: { last_used_at: string }, b: { last_used_at: string }) =>
184 new Date(b.last_used_at).getTime() -
185 new Date(a.last_used_at).getTime(),
186 ) || [];
187
188 // Return the keys of the most recent projects up to the limit
189 return sortedProjects
190 .slice(0, limit)
191 .map((project: { key: string }) => project.key);
192}