a fun bot for the hc slack
at main 5.5 kB view raw
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}