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",
13 apiUrl: "https://hackatime.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_projects_sum?: number;
88 total_projects_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 if (response.status === 401) {
128 // Return blank info for 401 Unauthorized errors
129 return {
130 categories: [],
131 projects: [],
132 languages: [],
133 editors: [],
134 operating_systems: [],
135 total_projects_sum: 0,
136 total_projects_human_readable: "0h 0m 0s",
137 projectsKeys: [],
138 };
139 }
140 throw new Error(
141 `Failed to fetch Hackatime summary: ${response.status} ${response.statusText}: ${await response.text()}`,
142 );
143 }
144
145 const data = await response.json();
146
147 // Add derived properties similar to the shell command
148 const totalProjectsSum =
149 data.projects?.reduce(
150 (sum: number, project: { total: number }) => sum + project.total,
151 0,
152 ) || 0;
153 const hours = Math.floor(totalProjectsSum / 3600);
154 const minutes = Math.floor((totalProjectsSum % 3600) / 60);
155 const seconds = totalProjectsSum % 60;
156
157 // Get all project keys from the data
158 const allProjectsKeys =
159 data.projects
160 ?.sort(
161 (a: { total: number }, b: { total: number }) =>
162 b.total - a.total,
163 )
164 .map((project: { key: string }) => project.key) || [];
165
166 // Filter by provided project keys if any
167 const projectsKeys = projectKeys
168 ? allProjectsKeys.filter((key: string) => projectKeys.includes(key))
169 : allProjectsKeys;
170
171 return {
172 ...data,
173 total_projects_sum: totalProjectsSum,
174 total_projects_human_readable: `${hours}h ${minutes}m ${seconds}s`,
175 projectsKeys: projectsKeys,
176 };
177}
178
179/**
180 * Fetches the most recent project keys from a user's Hackatime data
181 * @param userId The user ID to fetch the project keys for
182 * @param limit The maximum number of projects to return (defaults to 10)
183 * @param version The Hackatime version to use (defaults to v1)
184 * @returns A promise that resolves to an array of recent project keys
185 */
186export async function fetchRecentProjectKeys(
187 userId: string,
188 limit = 10,
189 version: HackatimeVersion = "v1",
190): Promise<string[]> {
191 const summary = await fetchHackatimeSummary(userId, version);
192
193 // Extract projects and sort by most recent
194 const sortedProjects =
195 summary.projects?.sort(
196 (a: { last_used_at: string }, b: { last_used_at: string }) =>
197 new Date(b.last_used_at).getTime() -
198 new Date(a.last_used_at).getTime(),
199 ) || [];
200
201 // Return the keys of the most recent projects up to the limit
202 return sortedProjects
203 .slice(0, limit)
204 .map((project: { key: string }) => project.key);
205}