providing password reset services for a long while: circa 2025
1import type { LinkUnfurls, MessageAttachment } from "slack-edge";
2import { slackApp } from "../index";
3import type { UserData } from "./unfurl.types";
4
5const unfurl = async () => {
6 slackApp.event("link_shared", async ({ context, payload }) => {
7 const unfurls: LinkUnfurls = {};
8
9 for (const link of payload.links) {
10 const url = new URL(link.url);
11 const path = url.pathname;
12 if (path.includes("api") || path.includes("swagger")) {
13 unfurls[link.url] = {
14 blocks: [
15 {
16 type: "section",
17 text: {
18 type: "mrkdwn",
19 text: "*API Documentation*",
20 },
21 },
22 {
23 type: "context",
24 elements: [
25 {
26 type: "mrkdwn",
27 text: "The api has full swagger docs available at <https://waka.hackclub.com/swagger-ui/swagger-ui/index.html|`/swagger-ui`> if you have any questions about the api or need an admin token dm <@U062UG485EE>",
28 },
29 ],
30 },
31 {
32 type: "divider",
33 },
34 {
35 type: "image",
36 image_url:
37 "https://hc-cdn.hel1.your-objectstorage.com/s/v3/c8e02c0a89535d80ce8e0a64ef36f0ead31ad26a_image.png",
38 alt_text: "Hackatime Swagger Docs OG Image",
39 },
40 ],
41 };
42 } else {
43 const unfurl = await codingTime(url, payload.user);
44 if (unfurl) unfurls[link.url] = unfurl;
45 }
46 }
47
48 await context.client.chat.unfurl({
49 channel: payload.channel,
50 ts: payload.message_ts,
51 unfurls,
52 });
53 });
54};
55
56async function codingTime(
57 link: URL,
58 user: string,
59): Promise<MessageAttachment | null> {
60 const interval = link.searchParams.get("interval") || "last_7_days";
61
62 const userData = await fetchUserData(user, interval);
63 if (!userData) return null;
64 const totalSeconds = userData.projects.reduce(
65 (acc: number, curr: { total: number }) => acc + curr.total,
66 0,
67 );
68 const hours = Math.floor(totalSeconds / 3600);
69 const minutes = Math.floor((totalSeconds % 3600) / 60);
70 const seconds = Math.floor(totalSeconds % 60);
71
72 const pageTitle = await (async () => {
73 const response = await fetch(link);
74 const html = await response.text();
75 const match = html.match(/<title>(.*?)<\/title>/);
76 return match ? match[1] : "Coding Activity";
77 })();
78
79 return {
80 blocks: [
81 {
82 type: "section",
83 text: {
84 type: "mrkdwn",
85 text: `*${pageTitle}*\n\nThe best place to track your coding activity :orpheus-leaf-grow:`,
86 },
87 },
88 {
89 type: "image",
90 image_url: "https://waka.hackclub.com/assets/images/og.jpg",
91 alt_text: "Hackatime OG Image",
92 },
93 {
94 type: "divider",
95 },
96 {
97 type: "context",
98 elements: [
99 {
100 type: "mrkdwn",
101 text: `<@${user}> has spent ${hours} hours, ${minutes} minutes, and ${seconds} seconds coding in the ${interval.replaceAll("_", " ")}${interval.includes("days") || interval.includes("month") ? "" : " interval"}.${link.toString().includes("settings#danger_zone") ? "\n\ncareful there bud :eyes: the danger zone is no joke" : ""}${link.toString().includes("projects") ? `\n\nthey have over ${userData.projects.length} projects with the biggest being \`${userData.projects.sort((a: { total: number }, b: { total: number }) => b.total - a.total)[0].key}\` at \`${Math.floor(userData.projects.sort((a: { total: number }, b: { total: number }) => b.total - a.total)[0].total / 3600)} hours\`` : ""}`,
102 },
103 ],
104 },
105 ],
106 };
107}
108
109export async function fetchUserData(
110 user: string | undefined,
111 interval?: string,
112): Promise<UserData | null> {
113 const response = await fetch(
114 `https://waka.hackclub.com/api/summary?user=${user}&interval=${interval || "last_7_days"}`,
115 {
116 method: "GET",
117 headers: {
118 accept: "application/json",
119 Authorization: `Bearer ${process.env.HACKATIME_API_KEY}`,
120 },
121 },
122 );
123
124 return response.status === 200 ? ((await response.json()) as UserData) : null;
125}
126
127export default unfurl;