1- import { ChatPostMessageResponse , WebClient } from '@slack/web-api' ;
1+ import bolt from '@slack/bolt' ;
2+ import type { App , ExpressReceiver } from '@slack/bolt' ;
23import { HttpException } from '../utils/errors.utils.js' ;
34
4- const slack = new WebClient ( process . env . SLACK_BOT_TOKEN ) ;
5+ const { App : AppClass , ExpressReceiver : ExpressReceiverClass } = bolt ;
6+
7+ let receiver : ExpressReceiver | null = null ;
8+ let slackApp : App | null = null ;
9+ let slack : any = null ; // Type will be inferred from slackApp.client (WebClient from Bolt)
10+
11+ /**
12+ * Initializes the Slack Bolt app, receiver, and client if not already initialized
13+ * Only initializes if SLACK_BOT_TOKEN is present
14+ */
15+ const initializeSlack = ( ) => {
16+ const { SLACK_BOT_TOKEN , SLACK_SIGNING_SECRET } = process . env ;
17+
18+ // Don't initialize if no token is configured (e.g., in tests)
19+ if ( ! SLACK_BOT_TOKEN ) {
20+ return ;
21+ }
22+
23+ // Don't re-initialize if already initialized
24+ if ( slackApp ) {
25+ return ;
26+ }
27+
28+ // Initialize the receiver, app, and client
29+ receiver = new ExpressReceiverClass ( {
30+ signingSecret : SLACK_SIGNING_SECRET || '' ,
31+ endpoints : '/slack/events'
32+ } ) ;
33+
34+ slackApp = new AppClass ( {
35+ token : SLACK_BOT_TOKEN ,
36+ receiver
37+ } ) ;
38+
39+ slack = slackApp . client ;
40+ } ;
41+
42+ /**
43+ * Get the Slack WebClient (initializes Slack if needed)
44+ * @returns the Slack WebClient or null if no token is configured
45+ */
46+ const getSlackClient = ( ) => {
47+ initializeSlack ( ) ;
48+ return slack ;
49+ } ;
550
651/**
752 * Send a slack message
@@ -12,14 +57,13 @@ const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
1257 * @returns the channel id and timestamp of the created slack message
1358 */
1459export const sendMessage = async ( slackId : string , message : string , link ?: string , linkButtonText ?: string ) => {
15- const { SLACK_BOT_TOKEN } = process . env ;
16- if ( ! SLACK_BOT_TOKEN ) return ;
60+ const client = getSlackClient ( ) ;
61+ if ( ! client ) return ;
1762
1863 const block = generateSlackTextBlock ( message , link , linkButtonText ) ;
1964
2065 try {
21- const response : ChatPostMessageResponse = await slack . chat . postMessage ( {
22- token : SLACK_BOT_TOKEN ,
66+ const response = await client . chat . postMessage ( {
2367 channel : slackId ,
2468 text : message ,
2569 blocks : [ block ] ,
@@ -48,14 +92,13 @@ export const replyToMessageInThread = async (
4892 link ?: string ,
4993 linkButtonText ?: string
5094) => {
51- const { SLACK_BOT_TOKEN } = process . env ;
52- if ( ! SLACK_BOT_TOKEN ) return ;
95+ const client = getSlackClient ( ) ;
96+ if ( ! client ) return ;
5397
5498 const block = generateSlackTextBlock ( message , link , linkButtonText ) ;
5599
56100 try {
57- await slack . chat . postMessage ( {
58- token : SLACK_BOT_TOKEN ,
101+ await client . chat . postMessage ( {
59102 channel : slackId ,
60103 thread_ts : parentTimestamp ,
61104 text : message ,
@@ -82,14 +125,13 @@ export const editMessage = async (
82125 link ?: string ,
83126 linkButtonText ?: string
84127) => {
85- const { SLACK_BOT_TOKEN } = process . env ;
86- if ( ! SLACK_BOT_TOKEN ) return ;
128+ const client = getSlackClient ( ) ;
129+ if ( ! client ) return ;
87130
88131 const block = generateSlackTextBlock ( message , link , linkButtonText ) ;
89132
90133 try {
91- await slack . chat . update ( {
92- token : SLACK_BOT_TOKEN ,
134+ await client . chat . update ( {
93135 channel : slackId ,
94136 ts : timestamp ,
95137 text : message ,
@@ -108,12 +150,11 @@ export const editMessage = async (
108150 * @param emoji - the emoji to react with
109151 */
110152export const reactToMessage = async ( slackId : string , parentTimestamp : string , emoji : string ) => {
111- const { SLACK_BOT_TOKEN } = process . env ;
112- if ( ! SLACK_BOT_TOKEN ) return ;
153+ const client = getSlackClient ( ) ;
154+ if ( ! client ) return ;
113155
114156 try {
115- await slack . reactions . add ( {
116- token : SLACK_BOT_TOKEN ,
157+ await client . reactions . add ( {
117158 channel : slackId ,
118159 timestamp : parentTimestamp ,
119160 name : emoji
@@ -165,12 +206,15 @@ const generateSlackTextBlock = (message: string, link?: string, linkButtonText?:
165206 * @returns an array of strings of all the slack ids of the users in the given channel
166207 */
167208export const getUsersInChannel = async ( channelId : string ) => {
209+ const client = getSlackClient ( ) ;
210+ if ( ! client ) return [ ] ;
211+
168212 let members : string [ ] = [ ] ;
169213 let cursor : string | undefined ;
170214
171215 try {
172216 do {
173- const response = await slack . conversations . members ( {
217+ const response = await client . conversations . members ( {
174218 channel : channelId ,
175219 cursor,
176220 limit : 200
@@ -196,8 +240,11 @@ export const getUsersInChannel = async (channelId: string) => {
196240 * @returns the name of the channel or undefined if it cannot be found
197241 */
198242export const getChannelName = async ( channelId : string ) => {
243+ const client = getSlackClient ( ) ;
244+ if ( ! client ) return undefined ;
245+
199246 try {
200- const channelRes = await slack . conversations . info ( { channel : channelId } ) ;
247+ const channelRes = await client . conversations . info ( { channel : channelId } ) ;
201248 return channelRes . channel ?. name ;
202249 } catch ( error ) {
203250 return undefined ;
@@ -210,8 +257,11 @@ export const getChannelName = async (channelId: string) => {
210257 * @returns the name of the user (real name if no display name), undefined if cannot be found
211258 */
212259export const getUserName = async ( userId : string ) => {
260+ const client = getSlackClient ( ) ;
261+ if ( ! client ) return undefined ;
262+
213263 try {
214- const userRes = await slack . users . info ( { user : userId } ) ;
264+ const userRes = await client . users . info ( { user : userId } ) ;
215265 return userRes . user ?. profile ?. display_name || userRes . user ?. real_name ;
216266 } catch ( error ) {
217267 return undefined ;
@@ -223,8 +273,13 @@ export const getUserName = async (userId: string) => {
223273 * @returns the id of the workspace
224274 */
225275export const getWorkspaceId = async ( ) => {
276+ const client = getSlackClient ( ) ;
277+ if ( ! client ) {
278+ throw new HttpException ( 500 , 'Slack client not configured' ) ;
279+ }
280+
226281 try {
227- const response = await slack . auth . test ( ) ;
282+ const response = await client . auth . test ( ) ;
228283 if ( response . ok ) {
229284 return response . team_id ;
230285 }
@@ -234,4 +289,57 @@ export const getWorkspaceId = async () => {
234289 }
235290} ;
236291
237- export default slack ;
292+ /**
293+ * Sends a slack ephemeral message to a user
294+ * @param channelId - the channel id of the channel to send to
295+ * @param threadTs - the timestamp of the thread to send to
296+ * @param userId - the id of the user to send to
297+ * @param text - the text of the message to send (should always be populated in case blocks can't be rendered, but if blocks render text will not)
298+ * @param blocks - the blocks of the message to send
299+ */
300+ export async function sendEphemeralMessage (
301+ channelId : string ,
302+ threadTs : string ,
303+ userId : string ,
304+ text : string ,
305+ blocks : any [ ]
306+ ) {
307+ const client = getSlackClient ( ) ;
308+ if ( ! client ) return ;
309+
310+ try {
311+ await client . chat . postEphemeral ( {
312+ channel : channelId ,
313+ user : userId ,
314+ thread_ts : threadTs ,
315+ text,
316+ blocks
317+ } ) ;
318+ } catch ( err : unknown ) {
319+ if ( err instanceof Error ) {
320+ throw new HttpException ( 500 , `Failed to send slack notifications: ${ err . message } ` ) ;
321+ }
322+ }
323+ }
324+
325+ /**
326+ * Get the Slack Bolt app instance (initializes Slack if needed)
327+ * @returns the Slack Bolt App or null if no token is configured
328+ */
329+ export const getSlackApp = ( ) : App | null => {
330+ initializeSlack ( ) ;
331+ return slackApp ;
332+ } ;
333+
334+ /**
335+ * Get the Express receiver instance (initializes Slack if needed)
336+ * @returns the ExpressReceiver or null if no token is configured
337+ */
338+ export const getReceiver = ( ) : ExpressReceiver | null => {
339+ initializeSlack ( ) ;
340+ return receiver ;
341+ } ;
342+
343+ // Export the getters for any direct usage if needed
344+ export { getSlackClient } ;
345+ export default getSlackClient ;
0 commit comments