hacker news alerts in slack (incessant pings if you make front page)
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 };