hacker news alerts in slack (incessant pings if you make front page)
at space-optimization 145 lines 3.2 kB view raw
1import { slackClient } from "../index"; 2import Bottleneck from "bottleneck"; 3import Queue from "./queue"; 4import colors from "colors"; 5import * as Sentry from "@sentry/bun"; 6import type { 7 ChatPostMessageRequest, 8 ChatPostMessageResponse, 9} from "slack-edge"; 10 11// Create a rate limiter with Bottleneck 12const limiter = new Bottleneck({ 13 minTime: 1000, // 1 second between each request 14}); 15 16const messageQueue = new Queue(); 17 18async function sendMessage( 19 message: ChatPostMessageRequest, 20): Promise<ChatPostMessageResponse> { 21 try { 22 return await limiter.schedule(() => slackClient.chat.postMessage(message)); 23 } catch (error) { 24 Sentry.captureException(error, { 25 extra: { channel: message.channel, text: message.text }, 26 tags: { type: "slack_message_error" }, 27 }); 28 console.error("Failed to send Slack message:", error); 29 throw error; 30 } 31} 32 33async function slog( 34 logMessage: string, 35 location?: { 36 thread_ts?: string; 37 channel: string; 38 }, 39): Promise<void> { 40 try { 41 const channel = location?.channel || process.env.SLACK_LOG_CHANNEL; 42 43 if (!channel) { 44 throw new Error("No Slack channel specified for logging"); 45 } 46 47 const message: ChatPostMessageRequest = { 48 channel, 49 thread_ts: location?.thread_ts, 50 text: logMessage.substring(0, 2500), 51 blocks: [ 52 { 53 type: "section", 54 text: { 55 type: "mrkdwn", 56 text: logMessage 57 .split("\n") 58 .map((a) => `> ${a}`) 59 .join("\n"), 60 }, 61 }, 62 { 63 type: "context", 64 elements: [ 65 { 66 type: "mrkdwn", 67 text: `${new Date().toString()}`, 68 }, 69 ], 70 }, 71 ], 72 }; 73 74 messageQueue.enqueue(() => sendMessage(message)); 75 } catch (error) { 76 Sentry.captureException(error, { 77 extra: { logMessage, location, channel: location?.channel }, 78 tags: { type: "slog_error" }, 79 }); 80 console.error("Failed to queue Slack log message:", error); 81 } 82} 83 84type LogType = "info" | "start" | "cron" | "error"; 85 86type LogMetadata = { 87 error?: Error; 88 context?: string; 89 additional?: Record<string, unknown>; 90}; 91 92export async function clog( 93 logMessage: string, 94 type: LogType, 95 metadata?: LogMetadata, 96): Promise<void> { 97 const timestamp = new Date().toISOString(); 98 const formattedMessage = `[${timestamp}] ${logMessage}`; 99 100 switch (type) { 101 case "info": 102 console.log(colors.blue(formattedMessage)); 103 break; 104 case "start": 105 console.log(colors.green(formattedMessage)); 106 break; 107 case "cron": 108 console.log(colors.magenta(`[CRON]: ${formattedMessage}`)); 109 break; 110 case "error": { 111 const errorMessage = colors.red.bold( 112 `Yo <@S0790GPRA48> deres an error \n\n [ERROR]: ${formattedMessage}`, 113 ); 114 console.error(errorMessage); 115 break; 116 } 117 default: 118 console.log(formattedMessage); 119 } 120} 121 122export async function blog( 123 logMessage: string, 124 type: LogType, 125 location?: { 126 thread_ts?: string; 127 channel: string; 128 }, 129 metadata?: LogMetadata, 130): Promise<void> { 131 try { 132 await Promise.all([ 133 slog(logMessage, location), 134 clog(logMessage, type, metadata), 135 ]); 136 } catch (error) { 137 console.error("Failed to log message:", error); 138 Sentry.captureException(error, { 139 extra: { logMessage, type, location, metadata }, 140 tags: { type: "blog_error" }, 141 }); 142 } 143} 144 145export { clog as default, slog };