this repo has no description
at main 13 kB view raw
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}