a fun bot for the hc slack
at v0.2.0 5.1 kB view raw
1import { eq } from "drizzle-orm"; 2import { slackApp, slackClient } from "../../../index"; 3import { db } from "../../../libs/db"; 4import { takes as takesTable, users as usersTable } from "../../../libs/schema"; 5import * as Sentry from "@sentry/bun"; 6import { 7 fetchHackatimeSummary, 8 type HackatimeVersion, 9} from "../../../libs/hackatime"; 10import { prettyPrintTime } from "../../../libs/time"; 11 12export default async function upload() { 13 slackApp.anyMessage(async ({ payload, context }) => { 14 try { 15 const user = payload.user as string; 16 17 if ( 18 payload.subtype === "bot_message" || 19 payload.subtype === "thread_broadcast" || 20 payload.thread_ts || 21 payload.channel !== process.env.SLACK_LISTEN_CHANNEL 22 ) 23 return; 24 25 const userInDB = await db 26 .select() 27 .from(usersTable) 28 .where(eq(usersTable.id, user)) 29 .then((users) => users[0] || null); 30 31 if (!userInDB) { 32 await slackClient.chat.postMessage({ 33 channel: payload.channel, 34 thread_ts: payload.ts, 35 text: "We don't have a project for you; set one up by clicking the button below or by running `/takes`", 36 blocks: [ 37 { 38 type: "section", 39 text: { 40 type: "mrkdwn", 41 text: "We don't have a project for you; set one up by clicking the button below or by running `/takes`", 42 }, 43 }, 44 { 45 type: "actions", 46 elements: [ 47 { 48 type: "button", 49 text: { 50 type: "plain_text", 51 text: "setup your project", 52 }, 53 action_id: "takes_setup", 54 }, 55 ], 56 }, 57 { 58 type: "context", 59 elements: [ 60 { 61 type: "plain_text", 62 text: "don't forget to resend your update after setting up your project!", 63 }, 64 ], 65 }, 66 ], 67 }); 68 return; 69 } 70 71 // Convert Slack formatting to markdown 72 const replaceUserMentions = async (text: string) => { 73 const regex = /<@([A-Z0-9]+)>/g; 74 const matches = text.match(regex); 75 76 if (!matches) return text; 77 78 let result = text; 79 for (const match of matches) { 80 const userId = match.match(/[A-Z0-9]+/)?.[0]; 81 if (!userId) continue; 82 83 try { 84 const userInfo = await slackClient.users.info({ 85 user: userId, 86 }); 87 const name = 88 userInfo.user?.profile?.display_name || 89 userInfo.user?.real_name || 90 userId; 91 result = result.replace(match, `@${name}`); 92 } catch (e) { 93 result = result.replace(match, `@${userId}`); 94 } 95 } 96 return result; 97 }; 98 99 const markdownText = (await replaceUserMentions(payload.text)) 100 .replace(/\*(.*?)\*/g, "**$1**") // Bold 101 .replace(/_(.*?)_/g, "*$1*") // Italic 102 .replace(/~(.*?)~/g, "~~$1~~") // Strikethrough 103 .replace(/<(https?:\/\/[^|]+)\|([^>]+)>/g, "[$2]($1)"); // Links 104 105 const mediaUrls = []; 106 107 if (payload.files && payload.files.length > 0) { 108 for (const file of payload.files) { 109 if ( 110 file.mimetype && 111 (file.mimetype.startsWith("image/") || 112 file.mimetype.startsWith("video/")) 113 ) { 114 const fileres = await slackClient.files.sharedPublicURL( 115 { 116 file: file.id as string, 117 token: process.env.SLACK_USER_TOKEN, 118 }, 119 ); 120 121 const fetchRes = await fetch( 122 fileres.file?.permalink_public as string, 123 ); 124 const html = await fetchRes.text(); 125 const match = html.match( 126 /https:\/\/files.slack.com\/files-pri\/[^"]+pub_secret=([^"&]*)/, 127 ); 128 const filePublicUrl = match?.[0]; 129 130 if (filePublicUrl) { 131 mediaUrls.push(filePublicUrl); 132 } 133 } 134 } 135 } 136 137 // fetch time spent on project via hackatime 138 const timeSpent = await fetchHackatimeSummary( 139 user, 140 userInDB.hackatimeVersion as HackatimeVersion, 141 JSON.parse(userInDB.hackatimeKeys), 142 new Date(userInDB.lastTakeUploadDate), 143 new Date(), 144 ).then((res) => res.total_categories_sum || 0); 145 146 await db.insert(takesTable).values({ 147 id: Bun.randomUUIDv7(), 148 userId: user, 149 ts: payload.ts, 150 notes: markdownText, 151 media: JSON.stringify(mediaUrls), 152 elapsedTime: timeSpent, 153 }); 154 155 await slackClient.reactions.add({ 156 channel: payload.channel, 157 timestamp: payload.ts, 158 name: "fire", 159 }); 160 161 await slackClient.chat.postMessage({ 162 channel: payload.channel, 163 thread_ts: payload.ts, 164 text: ":inbox_tray: saved! thanks for the upload", 165 blocks: [ 166 { 167 type: "section", 168 text: { 169 type: "mrkdwn", 170 text: `:inbox_tray: ${mediaUrls.length > 0 ? "uploaded media and " : ""}saved your notes! That's \`${prettyPrintTime(timeSpent * 1000)}\`!`, 171 }, 172 }, 173 ], 174 }); 175 } catch (error) { 176 console.error("Error handling file message:", error); 177 await slackClient.chat.postMessage({ 178 channel: payload.channel, 179 thread_ts: payload.ts, 180 text: ":warning: there was an error processing your upload", 181 }); 182 183 Sentry.captureException(error, { 184 extra: { 185 channel: payload.channel, 186 user: payload.user, 187 thread_ts: payload.ts, 188 }, 189 tags: { 190 type: "file_upload_error", 191 }, 192 }); 193 } 194 }); 195}