this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: move everything over to modals

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