this repo has no description
1import type { AnyMessageBlock } from "slack-edge";
2import { ircClient, slackApp } from "./index";
3import { channelMappings, userMappings } from "./lib/db";
4import { canManageChannel } from "./lib/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 // Check if IRC channel is already linked
90 const existingIrcMapping = channelMappings.getByIrcChannel(ircChannel);
91 if (existingIrcMapping) {
92 context.respond({
93 response_type: "ephemeral",
94 text: `❌ IRC channel ${ircChannel} is already bridged to <#${existingIrcMapping.slack_channel_id}>`,
95 replace_original: true,
96 });
97 return;
98 }
99
100 // Check if Slack channel is already linked
101 const existingSlackMapping = channelMappings.getBySlackChannel(slackChannelId);
102 if (existingSlackMapping) {
103 context.respond({
104 response_type: "ephemeral",
105 text: `❌ This channel is already bridged to ${existingSlackMapping.irc_channel}`,
106 replace_original: true,
107 });
108 return;
109 }
110
111 channelMappings.create(slackChannelId, ircChannel);
112 ircClient.join(ircChannel);
113
114 await context.client.conversations.join({
115 channel: slackChannelId,
116 });
117
118 console.log(
119 `Created channel mapping: ${slackChannelId} -> ${ircChannel}`,
120 );
121
122 context.respond({
123 response_type: "ephemeral",
124 text: `✅ Successfully bridged <#${slackChannelId}> to ${ircChannel}`,
125 replace_original: true,
126 });
127 } catch (error) {
128 console.error("Error creating channel mapping:", error);
129 context.respond({
130 response_type: "ephemeral",
131 text: `❌ Failed to bridge channel: ${error}`,
132 replace_original: true,
133 });
134 }
135 });
136
137 // Unlink Slack channel from IRC
138 slackApp.command("/irc-unbridge-channel", async ({ payload, context }) => {
139 const slackChannelId = payload.channel_id;
140 const userId = payload.user_id;
141 const mapping = channelMappings.getBySlackChannel(slackChannelId);
142
143 if (!mapping) {
144 context.respond({
145 response_type: "ephemeral",
146 text: "❌ This channel is not bridged to IRC",
147 });
148 return;
149 }
150
151 // Check if user has permission to manage this channel
152 if (!(await canManageChannel(userId, slackChannelId))) {
153 context.respond({
154 response_type: "ephemeral",
155 text: "❌ You don't have permission to manage this channel. You must be the channel creator, a channel manager, or an admin.",
156 });
157 return;
158 }
159
160 context.respond({
161 response_type: "ephemeral",
162 text: `Are you sure you want to remove the bridge to *${mapping.irc_channel}*?`,
163 blocks: [
164 {
165 type: "section",
166 text: {
167 type: "mrkdwn",
168 text: `Are you sure you want to remove the bridge to *${mapping.irc_channel}*?`,
169 },
170 },
171 {
172 type: "actions",
173 elements: [
174 {
175 type: "button",
176 text: {
177 type: "plain_text",
178 text: "Remove Bridge",
179 },
180 style: "danger",
181 action_id: "unbridge_channel_confirm",
182 value: slackChannelId,
183 },
184 {
185 type: "button",
186 text: {
187 type: "plain_text",
188 text: "Cancel",
189 },
190 action_id: "cancel",
191 },
192 ],
193 },
194 ],
195 });
196 });
197
198 // Handle unbridge confirmation
199 slackApp.action("unbridge_channel_confirm", async ({ payload, context }) => {
200 // @ts-expect-error
201 const slackChannelId = payload.actions?.[0]?.value;
202 if (!context.respond) return;
203
204 try {
205 const mapping = channelMappings.getBySlackChannel(slackChannelId);
206 if (!mapping) {
207 context.respond({
208 response_type: "ephemeral",
209 text: "❌ This channel is not bridged to IRC",
210 replace_original: true,
211 });
212 return;
213 }
214
215 channelMappings.delete(slackChannelId);
216 console.log(
217 `Removed channel mapping: ${slackChannelId} -> ${mapping.irc_channel}`,
218 );
219
220 context.respond({
221 response_type: "ephemeral",
222 text: `✅ Removed bridge to ${mapping.irc_channel}`,
223 replace_original: true,
224 });
225 } catch (error) {
226 console.error("Error removing channel mapping:", error);
227 context.respond({
228 response_type: "ephemeral",
229 text: `❌ Failed to remove bridge: ${error}`,
230 replace_original: true,
231 });
232 }
233 });
234
235 // Link Slack user to IRC nick
236 slackApp.command("/irc-bridge-user", async ({ payload, context }) => {
237 context.respond({
238 response_type: "ephemeral",
239 text: "Enter your IRC nickname",
240 blocks: [
241 {
242 type: "input",
243 block_id: "irc_nick_input",
244 element: {
245 type: "plain_text_input",
246 action_id: "irc_nick",
247 placeholder: {
248 type: "plain_text",
249 text: "myircnick",
250 },
251 },
252 label: {
253 type: "plain_text",
254 text: "IRC Nickname",
255 },
256 },
257 {
258 type: "actions",
259 elements: [
260 {
261 type: "button",
262 text: {
263 type: "plain_text",
264 text: "Link Account",
265 },
266 style: "primary",
267 action_id: "bridge_user_submit",
268 value: payload.user_id,
269 },
270 {
271 type: "button",
272 text: {
273 type: "plain_text",
274 text: "Cancel",
275 },
276 action_id: "cancel",
277 },
278 ],
279 },
280 ],
281 replace_original: true,
282 });
283 });
284
285 // Handle bridge user submission
286 slackApp.action("bridge_user_submit", async ({ payload, context }) => {
287 const stateValues = payload.state?.values;
288 const ircNick = stateValues?.irc_nick_input?.irc_nick?.value;
289 // @ts-expect-error
290 const slackUserId = payload.actions?.[0]?.value;
291 if (!context.respond) {
292 return;
293 }
294
295 if (!ircNick) {
296 context.respond({
297 response_type: "ephemeral",
298 text: "❌ IRC nickname is required",
299 replace_original: true,
300 });
301 return;
302 }
303
304 try {
305 // Check if IRC nick is already linked
306 const existingIrcMapping = userMappings.getByIrcNick(ircNick);
307 if (existingIrcMapping) {
308 context.respond({
309 response_type: "ephemeral",
310 text: `❌ IRC nick *${ircNick}* is already linked to <@${existingIrcMapping.slack_user_id}>`,
311 replace_original: true,
312 });
313 return;
314 }
315
316 // Check if Slack user is already linked
317 const existingSlackMapping = userMappings.getBySlackUser(slackUserId);
318 if (existingSlackMapping) {
319 context.respond({
320 response_type: "ephemeral",
321 text: `❌ You are already linked to IRC nick *${existingSlackMapping.irc_nick}*`,
322 replace_original: true,
323 });
324 return;
325 }
326
327 userMappings.create(slackUserId, ircNick);
328 console.log(`Created user mapping: ${slackUserId} -> ${ircNick}`);
329
330 context.respond({
331 response_type: "ephemeral",
332 text: `✅ Successfully linked your account to IRC nick: *${ircNick}*`,
333 replace_original: true,
334 });
335 } catch (error) {
336 console.error("Error creating user mapping:", error);
337 context.respond({
338 response_type: "ephemeral",
339 text: `❌ Failed to link user: ${error}`,
340 replace_original: true,
341 });
342 }
343 });
344
345 // Unlink Slack user from IRC
346 slackApp.command("/irc-unbridge-user", async ({ payload, context }) => {
347 const slackUserId = payload.user_id;
348 const mapping = userMappings.getBySlackUser(slackUserId);
349
350 if (!mapping) {
351 context.respond({
352 response_type: "ephemeral",
353 text: "❌ You don't have an IRC nick mapping",
354 });
355 return;
356 }
357
358 context.respond({
359 response_type: "ephemeral",
360 text: `Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?`,
361 blocks: [
362 {
363 type: "section",
364 text: {
365 type: "mrkdwn",
366 text: `Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?`,
367 },
368 },
369 {
370 type: "actions",
371 elements: [
372 {
373 type: "button",
374 text: {
375 type: "plain_text",
376 text: "Remove Link",
377 },
378 style: "danger",
379 action_id: "unbridge_user_confirm",
380 value: slackUserId,
381 },
382 {
383 type: "button",
384 text: {
385 type: "plain_text",
386 text: "Cancel",
387 },
388 action_id: "cancel",
389 },
390 ],
391 },
392 ],
393 replace_original: true,
394 });
395 });
396
397 // Handle unbridge user confirmation
398 slackApp.action("unbridge_user_confirm", async ({ payload, context }) => {
399 // @ts-expect-error
400 const slackUserId = payload.actions?.[0]?.value;
401 if (!context.respond) {
402 return;
403 }
404
405 try {
406 const mapping = userMappings.getBySlackUser(slackUserId);
407 if (!mapping) {
408 context.respond({
409 response_type: "ephemeral",
410 text: "❌ You don't have an IRC nick mapping",
411 replace_original: true,
412 });
413 return;
414 }
415
416 userMappings.delete(slackUserId);
417 console.log(
418 `Removed user mapping: ${slackUserId} -> ${mapping.irc_nick}`,
419 );
420
421 context.respond({
422 response_type: "ephemeral",
423 text: `✅ Removed link to IRC nick: ${mapping.irc_nick}`,
424 replace_original: true,
425 });
426 } catch (error) {
427 console.error("Error removing user mapping:", error);
428 context.respond({
429 response_type: "ephemeral",
430 text: `❌ Failed to remove link: ${error}`,
431 replace_original: true,
432 });
433 }
434 });
435
436 // Handle cancel button
437 slackApp.action("cancel", async ({ context }) => {
438 if (!context.respond) return;
439
440 context.respond({
441 response_type: "ephemeral",
442 delete_original: true,
443 });
444 });
445
446 // List channel mappings
447 slackApp.command("/irc-bridge-list", async ({ context }) => {
448 const channelMaps = channelMappings.getAll();
449 const userMaps = userMappings.getAll();
450
451 const blocks: AnyMessageBlock[] = [
452 {
453 type: "header",
454 text: {
455 type: "plain_text",
456 text: "IRC Bridge Status",
457 },
458 },
459 {
460 type: "section",
461 text: {
462 type: "mrkdwn",
463 text: "*Channel Bridges:*",
464 },
465 },
466 ];
467
468 if (channelMaps.length === 0) {
469 blocks.push({
470 type: "section",
471 text: {
472 type: "mrkdwn",
473 text: "_No channel bridges configured_",
474 },
475 });
476 } else {
477 for (const map of channelMaps) {
478 blocks.push({
479 type: "section",
480 text: {
481 type: "mrkdwn",
482 text: `• <#${map.slack_channel_id}> ↔️ *${map.irc_channel}*`,
483 },
484 });
485 }
486 }
487
488 blocks.push(
489 {
490 type: "divider",
491 },
492 {
493 type: "section",
494 text: {
495 type: "mrkdwn",
496 text: "*User Mappings:*",
497 },
498 },
499 );
500
501 if (userMaps.length === 0) {
502 blocks.push({
503 type: "section",
504 text: {
505 type: "mrkdwn",
506 text: "_No user mappings configured_",
507 },
508 });
509 } else {
510 for (const map of userMaps) {
511 blocks.push({
512 type: "section",
513 text: {
514 type: "mrkdwn",
515 text: `• <@${map.slack_user_id}> ↔️ *${map.irc_nick}*`,
516 },
517 });
518 }
519 }
520
521 context.respond({
522 response_type: "ephemeral",
523 text: "IRC mapping list",
524 blocks,
525 replace_original: true,
526 });
527 });
528}