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}