this repo has no description
1import type { AnyMessageBlock, Block, BlockElement } from "slack-edge";
2import { channelMappings, userMappings } from "./db";
3import { slackApp, ircClient } from "./index";
4import { canManageChannel } from "./permissions";
5
6export function registerCommands() {
7 // Link Slack channel to IRC channel
8 slackApp.command("/irc-bridge-channel", async ({ payload, context }) => {
9 context.respond({
10 response_type: "ephemeral",
11 text: "Bridge channel command received",
12 blocks: [
13 {
14 type: "input",
15 block_id: "irc_channel_input",
16 element: {
17 type: "plain_text_input",
18 action_id: "irc_channel",
19 placeholder: {
20 type: "plain_text",
21 text: "#lounge",
22 },
23 },
24 label: {
25 type: "plain_text",
26 text: "IRC Channel",
27 },
28 },
29 {
30 type: "actions",
31 elements: [
32 {
33 type: "button",
34 text: {
35 type: "plain_text",
36 text: "Bridge Channel",
37 },
38 style: "primary",
39 action_id: "bridge_channel_submit",
40 value: payload.channel_id,
41 },
42 {
43 type: "button",
44 text: {
45 type: "plain_text",
46 text: "Cancel",
47 },
48 action_id: "cancel",
49 },
50 ],
51 },
52 ],
53 replace_original: true,
54 });
55 });
56
57 // Handle bridge channel submission
58 slackApp.action("bridge_channel_submit", async ({ payload, context }) => {
59 const stateValues = payload.state?.values;
60 const ircChannel = stateValues?.irc_channel_input?.irc_channel?.value;
61 // @ts-expect-error
62 const slackChannelId = payload.actions?.[0]?.value;
63 const userId = payload.user?.id;
64
65 if (!context.respond) {
66 return;
67 }
68
69 if (!ircChannel || !ircChannel.startsWith("#")) {
70 context.respond({
71 response_type: "ephemeral",
72 text: "❌ IRC channel must start with #",
73 replace_original: true,
74 });
75 return;
76 }
77
78 // Check if user has permission to manage this channel
79 if (!userId || !(await canManageChannel(userId, slackChannelId))) {
80 context.respond({
81 response_type: "ephemeral",
82 text: "❌ You don't have permission to manage this channel. You must be the channel creator, a channel manager, or an admin.",
83 replace_original: true,
84 });
85 return;
86 }
87
88 try {
89 channelMappings.create(slackChannelId, ircChannel);
90 ircClient.join(ircChannel);
91
92 await context.client.conversations.join({
93 channel: slackChannelId,
94 });
95
96 console.log(
97 `Created channel mapping: ${slackChannelId} -> ${ircChannel}`,
98 );
99
100 context.respond({
101 response_type: "ephemeral",
102 text: `✅ Successfully bridged <#${slackChannelId}> to ${ircChannel}`,
103 replace_original: true,
104 });
105 } catch (error) {
106 console.error("Error creating channel mapping:", error);
107 context.respond({
108 response_type: "ephemeral",
109 text: `❌ Failed to bridge channel: ${error}`,
110 replace_original: true,
111 });
112 }
113 });
114
115 // Unlink Slack channel from IRC
116 slackApp.command("/irc-unbridge-channel", async ({ payload, context }) => {
117 const slackChannelId = payload.channel_id;
118 const userId = payload.user_id;
119 const mapping = channelMappings.getBySlackChannel(slackChannelId);
120
121 if (!mapping) {
122 context.respond({
123 response_type: "ephemeral",
124 text: "❌ This channel is not bridged to IRC",
125 });
126 return;
127 }
128
129 // Check if user has permission to manage this channel
130 if (!(await canManageChannel(userId, slackChannelId))) {
131 context.respond({
132 response_type: "ephemeral",
133 text: "❌ You don't have permission to manage this channel. You must be the channel creator, a channel manager, or an admin.",
134 });
135 return;
136 }
137
138 context.respond({
139 response_type: "ephemeral",
140 text: "Are you sure you want to remove the bridge to *${mapping.irc_channel}*?",
141 blocks: [
142 {
143 type: "section",
144 text: {
145 type: "mrkdwn",
146 text: `Are you sure you want to remove the bridge to *${mapping.irc_channel}*?`,
147 },
148 },
149 {
150 type: "actions",
151 elements: [
152 {
153 type: "button",
154 text: {
155 type: "plain_text",
156 text: "Remove Bridge",
157 },
158 style: "danger",
159 action_id: "unbridge_channel_confirm",
160 value: slackChannelId,
161 },
162 {
163 type: "button",
164 text: {
165 type: "plain_text",
166 text: "Cancel",
167 },
168 action_id: "cancel",
169 },
170 ],
171 },
172 ],
173 });
174 });
175
176 // Handle unbridge confirmation
177 slackApp.action("unbridge_channel_confirm", async ({ payload, context }) => {
178 // @ts-expect-error
179 const slackChannelId = payload.actions?.[0]?.value;
180 if (!context.respond) return;
181
182 try {
183 const mapping = channelMappings.getBySlackChannel(slackChannelId);
184 if (!mapping) {
185 context.respond({
186 response_type: "ephemeral",
187 text: "❌ This channel is not bridged to IRC",
188 replace_original: true,
189 });
190 return;
191 }
192
193 channelMappings.delete(slackChannelId);
194 console.log(
195 `Removed channel mapping: ${slackChannelId} -> ${mapping.irc_channel}`,
196 );
197
198 context.respond({
199 response_type: "ephemeral",
200 text: `✅ Removed bridge to ${mapping.irc_channel}`,
201 replace_original: true,
202 });
203 } catch (error) {
204 console.error("Error removing channel mapping:", error);
205 context.respond({
206 response_type: "ephemeral",
207 text: `❌ Failed to remove bridge: ${error}`,
208 replace_original: true,
209 });
210 }
211 });
212
213 // Link Slack user to IRC nick
214 slackApp.command("/irc-bridge-user", async ({ payload, context }) => {
215 context.respond({
216 response_type: "ephemeral",
217 text: "Enter your IRC nickname",
218 blocks: [
219 {
220 type: "input",
221 block_id: "irc_nick_input",
222 element: {
223 type: "plain_text_input",
224 action_id: "irc_nick",
225 placeholder: {
226 type: "plain_text",
227 text: "myircnick",
228 },
229 },
230 label: {
231 type: "plain_text",
232 text: "IRC Nickname",
233 },
234 },
235 {
236 type: "actions",
237 elements: [
238 {
239 type: "button",
240 text: {
241 type: "plain_text",
242 text: "Link Account",
243 },
244 style: "primary",
245 action_id: "bridge_user_submit",
246 value: payload.user_id,
247 },
248 {
249 type: "button",
250 text: {
251 type: "plain_text",
252 text: "Cancel",
253 },
254 action_id: "cancel",
255 },
256 ],
257 },
258 ],
259 replace_original: true,
260 });
261 });
262
263 // Handle bridge user submission
264 slackApp.action("bridge_user_submit", async ({ payload, context }) => {
265 const stateValues = payload.state?.values;
266 const ircNick = stateValues?.irc_nick_input?.irc_nick?.value;
267 // @ts-expect-error
268 const slackUserId = payload.actions?.[0]?.value;
269 if (!context.respond) {
270 return;
271 }
272
273 if (!ircNick) {
274 context.respond({
275 response_type: "ephemeral",
276 text: "❌ IRC nickname is required",
277 replace_original: true,
278 });
279 return;
280 }
281
282 try {
283 userMappings.create(slackUserId, ircNick);
284 console.log(`Created user mapping: ${slackUserId} -> ${ircNick}`);
285
286 context.respond({
287 response_type: "ephemeral",
288 text: `✅ Successfully linked your account to IRC nick: *${ircNick}*`,
289 replace_original: true,
290 });
291 } catch (error) {
292 console.error("Error creating user mapping:", error);
293 context.respond({
294 response_type: "ephemeral",
295 text: `❌ Failed to link user: ${error}`,
296 replace_original: true,
297 });
298 }
299 });
300
301 // Unlink Slack user from IRC
302 slackApp.command("/irc-unbridge-user", async ({ payload, context }) => {
303 const slackUserId = payload.user_id;
304 const mapping = userMappings.getBySlackUser(slackUserId);
305
306 if (!mapping) {
307 context.respond({
308 response_type: "ephemeral",
309 text: "❌ You don't have an IRC nick mapping",
310 });
311 return;
312 }
313
314 context.respond({
315 response_type: "ephemeral",
316 text: "Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?",
317 blocks: [
318 {
319 type: "section",
320 text: {
321 type: "mrkdwn",
322 text: `Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?`,
323 },
324 },
325 {
326 type: "actions",
327 elements: [
328 {
329 type: "button",
330 text: {
331 type: "plain_text",
332 text: "Remove Link",
333 },
334 style: "danger",
335 action_id: "unbridge_user_confirm",
336 value: slackUserId,
337 },
338 {
339 type: "button",
340 text: {
341 type: "plain_text",
342 text: "Cancel",
343 },
344 action_id: "cancel",
345 },
346 ],
347 },
348 ],
349 replace_original: true,
350 });
351 });
352
353 // Handle unbridge user confirmation
354 slackApp.action("unbridge_user_confirm", async ({ payload, context }) => {
355 // @ts-expect-error
356 const slackUserId = payload.actions?.[0]?.value;
357 if (!context.respond) {
358 return;
359 }
360
361 try {
362 const mapping = userMappings.getBySlackUser(slackUserId);
363 if (!mapping) {
364 context.respond({
365 response_type: "ephemeral",
366 text: "❌ You don't have an IRC nick mapping",
367 replace_original: true,
368 });
369 return;
370 }
371
372 userMappings.delete(slackUserId);
373 console.log(
374 `Removed user mapping: ${slackUserId} -> ${mapping.irc_nick}`,
375 );
376
377 context.respond({
378 response_type: "ephemeral",
379 text: `✅ Removed link to IRC nick: ${mapping.irc_nick}`,
380 replace_original: true,
381 });
382 } catch (error) {
383 console.error("Error removing user mapping:", error);
384 context.respond({
385 response_type: "ephemeral",
386 text: `❌ Failed to remove link: ${error}`,
387 replace_original: true,
388 });
389 }
390 });
391
392 // Handle cancel button
393 slackApp.action("cancel", async ({ context }) => {
394 if (!context.respond) return;
395
396 context.respond({
397 response_type: "ephemeral",
398 delete_original: true,
399 });
400 });
401
402 // List channel mappings
403 slackApp.command("/irc-bridge-list", async ({ payload, context }) => {
404 const channelMaps = channelMappings.getAll();
405 const userMaps = userMappings.getAll();
406
407 const blocks: AnyMessageBlock[] = [
408 {
409 type: "header",
410 text: {
411 type: "plain_text",
412 text: "IRC Bridge Status",
413 },
414 },
415 {
416 type: "section",
417 text: {
418 type: "mrkdwn",
419 text: "*Channel Bridges:*",
420 },
421 },
422 ];
423
424 if (channelMaps.length === 0) {
425 blocks.push({
426 type: "section",
427 text: {
428 type: "mrkdwn",
429 text: "_No channel bridges configured_",
430 },
431 });
432 } else {
433 for (const map of channelMaps) {
434 blocks.push({
435 type: "section",
436 text: {
437 type: "mrkdwn",
438 text: `• <#${map.slack_channel_id}> ↔️ *${map.irc_channel}*`,
439 },
440 });
441 }
442 }
443
444 blocks.push(
445 {
446 type: "divider",
447 },
448 {
449 type: "section",
450 text: {
451 type: "mrkdwn",
452 text: "*User Mappings:*",
453 },
454 },
455 );
456
457 if (userMaps.length === 0) {
458 blocks.push({
459 type: "section",
460 text: {
461 type: "mrkdwn",
462 text: "_No user mappings configured_",
463 },
464 });
465 } else {
466 for (const map of userMaps) {
467 blocks.push({
468 type: "section",
469 text: {
470 type: "mrkdwn",
471 text: `• <@${map.slack_user_id}> ↔️ *${map.irc_nick}*`,
472 },
473 });
474 }
475 }
476
477 context.respond({
478 response_type: "ephemeral",
479 text: "IRC mapping list",
480 blocks,
481 replace_original: true,
482 });
483 });
484}