this repo has no description

feat: add basic channel mapping and user mapping

dunkirk.sh b0be7a6e b56446a5

verified
+3 -2
.env.example
···
# Slack Configuration
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here
-
SLACK_CHANNEL=C1234567890
# IRC Configuration
IRC_NICK=slackbridge
-
IRC_CHANNEL=#general
# Admin users (comma-separated Slack user IDs)
ADMINS=U1234567890
# Server Configuration (optional)
PORT=3000
+
+
# Note: Channel and user mappings are now stored in the SQLite database (bridge.db)
+
# Use the API or database tools to manage mappings
+5
.gitignore
···
# Finder (MacOS) folder config
.DS_Store
+
+
# database
+
*.db
+
*.db-shm
+
*.db-wal
+24 -5
README.md
···
# Slack Configuration
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here
-
SLACK_CHANNEL=C1234567890 # Optional: for bidirectional bridging
# IRC Configuration
IRC_NICK=slackbridge
-
IRC_CHANNEL=#general
# Admin users (comma-separated Slack user IDs)
ADMINS=U1234567890
···
See `.env.example` for a template.
+
### Managing Channel and User Mappings
+
+
Channel and user mappings are stored in a SQLite database (`bridge.db`). You can manage them through:
+
+
**Using Bun REPL:**
+
```bash
+
bun repl
+
> import { channelMappings, userMappings } from "./src/db"
+
> channelMappings.create("C1234567890", "#general")
+
> userMappings.create("U1234567890", "myircnick")
+
> channelMappings.getAll()
+
```
+
+
**Using SQLite directly:**
+
```bash
+
bun:sqlite bridge.db
+
sqlite> SELECT * FROM channel_mappings;
+
sqlite> INSERT INTO channel_mappings (slack_channel_id, irc_channel) VALUES ('C1234567890', '#general');
+
```
+
### How it works
-
The bridge connects to `irc.hackclub.com:6667` (no TLS) and forwards messages bidirectionally:
+
The bridge connects to `irc.hackclub.com:6667` (no TLS) and forwards messages bidirectionally based on channel mappings:
-
- **IRC → Slack**: Messages from IRC appear in the configured Slack channel
-
- **Slack → IRC**: Messages from Slack are sent to the IRC channel (if SLACK_CHANNEL is configured)
+
- **IRC → Slack**: Messages from mapped IRC channels appear in their corresponding Slack channels
+
- **Slack → IRC**: Messages from mapped Slack channels are sent to their corresponding IRC channels
+
- User mappings allow custom IRC nicknames for specific Slack users
The bridge ignores its own messages and bot messages to prevent loops.
+9 -102
bun.lock
···
"": {
"name": "irc-slack-bridge",
"dependencies": {
-
"irc-framework": "^4.14.0",
+
"irc": "^0.5.2",
"slack-edge": "^1.3.12",
},
"devDependencies": {
"@types/bun": "latest",
+
"@types/irc": "^0.5.4",
},
"peerDependencies": {
"typescript": "^5",
···
"packages": {
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
-
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
-
-
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
-
-
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
"@types/irc": ["@types/irc@0.5.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-73npDB2rOidw5a30tEmbVZg28bzbfamOa1gK4h5h+6bdSrtVvegchmEKdmsBPTHStjQcCbNqlXS4rRbBUuIrJg=="],
-
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
+
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
-
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
-
-
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
-
-
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
-
-
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
-
-
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
-
-
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
-
-
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
-
-
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
-
-
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
-
-
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
-
-
"fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="],
-
-
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
-
-
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
-
-
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
-
-
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
-
-
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
-
-
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
"iconv": ["iconv@2.2.3", "", { "dependencies": { "nan": "^2.3.5" } }, "sha512-evIiYeKdt5nEGYKNkQcGPQy781sYgbBKi3gEkt1s4CwteCdOHSjGGRyyp6lP8inYFZwvzG3lgjXEvGUC8nqQ5A=="],
-
"grapheme-splitter": ["grapheme-splitter@1.0.4", "", {}, "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="],
-
-
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+
"irc": ["irc@0.5.2", "", { "dependencies": { "irc-colors": "^1.1.0" }, "optionalDependencies": { "iconv": "~2.2.1", "node-icu-charset-detector": "~0.2.0" } }, "sha512-KnrvkV05Y71SWmRWHtnlWEIH7LA/YeDul6l7tncCGLNEw4B6Obtmkatb3ACnSLj0kOJ6UBiuhss9e+eRG3zlxw=="],
-
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
"irc-colors": ["irc-colors@1.5.0", "", {}, "sha512-HtszKchBQTcqw1DC09uD7i7vvMayHGM1OCo6AHt5pkgZEyo99ClhHTMJdf+Ezc9ovuNNxcH89QfyclGthjZJOw=="],
-
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
"nan": ["nan@2.23.1", "", {}, "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw=="],
-
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
-
-
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
-
-
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
-
-
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
-
-
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
-
-
"irc-framework": ["irc-framework@4.14.0", "", { "dependencies": { "buffer": "^6.0.3", "core-js": "^3.38.1", "eventemitter3": "^5.0.1", "grapheme-splitter": "^1.0.4", "iconv-lite": "^0.6.3", "isomorphic-textencoder": "^1.0.1", "lodash": "^4.17.21", "middleware-handler": "^0.2.0", "regenerator-runtime": "^0.14.1", "socks": "^2.8.3", "stream-browserify": "^3.0.0", "util": "^0.12.5" } }, "sha512-lNujDAxy9kcu89WbU5H7IDWly64aD1B9nN9AV5M6btfx88qyQuyH16j1tjS40nmkQH6ld6vvaihKRn9cjk1JrA=="],
-
-
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
-
-
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
-
-
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
-
-
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
-
-
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
-
-
"isomorphic-textencoder": ["isomorphic-textencoder@1.0.1", "", { "dependencies": { "fast-text-encoding": "^1.0.0" } }, "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ=="],
-
-
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
-
-
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
-
-
"middleware-handler": ["middleware-handler@0.2.0", "", {}, "sha512-Qz4B0yWndSokapr3Kl7fpMRysS0DaBlOuATrExFuZbr+oXZ3rsAPufdLe8mUJXiG5A4aJGW6GfKS4PDfQwu7Mg=="],
-
-
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
-
-
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
-
-
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
-
-
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
-
-
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
-
-
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
-
-
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
"node-icu-charset-detector": ["node-icu-charset-detector@0.2.0", "", { "dependencies": { "nan": "^2.3.3" } }, "sha512-DYOFJ3NfKdxEi9hPbmoCss6WydGhJsxpSleUlZfAWEbZt3AU7JuxailgA9tnqQdsHiujfUY9VtDfWD9m0+ThtQ=="],
"slack-edge": ["slack-edge@1.3.12", "", { "dependencies": { "slack-web-api-client": "^1.1.7" } }, "sha512-9+He610rMeEeZawJY2UB4MGqQ4YwqOJXa+g5z0j26JCguFPv3MkByLiYsIf9fJMsCega4863RqzLCQ61AvCPAA=="],
"slack-web-api-client": ["slack-web-api-client@1.1.7", "", {}, "sha512-R36tRp8JcBTHXHMs60sNJTHbUrfj/+5X3ezSNbdoNXTdcRGPM8NZ6ANG3MeZEROxYEOm665Ya7QNfp/xmskQbw=="],
-
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
-
-
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
-
-
"stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="],
-
-
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
-
-
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
-
-
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
-
-
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
}
}
-133
index.ts
···
-
import { SlackApp } from "slack-edge";
-
import { version } from "./package.json";
-
import * as irc from "irc-framework";
-
-
const missingEnvVars = [];
-
if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN");
-
if (!process.env.SLACK_SIGNING_SECRET) missingEnvVars.push("SLACK_SIGNING_SECRET");
-
if (!process.env.ADMINS) missingEnvVars.push("ADMINS");
-
if (!process.env.IRC_NICK) missingEnvVars.push("IRC_NICK");
-
if (!process.env.IRC_CHANNEL) missingEnvVars.push("IRC_CHANNEL");
-
-
if (missingEnvVars.length > 0) {
-
throw new Error(
-
`Missing required environment variables: ${missingEnvVars.join(", ")}`,
-
);
-
}
-
-
const slackApp = new SlackApp({
-
env: {
-
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
-
SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET,
-
SLACK_LOGGING_LEVEL: "INFO",
-
},
-
startLazyListenerAfterAck: true,
-
});
-
const slackClient = slackApp.client;
-
-
// IRC client setup
-
const ircClient = new irc.Client();
-
ircClient.connect({
-
host: "irc.hackclub.com",
-
port: 6667,
-
tls: false,
-
nick: process.env.IRC_NICK,
-
username: process.env.IRC_NICK,
-
gecos: "Slack IRC Bridge",
-
auto_reconnect: true,
-
auto_reconnect_wait: 4000,
-
auto_reconnect_max_retries: 0,
-
});
-
-
const ircChannel = process.env.IRC_CHANNEL;
-
const slackChannel = process.env.SLACK_CHANNEL;
-
-
// IRC event handlers
-
ircClient.on("registered", () => {
-
console.log("Connected to IRC server");
-
ircClient.join(ircChannel);
-
});
-
-
ircClient.on("join", (event) => {
-
if (event.nick === ircClient.user.nick) {
-
console.log(`Joined IRC channel: ${event.channel}`);
-
}
-
});
-
-
ircClient.on("message", async (event) => {
-
if (event.nick === ircClient.user.nick) return;
-
if (event.nick === "****") return;
-
-
if (slackChannel) {
-
try {
-
await slackClient.chat.postMessage({
-
token: process.env.SLACK_BOT_TOKEN,
-
channel: slackChannel,
-
text: event.message,
-
username: event.nick,
-
unfurl_links: false,
-
unfurl_media: false,
-
});
-
} catch (error) {
-
console.error("Error posting to Slack:", error);
-
}
-
}
-
});
-
-
ircClient.on("close", () => {
-
console.log("Disconnected from IRC server");
-
});
-
-
ircClient.on("error", (error) => {
-
console.error("IRC error:", error);
-
});
-
-
// Slack event handlers
-
slackApp.event("message", async ({ payload }) => {
-
if (payload.subtype) return;
-
if (payload.bot_id) return;
-
if (!slackChannel || payload.channel !== slackChannel) return;
-
-
try {
-
const userInfo = await slackClient.users.info({
-
token: process.env.SLACK_BOT_TOKEN,
-
user: payload.user,
-
});
-
-
const username = userInfo.user?.real_name || userInfo.user?.name || "Unknown";
-
const message = `<${username}> ${payload.text}`;
-
-
ircClient.say(ircChannel, message);
-
} catch (error) {
-
console.error("Error handling Slack message:", error);
-
}
-
});
-
-
export default {
-
port: process.env.PORT || 3000,
-
async fetch(request: Request) {
-
const url = new URL(request.url);
-
const path = url.pathname;
-
-
switch (path) {
-
case "/":
-
return new Response(`Hello World from irc-slack-bridge@${version}`);
-
case "/health":
-
return new Response("OK");
-
case "/slack":
-
return slackApp.run(request);
-
default:
-
return new Response("404 Not Found", { status: 404 });
-
}
-
},
-
};
-
-
console.log(
-
`🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
-
);
-
console.log(`Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`);
-
console.log(`IRC Channel: ${ircChannel}`);
-
console.log(`Slack Channel: ${slackChannel || "Not configured (IRC->Slack only)"}`);
-
-
export { slackApp, slackClient, ircClient, version };
-
+6 -5
package.json
···
{
"name": "irc-slack-bridge",
"version": "0.0.1",
-
"module": "index.ts",
+
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
-
"dev": "bun --hot index.ts",
-
"start": "bun index.ts",
+
"dev": "bun --hot src/index.ts",
+
"start": "bun src/index.ts",
"ngrok": "ngrok http 3000 --domain casual-renewing-reptile.ngrok-free.app"
},
"devDependencies": {
-
"@types/bun": "latest"
+
"@types/bun": "latest",
+
"@types/irc": "^0.5.4"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
-
"irc-framework": "^4.14.0",
+
"irc": "^0.5.2",
"slack-edge": "^1.3.12"
}
}
+6
slack-manifest.yaml
···
bot:
- channels:history
- channels:read
+
- channels:write
+
- channels:manage
- chat:write
- chat:write.customize
+
- groups:read
+
- groups:write
+
- mpim:write
+
- im:write
- users:read
settings:
event_subscriptions:
+87
src/db.ts
···
+
import { Database } from "bun:sqlite";
+
+
const db = new Database("bridge.db");
+
+
db.run(`
+
CREATE TABLE IF NOT EXISTS channel_mappings (
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
+
slack_channel_id TEXT NOT NULL UNIQUE,
+
irc_channel TEXT NOT NULL,
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
+
)
+
`);
+
+
db.run(`
+
CREATE TABLE IF NOT EXISTS user_mappings (
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
+
slack_user_id TEXT NOT NULL UNIQUE,
+
irc_nick TEXT NOT NULL,
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
+
)
+
`);
+
+
export interface ChannelMapping {
+
id?: number;
+
slack_channel_id: string;
+
irc_channel: string;
+
created_at?: number;
+
}
+
+
export interface UserMapping {
+
id?: number;
+
slack_user_id: string;
+
irc_nick: string;
+
created_at?: number;
+
}
+
+
export const channelMappings = {
+
getAll(): ChannelMapping[] {
+
return db.query("SELECT * FROM channel_mappings").all() as ChannelMapping[];
+
},
+
+
getBySlackChannel(slackChannelId: string): ChannelMapping | null {
+
return db.query("SELECT * FROM channel_mappings WHERE slack_channel_id = ?").get(slackChannelId) as ChannelMapping | null;
+
},
+
+
getByIrcChannel(ircChannel: string): ChannelMapping | null {
+
return db.query("SELECT * FROM channel_mappings WHERE irc_channel = ?").get(ircChannel) as ChannelMapping | null;
+
},
+
+
create(slackChannelId: string, ircChannel: string): void {
+
db.run(
+
"INSERT OR REPLACE INTO channel_mappings (slack_channel_id, irc_channel) VALUES (?, ?)",
+
[slackChannelId, ircChannel]
+
);
+
},
+
+
delete(slackChannelId: string): void {
+
db.run("DELETE FROM channel_mappings WHERE slack_channel_id = ?", [slackChannelId]);
+
},
+
};
+
+
export const userMappings = {
+
getAll(): UserMapping[] {
+
return db.query("SELECT * FROM user_mappings").all() as UserMapping[];
+
},
+
+
getBySlackUser(slackUserId: string): UserMapping | null {
+
return db.query("SELECT * FROM user_mappings WHERE slack_user_id = ?").get(slackUserId) as UserMapping | null;
+
},
+
+
getByIrcNick(ircNick: string): UserMapping | null {
+
return db.query("SELECT * FROM user_mappings WHERE irc_nick = ?").get(ircNick) as UserMapping | null;
+
},
+
+
create(slackUserId: string, ircNick: string): void {
+
db.run(
+
"INSERT OR REPLACE INTO user_mappings (slack_user_id, irc_nick) VALUES (?, ?)",
+
[slackUserId, ircNick]
+
);
+
},
+
+
delete(slackUserId: string): void {
+
db.run("DELETE FROM user_mappings WHERE slack_user_id = ?", [slackUserId]);
+
},
+
};
+
+
export default db;
+197
src/index.ts
···
+
import * as irc from "irc";
+
import { SlackAPIClient, SlackApp } from "slack-edge";
+
import { version } from "../package.json";
+
import { channelMappings, userMappings } from "./db";
+
+
const missingEnvVars = [];
+
if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN");
+
if (!process.env.SLACK_SIGNING_SECRET)
+
missingEnvVars.push("SLACK_SIGNING_SECRET");
+
if (!process.env.ADMINS) missingEnvVars.push("ADMINS");
+
if (!process.env.IRC_NICK) missingEnvVars.push("IRC_NICK");
+
+
if (missingEnvVars.length > 0) {
+
throw new Error(
+
`Missing required environment variables: ${missingEnvVars.join(", ")}`,
+
);
+
}
+
+
const slackApp = new SlackApp({
+
env: {
+
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN as string,
+
SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string,
+
SLACK_LOGGING_LEVEL: "INFO",
+
},
+
startLazyListenerAfterAck: true,
+
});
+
const slackClient = slackApp.client;
+
+
// Get bot user ID
+
let botUserId: string | undefined;
+
slackClient.auth.test({
+
token: process.env.SLACK_BOT_TOKEN,
+
}).then((result) => {
+
botUserId = result.user_id;
+
console.log(`Bot user ID: ${botUserId}`);
+
});
+
+
// IRC client setup
+
const ircClient = new irc.Client(
+
"irc.hackclub.com",
+
process.env.IRC_NICK || "slackbridge",
+
{
+
port: 6667,
+
autoRejoin: true,
+
autoConnect: true,
+
channels: [],
+
secure: false,
+
userName: process.env.IRC_NICK,
+
realName: "Slack IRC Bridge",
+
},
+
);
+
+
// Join all mapped IRC channels on connect
+
ircClient.addListener("registered", async () => {
+
console.log("Connected to IRC server");
+
const mappings = channelMappings.getAll();
+
for (const mapping of mappings) {
+
ircClient.join(mapping.irc_channel);
+
}
+
});
+
+
ircClient.addListener("join", (channel: string, nick: string) => {
+
if (nick === process.env.IRC_NICK) {
+
console.log(`Joined IRC channel: ${channel}`);
+
}
+
});
+
+
ircClient.addListener(
+
"message",
+
async (nick: string, to: string, text: string) => {
+
if (nick === process.env.IRC_NICK) return;
+
if (nick === "****") return;
+
+
// Find Slack channel mapping for this IRC channel
+
const mapping = channelMappings.getByIrcChannel(to);
+
if (!mapping) return;
+
+
// Check if this IRC nick is mapped to a Slack user
+
const userMapping = userMappings.getByIrcNick(nick);
+
+
const displayName = `${nick} <irc>`;
+
let iconUrl: string | undefined;
+
+
if (userMapping) {
+
try {
+
iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`;
+
} catch (error) {
+
console.error("Error fetching user info:", error);
+
}
+
}
+
+
try {
+
await slackClient.chat.postMessage({
+
token: process.env.SLACK_BOT_TOKEN,
+
channel: mapping.slack_channel_id,
+
text: text,
+
username: displayName,
+
icon_url: iconUrl,
+
unfurl_links: false,
+
unfurl_media: false,
+
});
+
} catch (error) {
+
console.error("Error posting to Slack:", error);
+
}
+
},
+
);
+
+
ircClient.addListener("error", (error: string) => {
+
console.error("IRC error:", error);
+
});
+
+
// Slack event handlers
+
slackApp.event("message", async ({ payload }) => {
+
if (payload.subtype) return;
+
if (payload.bot_id) return;
+
if (payload.user === botUserId) return;
+
+
// Find IRC channel mapping for this Slack channel
+
const mapping = channelMappings.getBySlackChannel(payload.channel);
+
if (!mapping) {
+
console.log(
+
`No IRC channel mapping found for Slack channel ${payload.channel}`,
+
);
+
slackClient.conversations.leave({
+
channel: payload.channel,
+
});
+
return;
+
}
+
+
try {
+
const userInfo = await slackClient.users.info({
+
token: process.env.SLACK_BOT_TOKEN,
+
user: payload.user,
+
});
+
+
// Check for user mapping, otherwise use Slack name
+
const userMapping = userMappings.getBySlackUser(payload.user);
+
const username =
+
userMapping?.irc_nick ||
+
userInfo.user?.real_name ||
+
userInfo.user?.name ||
+
"Unknown";
+
+
// Parse Slack mentions and replace with display names
+
let messageText = payload.text;
+
const mentionRegex = /<@(U[A-Z0-9]+)>/g;
+
const mentions = Array.from(messageText.matchAll(mentionRegex));
+
+
for (const match of mentions) {
+
const userId = match[1];
+
try {
+
const response = await fetch(`https://cachet.dunkirk.sh/users/${userId}`);
+
if (response.ok) {
+
const data = await response.json();
+
messageText = messageText.replace(match[0], `@${data.displayName}`);
+
}
+
} catch (error) {
+
console.error(`Error fetching user ${userId} from cachet:`, error);
+
}
+
}
+
+
const message = `<${username}> ${messageText}`;
+
+
console.log(`Sending to IRC ${mapping.irc_channel}: ${message}`);
+
ircClient.say(mapping.irc_channel, message);
+
} catch (error) {
+
console.error("Error handling Slack message:", error);
+
}
+
});
+
+
export default {
+
port: process.env.PORT || 3000,
+
async fetch(request: Request) {
+
const url = new URL(request.url);
+
const path = url.pathname;
+
+
switch (path) {
+
case "/":
+
return new Response(`Hello World from irc-slack-bridge@${version}`);
+
case "/health":
+
return new Response("OK");
+
case "/slack":
+
return slackApp.run(request);
+
default:
+
return new Response("404 Not Found", { status: 404 });
+
}
+
},
+
};
+
+
console.log(
+
`🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
+
);
+
console.log(
+
`Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`,
+
);
+
console.log(`Channel mappings: ${channelMappings.getAll().length}`);
+
console.log(`User mappings: ${userMappings.getAll().length}`);