this repo has no description
1
fork

Configure Feed

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

feat: add basic channel mapping and user mapping

+337 -247
+3 -2
.env.example
··· 1 1 # Slack Configuration 2 2 SLACK_BOT_TOKEN=xoxb-your-bot-token-here 3 3 SLACK_SIGNING_SECRET=your-signing-secret-here 4 - SLACK_CHANNEL=C1234567890 5 4 6 5 # IRC Configuration 7 6 IRC_NICK=slackbridge 8 - IRC_CHANNEL=#general 9 7 10 8 # Admin users (comma-separated Slack user IDs) 11 9 ADMINS=U1234567890 12 10 13 11 # Server Configuration (optional) 14 12 PORT=3000 13 + 14 + # Note: Channel and user mappings are now stored in the SQLite database (bridge.db) 15 + # Use the API or database tools to manage mappings
+5
.gitignore
··· 32 32 33 33 # Finder (MacOS) folder config 34 34 .DS_Store 35 + 36 + # database 37 + *.db 38 + *.db-shm 39 + *.db-wal
+24 -5
README.md
··· 30 30 # Slack Configuration 31 31 SLACK_BOT_TOKEN=xoxb-your-bot-token-here 32 32 SLACK_SIGNING_SECRET=your-signing-secret-here 33 - SLACK_CHANNEL=C1234567890 # Optional: for bidirectional bridging 34 33 35 34 # IRC Configuration 36 35 IRC_NICK=slackbridge 37 - IRC_CHANNEL=#general 38 36 39 37 # Admin users (comma-separated Slack user IDs) 40 38 ADMINS=U1234567890 ··· 45 43 46 44 See `.env.example` for a template. 47 45 46 + ### Managing Channel and User Mappings 47 + 48 + Channel and user mappings are stored in a SQLite database (`bridge.db`). You can manage them through: 49 + 50 + **Using Bun REPL:** 51 + ```bash 52 + bun repl 53 + > import { channelMappings, userMappings } from "./src/db" 54 + > channelMappings.create("C1234567890", "#general") 55 + > userMappings.create("U1234567890", "myircnick") 56 + > channelMappings.getAll() 57 + ``` 58 + 59 + **Using SQLite directly:** 60 + ```bash 61 + bun:sqlite bridge.db 62 + sqlite> SELECT * FROM channel_mappings; 63 + sqlite> INSERT INTO channel_mappings (slack_channel_id, irc_channel) VALUES ('C1234567890', '#general'); 64 + ``` 65 + 48 66 ### How it works 49 67 50 - The bridge connects to `irc.hackclub.com:6667` (no TLS) and forwards messages bidirectionally: 68 + The bridge connects to `irc.hackclub.com:6667` (no TLS) and forwards messages bidirectionally based on channel mappings: 51 69 52 - - **IRC → Slack**: Messages from IRC appear in the configured Slack channel 53 - - **Slack → IRC**: Messages from Slack are sent to the IRC channel (if SLACK_CHANNEL is configured) 70 + - **IRC → Slack**: Messages from mapped IRC channels appear in their corresponding Slack channels 71 + - **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 54 73 55 74 The bridge ignores its own messages and bot messages to prevent loops. 56 75
+9 -102
bun.lock
··· 5 5 "": { 6 6 "name": "irc-slack-bridge", 7 7 "dependencies": { 8 - "irc-framework": "^4.14.0", 8 + "irc": "^0.5.2", 9 9 "slack-edge": "^1.3.12", 10 10 }, 11 11 "devDependencies": { 12 12 "@types/bun": "latest", 13 + "@types/irc": "^0.5.4", 13 14 }, 14 15 "peerDependencies": { 15 16 "typescript": "^5", ··· 19 20 "packages": { 20 21 "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 21 22 22 - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 23 - 24 - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], 25 - 26 - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 23 + "@types/irc": ["@types/irc@0.5.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-73npDB2rOidw5a30tEmbVZg28bzbfamOa1gK4h5h+6bdSrtVvegchmEKdmsBPTHStjQcCbNqlXS4rRbBUuIrJg=="], 27 24 28 - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], 25 + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 29 26 30 27 "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 31 28 32 - "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=="], 33 - 34 - "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=="], 35 - 36 - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 37 - 38 - "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], 39 - 40 - "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=="], 41 - 42 - "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=="], 43 - 44 - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 45 - 46 - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 47 - 48 - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 49 - 50 - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], 51 - 52 - "fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="], 53 - 54 - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], 55 - 56 - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 57 - 58 - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], 59 - 60 - "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=="], 61 - 62 - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 63 - 64 - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 29 + "iconv": ["iconv@2.2.3", "", { "dependencies": { "nan": "^2.3.5" } }, "sha512-evIiYeKdt5nEGYKNkQcGPQy781sYgbBKi3gEkt1s4CwteCdOHSjGGRyyp6lP8inYFZwvzG3lgjXEvGUC8nqQ5A=="], 65 30 66 - "grapheme-splitter": ["grapheme-splitter@1.0.4", "", {}, "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="], 67 - 68 - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], 31 + "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=="], 69 32 70 - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 33 + "irc-colors": ["irc-colors@1.5.0", "", {}, "sha512-HtszKchBQTcqw1DC09uD7i7vvMayHGM1OCo6AHt5pkgZEyo99ClhHTMJdf+Ezc9ovuNNxcH89QfyclGthjZJOw=="], 71 34 72 - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], 35 + "nan": ["nan@2.23.1", "", {}, "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw=="], 73 36 74 - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 75 - 76 - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 77 - 78 - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 79 - 80 - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 81 - 82 - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], 83 - 84 - "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=="], 85 - 86 - "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=="], 87 - 88 - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], 89 - 90 - "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=="], 91 - 92 - "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=="], 93 - 94 - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], 95 - 96 - "isomorphic-textencoder": ["isomorphic-textencoder@1.0.1", "", { "dependencies": { "fast-text-encoding": "^1.0.0" } }, "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ=="], 97 - 98 - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 99 - 100 - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 101 - 102 - "middleware-handler": ["middleware-handler@0.2.0", "", {}, "sha512-Qz4B0yWndSokapr3Kl7fpMRysS0DaBlOuATrExFuZbr+oXZ3rsAPufdLe8mUJXiG5A4aJGW6GfKS4PDfQwu7Mg=="], 103 - 104 - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], 105 - 106 - "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=="], 107 - 108 - "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], 109 - 110 - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 111 - 112 - "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=="], 113 - 114 - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 115 - 116 - "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=="], 37 + "node-icu-charset-detector": ["node-icu-charset-detector@0.2.0", "", { "dependencies": { "nan": "^2.3.3" } }, "sha512-DYOFJ3NfKdxEi9hPbmoCss6WydGhJsxpSleUlZfAWEbZt3AU7JuxailgA9tnqQdsHiujfUY9VtDfWD9m0+ThtQ=="], 117 38 118 39 "slack-edge": ["slack-edge@1.3.12", "", { "dependencies": { "slack-web-api-client": "^1.1.7" } }, "sha512-9+He610rMeEeZawJY2UB4MGqQ4YwqOJXa+g5z0j26JCguFPv3MkByLiYsIf9fJMsCega4863RqzLCQ61AvCPAA=="], 119 40 120 41 "slack-web-api-client": ["slack-web-api-client@1.1.7", "", {}, "sha512-R36tRp8JcBTHXHMs60sNJTHbUrfj/+5X3ezSNbdoNXTdcRGPM8NZ6ANG3MeZEROxYEOm665Ya7QNfp/xmskQbw=="], 121 42 122 - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], 123 - 124 - "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], 125 - 126 - "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], 127 - 128 - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 129 - 130 43 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 131 44 132 45 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 133 - 134 - "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=="], 135 - 136 - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 137 - 138 - "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=="], 139 46 } 140 47 }
-133
index.ts
··· 1 - import { SlackApp } from "slack-edge"; 2 - import { version } from "./package.json"; 3 - import * as irc from "irc-framework"; 4 - 5 - const missingEnvVars = []; 6 - if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN"); 7 - if (!process.env.SLACK_SIGNING_SECRET) missingEnvVars.push("SLACK_SIGNING_SECRET"); 8 - if (!process.env.ADMINS) missingEnvVars.push("ADMINS"); 9 - if (!process.env.IRC_NICK) missingEnvVars.push("IRC_NICK"); 10 - if (!process.env.IRC_CHANNEL) missingEnvVars.push("IRC_CHANNEL"); 11 - 12 - if (missingEnvVars.length > 0) { 13 - throw new Error( 14 - `Missing required environment variables: ${missingEnvVars.join(", ")}`, 15 - ); 16 - } 17 - 18 - const slackApp = new SlackApp({ 19 - env: { 20 - SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN, 21 - SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET, 22 - SLACK_LOGGING_LEVEL: "INFO", 23 - }, 24 - startLazyListenerAfterAck: true, 25 - }); 26 - const slackClient = slackApp.client; 27 - 28 - // IRC client setup 29 - const ircClient = new irc.Client(); 30 - ircClient.connect({ 31 - host: "irc.hackclub.com", 32 - port: 6667, 33 - tls: false, 34 - nick: process.env.IRC_NICK, 35 - username: process.env.IRC_NICK, 36 - gecos: "Slack IRC Bridge", 37 - auto_reconnect: true, 38 - auto_reconnect_wait: 4000, 39 - auto_reconnect_max_retries: 0, 40 - }); 41 - 42 - const ircChannel = process.env.IRC_CHANNEL; 43 - const slackChannel = process.env.SLACK_CHANNEL; 44 - 45 - // IRC event handlers 46 - ircClient.on("registered", () => { 47 - console.log("Connected to IRC server"); 48 - ircClient.join(ircChannel); 49 - }); 50 - 51 - ircClient.on("join", (event) => { 52 - if (event.nick === ircClient.user.nick) { 53 - console.log(`Joined IRC channel: ${event.channel}`); 54 - } 55 - }); 56 - 57 - ircClient.on("message", async (event) => { 58 - if (event.nick === ircClient.user.nick) return; 59 - if (event.nick === "****") return; 60 - 61 - if (slackChannel) { 62 - try { 63 - await slackClient.chat.postMessage({ 64 - token: process.env.SLACK_BOT_TOKEN, 65 - channel: slackChannel, 66 - text: event.message, 67 - username: event.nick, 68 - unfurl_links: false, 69 - unfurl_media: false, 70 - }); 71 - } catch (error) { 72 - console.error("Error posting to Slack:", error); 73 - } 74 - } 75 - }); 76 - 77 - ircClient.on("close", () => { 78 - console.log("Disconnected from IRC server"); 79 - }); 80 - 81 - ircClient.on("error", (error) => { 82 - console.error("IRC error:", error); 83 - }); 84 - 85 - // Slack event handlers 86 - slackApp.event("message", async ({ payload }) => { 87 - if (payload.subtype) return; 88 - if (payload.bot_id) return; 89 - if (!slackChannel || payload.channel !== slackChannel) return; 90 - 91 - try { 92 - const userInfo = await slackClient.users.info({ 93 - token: process.env.SLACK_BOT_TOKEN, 94 - user: payload.user, 95 - }); 96 - 97 - const username = userInfo.user?.real_name || userInfo.user?.name || "Unknown"; 98 - const message = `<${username}> ${payload.text}`; 99 - 100 - ircClient.say(ircChannel, message); 101 - } catch (error) { 102 - console.error("Error handling Slack message:", error); 103 - } 104 - }); 105 - 106 - export default { 107 - port: process.env.PORT || 3000, 108 - async fetch(request: Request) { 109 - const url = new URL(request.url); 110 - const path = url.pathname; 111 - 112 - switch (path) { 113 - case "/": 114 - return new Response(`Hello World from irc-slack-bridge@${version}`); 115 - case "/health": 116 - return new Response("OK"); 117 - case "/slack": 118 - return slackApp.run(request); 119 - default: 120 - return new Response("404 Not Found", { status: 404 }); 121 - } 122 - }, 123 - }; 124 - 125 - console.log( 126 - `🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`, 127 - ); 128 - console.log(`Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`); 129 - console.log(`IRC Channel: ${ircChannel}`); 130 - console.log(`Slack Channel: ${slackChannel || "Not configured (IRC->Slack only)"}`); 131 - 132 - export { slackApp, slackClient, ircClient, version }; 133 -
+6 -5
package.json
··· 1 1 { 2 2 "name": "irc-slack-bridge", 3 3 "version": "0.0.1", 4 - "module": "index.ts", 4 + "module": "src/index.ts", 5 5 "type": "module", 6 6 "private": true, 7 7 "scripts": { 8 - "dev": "bun --hot index.ts", 9 - "start": "bun index.ts", 8 + "dev": "bun --hot src/index.ts", 9 + "start": "bun src/index.ts", 10 10 "ngrok": "ngrok http 3000 --domain casual-renewing-reptile.ngrok-free.app" 11 11 }, 12 12 "devDependencies": { 13 - "@types/bun": "latest" 13 + "@types/bun": "latest", 14 + "@types/irc": "^0.5.4" 14 15 }, 15 16 "peerDependencies": { 16 17 "typescript": "^5" 17 18 }, 18 19 "dependencies": { 19 - "irc-framework": "^4.14.0", 20 + "irc": "^0.5.2", 20 21 "slack-edge": "^1.3.12" 21 22 } 22 23 }
+6
slack-manifest.yaml
··· 11 11 bot: 12 12 - channels:history 13 13 - channels:read 14 + - channels:write 15 + - channels:manage 14 16 - chat:write 15 17 - chat:write.customize 18 + - groups:read 19 + - groups:write 20 + - mpim:write 21 + - im:write 16 22 - users:read 17 23 settings: 18 24 event_subscriptions:
+87
src/db.ts
··· 1 + import { Database } from "bun:sqlite"; 2 + 3 + const db = new Database("bridge.db"); 4 + 5 + db.run(` 6 + CREATE TABLE IF NOT EXISTS channel_mappings ( 7 + id INTEGER PRIMARY KEY AUTOINCREMENT, 8 + slack_channel_id TEXT NOT NULL UNIQUE, 9 + irc_channel TEXT NOT NULL, 10 + created_at INTEGER DEFAULT (strftime('%s', 'now')) 11 + ) 12 + `); 13 + 14 + db.run(` 15 + CREATE TABLE IF NOT EXISTS user_mappings ( 16 + id INTEGER PRIMARY KEY AUTOINCREMENT, 17 + slack_user_id TEXT NOT NULL UNIQUE, 18 + irc_nick TEXT NOT NULL, 19 + created_at INTEGER DEFAULT (strftime('%s', 'now')) 20 + ) 21 + `); 22 + 23 + export interface ChannelMapping { 24 + id?: number; 25 + slack_channel_id: string; 26 + irc_channel: string; 27 + created_at?: number; 28 + } 29 + 30 + export interface UserMapping { 31 + id?: number; 32 + slack_user_id: string; 33 + irc_nick: string; 34 + created_at?: number; 35 + } 36 + 37 + export const channelMappings = { 38 + getAll(): ChannelMapping[] { 39 + return db.query("SELECT * FROM channel_mappings").all() as ChannelMapping[]; 40 + }, 41 + 42 + getBySlackChannel(slackChannelId: string): ChannelMapping | null { 43 + return db.query("SELECT * FROM channel_mappings WHERE slack_channel_id = ?").get(slackChannelId) as ChannelMapping | null; 44 + }, 45 + 46 + getByIrcChannel(ircChannel: string): ChannelMapping | null { 47 + return db.query("SELECT * FROM channel_mappings WHERE irc_channel = ?").get(ircChannel) as ChannelMapping | null; 48 + }, 49 + 50 + create(slackChannelId: string, ircChannel: string): void { 51 + db.run( 52 + "INSERT OR REPLACE INTO channel_mappings (slack_channel_id, irc_channel) VALUES (?, ?)", 53 + [slackChannelId, ircChannel] 54 + ); 55 + }, 56 + 57 + delete(slackChannelId: string): void { 58 + db.run("DELETE FROM channel_mappings WHERE slack_channel_id = ?", [slackChannelId]); 59 + }, 60 + }; 61 + 62 + export const userMappings = { 63 + getAll(): UserMapping[] { 64 + return db.query("SELECT * FROM user_mappings").all() as UserMapping[]; 65 + }, 66 + 67 + getBySlackUser(slackUserId: string): UserMapping | null { 68 + return db.query("SELECT * FROM user_mappings WHERE slack_user_id = ?").get(slackUserId) as UserMapping | null; 69 + }, 70 + 71 + getByIrcNick(ircNick: string): UserMapping | null { 72 + return db.query("SELECT * FROM user_mappings WHERE irc_nick = ?").get(ircNick) as UserMapping | null; 73 + }, 74 + 75 + create(slackUserId: string, ircNick: string): void { 76 + db.run( 77 + "INSERT OR REPLACE INTO user_mappings (slack_user_id, irc_nick) VALUES (?, ?)", 78 + [slackUserId, ircNick] 79 + ); 80 + }, 81 + 82 + delete(slackUserId: string): void { 83 + db.run("DELETE FROM user_mappings WHERE slack_user_id = ?", [slackUserId]); 84 + }, 85 + }; 86 + 87 + export default db;
+197
src/index.ts
··· 1 + import * as irc from "irc"; 2 + import { SlackAPIClient, SlackApp } from "slack-edge"; 3 + import { version } from "../package.json"; 4 + import { channelMappings, userMappings } from "./db"; 5 + 6 + const missingEnvVars = []; 7 + if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN"); 8 + if (!process.env.SLACK_SIGNING_SECRET) 9 + missingEnvVars.push("SLACK_SIGNING_SECRET"); 10 + if (!process.env.ADMINS) missingEnvVars.push("ADMINS"); 11 + if (!process.env.IRC_NICK) missingEnvVars.push("IRC_NICK"); 12 + 13 + if (missingEnvVars.length > 0) { 14 + throw new Error( 15 + `Missing required environment variables: ${missingEnvVars.join(", ")}`, 16 + ); 17 + } 18 + 19 + const slackApp = new SlackApp({ 20 + env: { 21 + SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN as string, 22 + SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string, 23 + SLACK_LOGGING_LEVEL: "INFO", 24 + }, 25 + startLazyListenerAfterAck: true, 26 + }); 27 + const slackClient = slackApp.client; 28 + 29 + // Get bot user ID 30 + let botUserId: string | undefined; 31 + slackClient.auth.test({ 32 + token: process.env.SLACK_BOT_TOKEN, 33 + }).then((result) => { 34 + botUserId = result.user_id; 35 + console.log(`Bot user ID: ${botUserId}`); 36 + }); 37 + 38 + // IRC client setup 39 + const ircClient = new irc.Client( 40 + "irc.hackclub.com", 41 + process.env.IRC_NICK || "slackbridge", 42 + { 43 + port: 6667, 44 + autoRejoin: true, 45 + autoConnect: true, 46 + channels: [], 47 + secure: false, 48 + userName: process.env.IRC_NICK, 49 + realName: "Slack IRC Bridge", 50 + }, 51 + ); 52 + 53 + // Join all mapped IRC channels on connect 54 + ircClient.addListener("registered", async () => { 55 + console.log("Connected to IRC server"); 56 + const mappings = channelMappings.getAll(); 57 + for (const mapping of mappings) { 58 + ircClient.join(mapping.irc_channel); 59 + } 60 + }); 61 + 62 + ircClient.addListener("join", (channel: string, nick: string) => { 63 + if (nick === process.env.IRC_NICK) { 64 + console.log(`Joined IRC channel: ${channel}`); 65 + } 66 + }); 67 + 68 + ircClient.addListener( 69 + "message", 70 + async (nick: string, to: string, text: string) => { 71 + if (nick === process.env.IRC_NICK) return; 72 + if (nick === "****") return; 73 + 74 + // Find Slack channel mapping for this IRC channel 75 + const mapping = channelMappings.getByIrcChannel(to); 76 + if (!mapping) return; 77 + 78 + // Check if this IRC nick is mapped to a Slack user 79 + const userMapping = userMappings.getByIrcNick(nick); 80 + 81 + const displayName = `${nick} <irc>`; 82 + let iconUrl: string | undefined; 83 + 84 + if (userMapping) { 85 + try { 86 + iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`; 87 + } catch (error) { 88 + console.error("Error fetching user info:", error); 89 + } 90 + } 91 + 92 + try { 93 + await slackClient.chat.postMessage({ 94 + token: process.env.SLACK_BOT_TOKEN, 95 + channel: mapping.slack_channel_id, 96 + text: text, 97 + username: displayName, 98 + icon_url: iconUrl, 99 + unfurl_links: false, 100 + unfurl_media: false, 101 + }); 102 + } catch (error) { 103 + console.error("Error posting to Slack:", error); 104 + } 105 + }, 106 + ); 107 + 108 + ircClient.addListener("error", (error: string) => { 109 + console.error("IRC error:", error); 110 + }); 111 + 112 + // Slack event handlers 113 + slackApp.event("message", async ({ payload }) => { 114 + if (payload.subtype) return; 115 + if (payload.bot_id) return; 116 + if (payload.user === botUserId) return; 117 + 118 + // Find IRC channel mapping for this Slack channel 119 + const mapping = channelMappings.getBySlackChannel(payload.channel); 120 + if (!mapping) { 121 + console.log( 122 + `No IRC channel mapping found for Slack channel ${payload.channel}`, 123 + ); 124 + slackClient.conversations.leave({ 125 + channel: payload.channel, 126 + }); 127 + return; 128 + } 129 + 130 + try { 131 + const userInfo = await slackClient.users.info({ 132 + token: process.env.SLACK_BOT_TOKEN, 133 + user: payload.user, 134 + }); 135 + 136 + // Check for user mapping, otherwise use Slack name 137 + const userMapping = userMappings.getBySlackUser(payload.user); 138 + const username = 139 + userMapping?.irc_nick || 140 + userInfo.user?.real_name || 141 + userInfo.user?.name || 142 + "Unknown"; 143 + 144 + // Parse Slack mentions and replace with display names 145 + let messageText = payload.text; 146 + const mentionRegex = /<@(U[A-Z0-9]+)>/g; 147 + const mentions = Array.from(messageText.matchAll(mentionRegex)); 148 + 149 + for (const match of mentions) { 150 + const userId = match[1]; 151 + try { 152 + const response = await fetch(`https://cachet.dunkirk.sh/users/${userId}`); 153 + if (response.ok) { 154 + const data = await response.json(); 155 + messageText = messageText.replace(match[0], `@${data.displayName}`); 156 + } 157 + } catch (error) { 158 + console.error(`Error fetching user ${userId} from cachet:`, error); 159 + } 160 + } 161 + 162 + const message = `<${username}> ${messageText}`; 163 + 164 + console.log(`Sending to IRC ${mapping.irc_channel}: ${message}`); 165 + ircClient.say(mapping.irc_channel, message); 166 + } catch (error) { 167 + console.error("Error handling Slack message:", error); 168 + } 169 + }); 170 + 171 + export default { 172 + port: process.env.PORT || 3000, 173 + async fetch(request: Request) { 174 + const url = new URL(request.url); 175 + const path = url.pathname; 176 + 177 + switch (path) { 178 + case "/": 179 + return new Response(`Hello World from irc-slack-bridge@${version}`); 180 + case "/health": 181 + return new Response("OK"); 182 + case "/slack": 183 + return slackApp.run(request); 184 + default: 185 + return new Response("404 Not Found", { status: 404 }); 186 + } 187 + }, 188 + }; 189 + 190 + console.log( 191 + `🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`, 192 + ); 193 + console.log( 194 + `Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`, 195 + ); 196 + console.log(`Channel mappings: ${channelMappings.getAll().length}`); 197 + console.log(`User mappings: ${userMappings.getAll().length}`);