this repo has no description

feat: add support for images

dunkirk.sh 0fb23519 1325701d

verified
+3
.env.example
···
# Admin users (comma-separated Slack user IDs)
ADMINS=U1234567890
+
# Hack Club CDN Token (for file uploads)
+
CDN_TOKEN=your-cdn-token-here
+
# Server Configuration (optional)
PORT=3000
+20 -1
README.md
···
# Admin users (comma-separated Slack user IDs)
ADMINS=U1234567890
+
# Hack Club CDN Token (for file uploads)
+
CDN_TOKEN=your-cdn-token-here
+
# Server Configuration (optional)
PORT=3000
```
See `.env.example` for a template.
+
+
### Slash Commands
+
+
The bridge provides interactive slash commands for managing mappings:
+
+
- `/irc-bridge-channel` - Bridge current Slack channel to an IRC channel
+
- `/irc-unbridge-channel` - Remove bridge from current channel
+
- `/irc-bridge-user` - Link your Slack account to an IRC nickname
+
- `/irc-unbridge-user` - Remove your IRC nickname link
+
- `/irc-bridge-list` - List all channel and user bridges
### Managing Channel and User Mappings
···
The bridge connects to `irc.hackclub.com:6667` (no TLS) and forwards messages bidirectionally based on channel mappings:
- **IRC → Slack**: Messages from mapped IRC channels appear in their corresponding Slack channels
+
- Image URLs are automatically displayed as inline attachments
+
- IRC mentions (`@nick` or `nick:`) are converted to Slack mentions for mapped users
+
- IRC formatting codes are converted to Slack markdown
- **Slack → IRC**: Messages from mapped Slack channels are sent to their corresponding IRC channels
-
- User mappings allow custom IRC nicknames for specific Slack users
+
- Slack mentions are converted to `@displayName` format using Cachet
+
- Slack markdown is converted to IRC formatting codes
+
- File attachments are uploaded to Hack Club CDN and URLs are shared
+
- **User mappings** allow custom IRC nicknames for specific Slack users and enable proper mentions both ways
The bridge ignores its own messages and bot messages to prevent loops.
+1
slack-manifest.yaml
···
- chat:write.public
- chat:write.customize
- commands
+
- files:read
- groups:read
- groups:write
- mpim:write
+95 -11
src/index.ts
···
iconUrl = getAvatarForNick(nick);
}
+
// Parse IRC mentions and convert to Slack mentions
+
let messageText = parseIRCFormatting(text);
+
+
// Extract image URLs from the message
+
const imagePattern = /https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp|svg)(?:\?[^\s]*)?/gi;
+
const imageUrls = Array.from(messageText.matchAll(imagePattern));
+
+
// Find all @mentions and nick: mentions in the IRC message
+
const atMentionPattern = /@(\w+)/g;
+
const nickMentionPattern = /(\w+):/g;
+
+
const atMentions = Array.from(messageText.matchAll(atMentionPattern));
+
const nickMentions = Array.from(messageText.matchAll(nickMentionPattern));
+
+
for (const match of atMentions) {
+
const mentionedNick = match[1] as string;
+
const mentionedUserMapping = userMappings.getByIrcNick(mentionedNick);
+
if (mentionedUserMapping) {
+
messageText = messageText.replace(match[0], `<@${mentionedUserMapping.slack_user_id}>`);
+
}
+
}
+
+
for (const match of nickMentions) {
+
const mentionedNick = match[1] as string;
+
const mentionedUserMapping = userMappings.getByIrcNick(mentionedNick);
+
if (mentionedUserMapping) {
+
messageText = messageText.replace(match[0], `<@${mentionedUserMapping.slack_user_id}>:`);
+
}
+
}
+
try {
-
await slackClient.chat.postMessage({
-
token: process.env.SLACK_BOT_TOKEN,
-
channel: mapping.slack_channel_id,
-
text: parseIRCFormatting(text),
-
username: displayName,
-
icon_url: iconUrl,
-
unfurl_links: false,
-
unfurl_media: false,
-
});
+
// If there are image URLs, send them as attachments
+
if (imageUrls.length > 0) {
+
const attachments = imageUrls.map((match) => ({
+
image_url: match[0],
+
fallback: match[0],
+
}));
+
+
await slackClient.chat.postMessage({
+
token: process.env.SLACK_BOT_TOKEN,
+
channel: mapping.slack_channel_id,
+
text: messageText,
+
username: displayName,
+
icon_url: iconUrl,
+
attachments: attachments,
+
unfurl_links: false,
+
unfurl_media: false,
+
});
+
} else {
+
await slackClient.chat.postMessage({
+
token: process.env.SLACK_BOT_TOKEN,
+
channel: mapping.slack_channel_id,
+
text: messageText,
+
username: displayName,
+
icon_url: iconUrl,
+
unfurl_links: false,
+
unfurl_media: false,
+
});
+
}
console.log(`IRC → Slack: <${nick}> ${text}`);
} catch (error) {
console.error("Error posting to Slack:", error);
···
});
// Slack event handlers
-
slackApp.event("message", async ({ payload }) => {
-
if (payload.subtype) return;
+
slackApp.event("message", async ({ payload, context }) => {
+
// Ignore bot messages and threaded messages
+
if (payload.subtype && payload.subtype !== "file_share") return;
if (payload.bot_id) return;
if (payload.user === botUserId) return;
if (payload.thread_ts) return;
···
ircClient.say(mapping.irc_channel, message);
console.log(`Slack → IRC: ${message}`);
+
+
// Handle file uploads
+
if (payload.files && payload.files.length > 0) {
+
try {
+
// Extract private file URLs
+
const fileUrls = payload.files.map((file) => file.url_private);
+
+
// Upload to Hack Club CDN
+
const response = await fetch("https://cdn.hackclub.com/api/v3/new", {
+
method: "POST",
+
headers: {
+
Authorization: `Bearer ${process.env.CDN_TOKEN}`,
+
"X-Download-Authorization": `Bearer ${process.env.SLACK_BOT_TOKEN}`,
+
"Content-Type": "application/json",
+
},
+
body: JSON.stringify(fileUrls),
+
});
+
+
if (response.ok) {
+
const data = await response.json();
+
+
// Send each uploaded file URL to IRC
+
for (const file of data.files) {
+
const fileMessage = `<${username}> ${file.deployedUrl}`;
+
ircClient.say(mapping.irc_channel, fileMessage);
+
console.log(`Slack → IRC (file): ${fileMessage}`);
+
}
+
} else {
+
console.error("Failed to upload files to CDN:", response.statusText);
+
}
+
} catch (error) {
+
console.error("Error uploading files to CDN:", error);
+
}
+
}
} catch (error) {
console.error("Error handling Slack message:", error);
}