this repo has no description

feat: add support for images

dunkirk.sh 0fb23519 1325701d

verified
+119 -12
+3
.env.example
··· 8 8 # Admin users (comma-separated Slack user IDs) 9 9 ADMINS=U1234567890 10 10 11 + # Hack Club CDN Token (for file uploads) 12 + CDN_TOKEN=your-cdn-token-here 13 + 11 14 # Server Configuration (optional) 12 15 PORT=3000 13 16
+20 -1
README.md
··· 37 37 # Admin users (comma-separated Slack user IDs) 38 38 ADMINS=U1234567890 39 39 40 + # Hack Club CDN Token (for file uploads) 41 + CDN_TOKEN=your-cdn-token-here 42 + 40 43 # Server Configuration (optional) 41 44 PORT=3000 42 45 ``` 43 46 44 47 See `.env.example` for a template. 48 + 49 + ### Slash Commands 50 + 51 + The bridge provides interactive slash commands for managing mappings: 52 + 53 + - `/irc-bridge-channel` - Bridge current Slack channel to an IRC channel 54 + - `/irc-unbridge-channel` - Remove bridge from current channel 55 + - `/irc-bridge-user` - Link your Slack account to an IRC nickname 56 + - `/irc-unbridge-user` - Remove your IRC nickname link 57 + - `/irc-bridge-list` - List all channel and user bridges 45 58 46 59 ### Managing Channel and User Mappings 47 60 ··· 68 81 The bridge connects to `irc.hackclub.com:6667` (no TLS) and forwards messages bidirectionally based on channel mappings: 69 82 70 83 - **IRC → Slack**: Messages from mapped IRC channels appear in their corresponding Slack channels 84 + - Image URLs are automatically displayed as inline attachments 85 + - IRC mentions (`@nick` or `nick:`) are converted to Slack mentions for mapped users 86 + - IRC formatting codes are converted to Slack markdown 71 87 - **Slack → IRC**: Messages from mapped Slack channels are sent to their corresponding IRC channels 72 - - User mappings allow custom IRC nicknames for specific Slack users 88 + - Slack mentions are converted to `@displayName` format using Cachet 89 + - Slack markdown is converted to IRC formatting codes 90 + - File attachments are uploaded to Hack Club CDN and URLs are shared 91 + - **User mappings** allow custom IRC nicknames for specific Slack users and enable proper mentions both ways 73 92 74 93 The bridge ignores its own messages and bot messages to prevent loops. 75 94
+1
slack-manifest.yaml
··· 40 40 - chat:write.public 41 41 - chat:write.customize 42 42 - commands 43 + - files:read 43 44 - groups:read 44 45 - groups:write 45 46 - mpim:write
+95 -11
src/index.ts
··· 124 124 iconUrl = getAvatarForNick(nick); 125 125 } 126 126 127 + // Parse IRC mentions and convert to Slack mentions 128 + let messageText = parseIRCFormatting(text); 129 + 130 + // Extract image URLs from the message 131 + const imagePattern = /https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp|svg)(?:\?[^\s]*)?/gi; 132 + const imageUrls = Array.from(messageText.matchAll(imagePattern)); 133 + 134 + // Find all @mentions and nick: mentions in the IRC message 135 + const atMentionPattern = /@(\w+)/g; 136 + const nickMentionPattern = /(\w+):/g; 137 + 138 + const atMentions = Array.from(messageText.matchAll(atMentionPattern)); 139 + const nickMentions = Array.from(messageText.matchAll(nickMentionPattern)); 140 + 141 + for (const match of atMentions) { 142 + const mentionedNick = match[1] as string; 143 + const mentionedUserMapping = userMappings.getByIrcNick(mentionedNick); 144 + if (mentionedUserMapping) { 145 + messageText = messageText.replace(match[0], `<@${mentionedUserMapping.slack_user_id}>`); 146 + } 147 + } 148 + 149 + for (const match of nickMentions) { 150 + const mentionedNick = match[1] as string; 151 + const mentionedUserMapping = userMappings.getByIrcNick(mentionedNick); 152 + if (mentionedUserMapping) { 153 + messageText = messageText.replace(match[0], `<@${mentionedUserMapping.slack_user_id}>:`); 154 + } 155 + } 156 + 127 157 try { 128 - await slackClient.chat.postMessage({ 129 - token: process.env.SLACK_BOT_TOKEN, 130 - channel: mapping.slack_channel_id, 131 - text: parseIRCFormatting(text), 132 - username: displayName, 133 - icon_url: iconUrl, 134 - unfurl_links: false, 135 - unfurl_media: false, 136 - }); 158 + // If there are image URLs, send them as attachments 159 + if (imageUrls.length > 0) { 160 + const attachments = imageUrls.map((match) => ({ 161 + image_url: match[0], 162 + fallback: match[0], 163 + })); 164 + 165 + await slackClient.chat.postMessage({ 166 + token: process.env.SLACK_BOT_TOKEN, 167 + channel: mapping.slack_channel_id, 168 + text: messageText, 169 + username: displayName, 170 + icon_url: iconUrl, 171 + attachments: attachments, 172 + unfurl_links: false, 173 + unfurl_media: false, 174 + }); 175 + } else { 176 + await slackClient.chat.postMessage({ 177 + token: process.env.SLACK_BOT_TOKEN, 178 + channel: mapping.slack_channel_id, 179 + text: messageText, 180 + username: displayName, 181 + icon_url: iconUrl, 182 + unfurl_links: false, 183 + unfurl_media: false, 184 + }); 185 + } 137 186 console.log(`IRC → Slack: <${nick}> ${text}`); 138 187 } catch (error) { 139 188 console.error("Error posting to Slack:", error); ··· 146 195 }); 147 196 148 197 // Slack event handlers 149 - slackApp.event("message", async ({ payload }) => { 150 - if (payload.subtype) return; 198 + slackApp.event("message", async ({ payload, context }) => { 199 + // Ignore bot messages and threaded messages 200 + if (payload.subtype && payload.subtype !== "file_share") return; 151 201 if (payload.bot_id) return; 152 202 if (payload.user === botUserId) return; 153 203 if (payload.thread_ts) return; ··· 205 255 206 256 ircClient.say(mapping.irc_channel, message); 207 257 console.log(`Slack → IRC: ${message}`); 258 + 259 + // Handle file uploads 260 + if (payload.files && payload.files.length > 0) { 261 + try { 262 + // Extract private file URLs 263 + const fileUrls = payload.files.map((file) => file.url_private); 264 + 265 + // Upload to Hack Club CDN 266 + const response = await fetch("https://cdn.hackclub.com/api/v3/new", { 267 + method: "POST", 268 + headers: { 269 + Authorization: `Bearer ${process.env.CDN_TOKEN}`, 270 + "X-Download-Authorization": `Bearer ${process.env.SLACK_BOT_TOKEN}`, 271 + "Content-Type": "application/json", 272 + }, 273 + body: JSON.stringify(fileUrls), 274 + }); 275 + 276 + if (response.ok) { 277 + const data = await response.json(); 278 + 279 + // Send each uploaded file URL to IRC 280 + for (const file of data.files) { 281 + const fileMessage = `<${username}> ${file.deployedUrl}`; 282 + ircClient.say(mapping.irc_channel, fileMessage); 283 + console.log(`Slack → IRC (file): ${fileMessage}`); 284 + } 285 + } else { 286 + console.error("Failed to upload files to CDN:", response.statusText); 287 + } 288 + } catch (error) { 289 + console.error("Error uploading files to CDN:", error); 290 + } 291 + } 208 292 } catch (error) { 209 293 console.error("Error handling Slack message:", error); 210 294 }