a fun bot for the hc slack
1import { slackApp, slackClient } from "../../../index";
2import { db } from "../../../libs/db";
3import { eq } from "drizzle-orm";
4import { users as usersTable } from "../../../libs/schema";
5import {
6 fetchRecentProjectKeys,
7 getHackatimeName,
8 HACKATIME_VERSIONS,
9 type HackatimeVersion,
10} from "../../../libs/hackatime";
11import { deployToHackClubCDN } from "../../../libs/cdn";
12import { getCategoryLabel } from "../../../libs/categories";
13import type { Project } from "../../api/routes/projects";
14
15export async function handleSettings(
16 triggerID: string,
17 user: string,
18 prefill = false,
19) {
20 let initialValues: {
21 project_name: string;
22 project_description: string;
23 project_category: Project["projectCategory"];
24 repo_link: string | undefined;
25 demo_link: string | undefined;
26 hackatime_version: HackatimeVersion;
27 hackatime_keys: string[];
28 } = {
29 project_name: "",
30 project_description: "",
31 project_category: "other",
32 repo_link: undefined,
33 demo_link: undefined,
34 hackatime_version: "v1",
35 hackatime_keys: [],
36 };
37
38 if (prefill) {
39 try {
40 // Check if user already has a project in the database
41 const existingUser = (
42 await db
43 .select()
44 .from(usersTable)
45 .where(eq(usersTable.id, user))
46 )[0];
47
48 if (existingUser) {
49 initialValues = {
50 project_name: existingUser.projectName,
51 project_description: existingUser.projectDescription,
52 project_category:
53 existingUser.projectCategory &&
54 [
55 "hardware",
56 "hardware_software",
57 "website",
58 "app",
59 "game",
60 "art_design",
61 "other",
62 ].includes(existingUser.projectCategory)
63 ? (existingUser.projectCategory as Project["projectCategory"])
64 : "other",
65 repo_link: existingUser.repoLink || undefined,
66 demo_link: existingUser.demoLink || undefined,
67 hackatime_version:
68 existingUser.hackatimeVersion as HackatimeVersion,
69 hackatime_keys: existingUser.hackatimeKeys
70 ? JSON.parse(existingUser.hackatimeKeys)
71 : [],
72 };
73 }
74 } catch (error) {
75 console.error("Error prefilling form:", error);
76 }
77 }
78
79 const hackatimeKeys = await fetchRecentProjectKeys(
80 user,
81 10,
82 initialValues.hackatime_version as HackatimeVersion,
83 );
84
85 await slackClient.views.open({
86 trigger_id: triggerID,
87 view: {
88 type: "modal",
89 title: {
90 type: "plain_text",
91 text: "Setup Project",
92 },
93 submit: {
94 type: "plain_text",
95 text: "Submit",
96 },
97 callback_id: "takes_setup_submit",
98 blocks: [
99 {
100 type: "input",
101 block_id: "project_name",
102 label: {
103 type: "plain_text",
104 text: "Project Name",
105 },
106 element: {
107 type: "plain_text_input",
108 action_id: "project_name_input",
109 initial_value: initialValues.project_name,
110 placeholder: {
111 type: "plain_text",
112 text: "Enter your project name",
113 },
114 },
115 },
116 {
117 type: "input",
118 block_id: "project_description",
119 label: {
120 type: "plain_text",
121 text: "Project Description",
122 },
123 element: {
124 type: "plain_text_input",
125 action_id: "project_description_input",
126 multiline: true,
127 initial_value: initialValues.project_description,
128 placeholder: {
129 type: "plain_text",
130 text: "Describe your project",
131 },
132 },
133 },
134 {
135 type: "input",
136 block_id: "project_category",
137 label: {
138 type: "plain_text",
139 text: "Project Category",
140 },
141 element: {
142 type: "static_select",
143 action_id: "project_category_input",
144 initial_option: initialValues.project_category
145 ? {
146 text: {
147 type: "plain_text",
148 text: getCategoryLabel(
149 initialValues.project_category,
150 ),
151 },
152 value: initialValues.project_category,
153 }
154 : undefined,
155 placeholder: {
156 type: "plain_text",
157 text: "Select a project category",
158 },
159 options: [
160 {
161 text: {
162 type: "plain_text",
163 text: "Hardware",
164 },
165 value: "hardware",
166 },
167 {
168 text: {
169 type: "plain_text",
170 text: "Hardware + Software",
171 },
172 value: "hardware_software",
173 },
174 {
175 text: {
176 type: "plain_text",
177 text: "Website",
178 },
179 value: "website",
180 },
181 {
182 text: {
183 type: "plain_text",
184 text: "App",
185 },
186 value: "app",
187 },
188 {
189 text: {
190 type: "plain_text",
191 text: "Game",
192 },
193 value: "game",
194 },
195 {
196 text: {
197 type: "plain_text",
198 text: "Art & Design",
199 },
200 value: "art_design",
201 },
202 {
203 text: {
204 type: "plain_text",
205 text: "Other",
206 },
207 value: "other",
208 },
209 ],
210 },
211 },
212 {
213 type: "input",
214 block_id: "project_banner",
215 label: {
216 type: "plain_text",
217 text: `Banner Image${prefill ? " (this will replace your current banner)" : ""}`,
218 },
219 element: {
220 type: "file_input",
221 action_id: "project_banner_input",
222 },
223 optional: true,
224 },
225 {
226 type: "input",
227 block_id: "repo_link",
228 optional: false,
229 label: {
230 type: "plain_text",
231 text: "Repository Link",
232 },
233 element: {
234 type: "plain_text_input",
235 action_id: "repo_link_input",
236 initial_value: initialValues.repo_link,
237 placeholder: {
238 type: "plain_text",
239 text: "Add a link to your repository",
240 },
241 },
242 },
243 {
244 type: "input",
245 block_id: "demo_link",
246 optional: true,
247 label: {
248 type: "plain_text",
249 text: "Demo Link",
250 },
251 element: {
252 type: "plain_text_input",
253 action_id: "demo_link_input",
254 initial_value: initialValues.demo_link,
255 placeholder: {
256 type: "plain_text",
257 text: "Optional: Add a link to your demo",
258 },
259 },
260 },
261 {
262 type: "input",
263 block_id: "hackatime_version",
264 label: {
265 type: "plain_text",
266 text: "Hackatime Version",
267 },
268 element: {
269 type: "static_select",
270 action_id: "hackatime_version_input",
271 initial_option: {
272 text: {
273 type: "plain_text",
274 text: getHackatimeName(
275 initialValues.hackatime_version,
276 ),
277 },
278 value: initialValues.hackatime_version,
279 },
280 options: Object.values(HACKATIME_VERSIONS).map((v) => ({
281 text: {
282 type: "plain_text",
283 text: v.name,
284 },
285 value: v.id,
286 })),
287 },
288 },
289 hackatimeKeys.length > 0
290 ? {
291 type: "input",
292 block_id: "project_keys",
293 label: {
294 type: "plain_text",
295 text: "Project Keys",
296 },
297 element: {
298 type: "multi_static_select",
299 action_id: "project_keys_input",
300 initial_options:
301 initialValues.hackatime_keys.length === 0
302 ? undefined
303 : initialValues.hackatime_keys.map(
304 (key) => ({
305 text: {
306 type: "plain_text",
307 text: key,
308 },
309 value: key,
310 }),
311 ),
312 options: hackatimeKeys.map((key) => ({
313 text: {
314 type: "plain_text",
315 text: key,
316 },
317 value: key,
318 })),
319 },
320 }
321 : {
322 type: "section",
323 text: {
324 text: "You don't have any hackatime projects. Go setup hackatime with `/hackatime`",
325 type: "mrkdwn",
326 },
327 },
328 ],
329 },
330 });
331}
332
333export async function setupSubmitListener() {
334 slackApp.view("takes_setup_submit", async ({ payload, context }) => {
335 if (payload.type !== "view_submission") return;
336 const values = payload.view.state.values;
337 const userId = payload.user.id;
338
339 const file = values.project_banner?.project_banner_input?.files?.[0]
340 ?.url_private_download as string;
341
342 const hackatimeKeys = JSON.stringify(
343 values.project_keys?.project_keys_input?.selected_options?.map(
344 (option) => option.value,
345 ) || [],
346 );
347
348 try {
349 const projectBannerUrl = file
350 ? await deployToHackClubCDN([file]).then(
351 (res) => res.files[0]?.deployedUrl,
352 )
353 : undefined;
354
355 const hackatimeVersion = values.hackatime_version
356 ?.hackatime_version_input?.selected_option
357 ?.value as HackatimeVersion;
358
359 await db
360 .insert(usersTable)
361 .values({
362 id: userId,
363 projectName: values.project_name?.project_name_input?.value,
364 projectDescription:
365 values.project_description?.project_description_input
366 ?.value,
367 projectCategory:
368 values.project_category?.project_category_input
369 ?.selected_option?.value,
370 projectBannerUrl,
371 repoLink: values.repo_link?.repo_link_input?.value,
372 demoLink: values.demo_link?.demo_link_input?.value,
373 hackatimeVersion,
374 hackatimeKeys,
375 })
376 .onConflictDoUpdate({
377 target: usersTable.id,
378 set: {
379 projectName:
380 values.project_name?.project_name_input?.value,
381 projectDescription:
382 values.project_description
383 ?.project_description_input?.value,
384 projectBannerUrl,
385 repoLink: values.repo_link?.repo_link_input?.value,
386 demoLink: values.demo_link?.demo_link_input?.value,
387 hackatimeVersion,
388 hackatimeKeys,
389 },
390 });
391 } catch (error) {
392 console.error("Error processing file:", error);
393 throw error;
394 }
395 });
396}