summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/gateway/bun.lock183
-rw-r--r--packages/gateway/package.json23
-rw-r--r--packages/gateway/src/commands/index.ts8
-rw-r--r--packages/gateway/src/commands/say.ts127
-rw-r--r--packages/gateway/src/commands/start.ts83
-rw-r--r--packages/gateway/src/commands/utilities.ts615
-rw-r--r--packages/gateway/src/constants.ts1
-rw-r--r--packages/gateway/src/index.ts21
-rw-r--r--packages/gateway/src/listeners/announcementReaction.ts17
-rw-r--r--packages/gateway/src/listeners/channelDeletion.ts111
-rw-r--r--packages/gateway/src/listeners/clientReady.ts118
-rw-r--r--packages/gateway/src/listeners/constants.ts1
-rw-r--r--packages/gateway/src/listeners/index.ts20
-rw-r--r--packages/gateway/src/listeners/iqdbModeration.ts210
-rw-r--r--packages/gateway/src/listeners/messageDeletion.ts55
-rw-r--r--packages/gateway/src/listeners/messageEdit.ts86
-rw-r--r--packages/gateway/src/listeners/roleProtection.ts119
-rw-r--r--packages/gateway/src/listeners/roleplayUmagram.ts47
-rw-r--r--packages/gateway/tsconfig.json28
19 files changed, 1873 insertions, 0 deletions
diff --git a/packages/gateway/bun.lock b/packages/gateway/bun.lock
new file mode 100644
index 0000000..8f4bd06
--- /dev/null
+++ b/packages/gateway/bun.lock
@@ -0,0 +1,183 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "umabot-gateway-client",
+ "dependencies": {
+ "discord.js": "^14.14.1",
+ "dotenv": "^16.0.3",
+ "iqdb-client": "^3.0.0",
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "tsx": "^4.7.0",
+ "typescript": "^5.0.0",
+ },
+ },
+ },
+ "packages": {
+ "@discordjs/builders": ["@discordjs/[email protected]", "", { "dependencies": { "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.16", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw=="],
+
+ "@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
+
+ "@discordjs/formatters": ["@discordjs/[email protected]", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="],
+
+ "@discordjs/rest": ["@discordjs/[email protected]", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="],
+
+ "@discordjs/util": ["@discordjs/[email protected]", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
+
+ "@discordjs/ws": ["@discordjs/[email protected]", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
+
+ "@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
+
+ "@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
+
+ "@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
+
+ "@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
+
+ "@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
+
+ "@sapphire/async-queue": ["@sapphire/[email protected]", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
+
+ "@sapphire/shapeshift": ["@sapphire/[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
+
+ "@sapphire/snowflake": ["@sapphire/[email protected]", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
+
+ "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="],
+
+ "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
+
+ "@vladfrangu/async_event_emitter": ["@vladfrangu/[email protected]", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
+
+ "boolbase": ["[email protected]", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+
+ "cheerio": ["[email protected]", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="],
+
+ "cheerio-select": ["[email protected]", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
+
+ "css-select": ["[email protected]", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
+
+ "css-what": ["[email protected]", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
+
+ "discord-api-types": ["[email protected]", "", {}, "sha512-xpmPviHjIJ6dFu1eNwNDIGQ3N6qmPUUYFVAx/YZ64h7ZgPkTcKjnciD8bZe8Vbeji7yS5uYljyciunpq0J5NSw=="],
+
+ "discord.js": ["[email protected]", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="],
+
+ "dom-serializer": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+ "domelementtype": ["[email protected]", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+ "domhandler": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+ "domutils": ["[email protected]", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
+ "dotenv": ["[email protected]", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+
+ "encoding-sniffer": ["[email protected]", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
+
+ "entities": ["[email protected]", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+ "esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
+
+ "fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "get-tsconfig": ["[email protected]", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
+
+ "htmlparser2": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
+
+ "iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+ "iqdb-client": ["[email protected]", "", { "dependencies": { "cheerio": "^1.0.0-rc.12" } }, "sha512-Rr5lNUBeJ663ufMtRhnBVuU0H4HuoyqjdCPz7cbX7oSpIi3KlKqzxYF1xPZWJ/TFtDh4DfM47T57WgFB6wh5DQ=="],
+
+ "lodash": ["[email protected]", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+
+ "lodash.snakecase": ["[email protected]", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
+
+ "magic-bytes.js": ["[email protected]", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
+
+ "nth-check": ["[email protected]", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
+ "parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "parse5-htmlparser2-tree-adapter": ["[email protected]", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
+
+ "parse5-parser-stream": ["[email protected]", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
+
+ "resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
+ "safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "ts-mixer": ["[email protected]", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
+
+ "tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "tsx": ["[email protected]", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="],
+
+ "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
+
+ "undici": ["[email protected]", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
+
+ "undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "whatwg-encoding": ["[email protected]", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
+
+ "whatwg-mimetype": ["[email protected]", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
+ "ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
+ "@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
+
+ "@discordjs/ws/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
+
+ "cheerio/undici": ["[email protected]", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
+
+ "htmlparser2/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+ }
+}
diff --git a/packages/gateway/package.json b/packages/gateway/package.json
new file mode 100644
index 0000000..3ecfdc7
--- /dev/null
+++ b/packages/gateway/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "UmaBotDiscordGateway",
+ "version": "0.1.0",
+ "description": "Official r/okbuddyumamusume Discord Server Gateway Client",
+ "type": "module",
+ "main": "src/index.ts",
+ "scripts": {
+ "start": "tsx src/index.ts",
+ "dev": "tsx watch src/index.ts",
+ "build": "tsc",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "discord.js": "^14.14.1",
+ "dotenv": "^16.0.3",
+ "iqdb-client": "^3.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "tsx": "^4.7.0",
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/packages/gateway/src/commands/index.ts b/packages/gateway/src/commands/index.ts
new file mode 100644
index 0000000..c94db00
--- /dev/null
+++ b/packages/gateway/src/commands/index.ts
@@ -0,0 +1,8 @@
+import { Client } from "discord.js";
+import { handleSayCommand } from "./say";
+import { handleStartCommand } from "./start";
+
+export const handleCommands = (client: Client) => {
+ handleSayCommand(client);
+ handleStartCommand(client);
+};
diff --git a/packages/gateway/src/commands/say.ts b/packages/gateway/src/commands/say.ts
new file mode 100644
index 0000000..4696574
--- /dev/null
+++ b/packages/gateway/src/commands/say.ts
@@ -0,0 +1,127 @@
+import { Client, Events, Message } from "discord.js";
+import { GUILD_ID } from "../constants";
+
+export const handleSayCommand = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ if (message.author.bot) return;
+
+ if (message.content.toLowerCase().startsWith("uma!say")) {
+ const application = await client.application?.fetch();
+ const ownerId = application?.owner?.id;
+
+ if (message.author.id !== ownerId) return;
+
+ const parameters = message.content.split(" ").slice(1);
+
+ if (parameters.length < 2) {
+ await message.reply(
+ "❌ Usage: `uma!say <channel_mention_or_message_id> <message>`\nExamples:\n- `uma!say #general Hello everyone!`\n- `uma!say 1234567890123456789 Thanks for the info!`",
+ );
+
+ return;
+ }
+
+ const firstParameter = parameters[0];
+ const messageContent = parameters.slice(1).join(" ");
+ let targetChannel: any;
+ let targetMessage: any = null;
+ const messageIdMatch = firstParameter.match(/^\d{17,19}$/);
+
+ if (messageIdMatch) {
+ try {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) {
+ await message.reply("❌ Guild not found.");
+
+ return;
+ }
+
+ let foundMessage = null;
+
+ for (const channel of guild.channels.cache.values()) {
+ if (channel.isTextBased()) {
+ try {
+ foundMessage = await channel.messages.fetch(firstParameter);
+
+ if (foundMessage) {
+ targetChannel = channel;
+ targetMessage = foundMessage;
+
+ break;
+ }
+ } catch {
+ continue;
+ }
+ }
+ }
+
+ if (!foundMessage) {
+ await message.reply("❌ Message not found.");
+
+ return;
+ }
+ } catch {
+ await message.reply("❌ Error finding message.");
+
+ return;
+ }
+ } else {
+ const channelMatch = firstParameter.match(/<#(\d+)>/);
+
+ if (!channelMatch) {
+ await message.reply(
+ "❌ Please mention a channel or provide a message ID. Example: `#general` or `1234567890123456789`",
+ );
+
+ return;
+ }
+
+ const channelId = channelMatch[1];
+
+ targetChannel = client.channels.cache.get(channelId);
+
+ if (!targetChannel || !targetChannel.isTextBased()) {
+ await message.reply("❌ Channel not found or is not a text channel.");
+
+ return;
+ }
+ }
+
+ try {
+ await message.delete();
+
+ const baseDuration = Math.max(1, messageContent.length / 20);
+ const complexityMultiplier =
+ (messageContent.match(/[.!?]/g) || []).length * 0.5;
+ const wordCount = messageContent.split(" ").length;
+ const wordComplexityMultiplier = Math.min(wordCount / 10, 2);
+ const typingDuration = Math.min(
+ baseDuration + complexityMultiplier + wordComplexityMultiplier,
+ 8,
+ );
+
+ await (targetChannel as any).sendTyping();
+ await new Promise((resolve) =>
+ setTimeout(resolve, typingDuration * 1000),
+ );
+
+ if (targetMessage) {
+ await targetMessage.reply(messageContent);
+ } else {
+ await (targetChannel as any).send(messageContent);
+ }
+ } catch (error) {
+ console.error("Error executing say command:", error);
+
+ try {
+ await message.reply(
+ "❌ Failed to execute the say command. Please check permissions.",
+ );
+ } catch (replyError) {
+ console.error("Failed to send error reply:", replyError);
+ }
+ }
+ }
+ });
+};
diff --git a/packages/gateway/src/commands/start.ts b/packages/gateway/src/commands/start.ts
new file mode 100644
index 0000000..4a771ed
--- /dev/null
+++ b/packages/gateway/src/commands/start.ts
@@ -0,0 +1,83 @@
+import { Client, Events, Message } from "discord.js";
+import { sendProgressUpdate, executeBulkRoleAssignment } from "./utilities";
+
+export const handleStartCommand = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ if (message.author.bot) return;
+
+ if (message.content.toLowerCase().startsWith("uma!start")) {
+ const application = await client.application?.fetch();
+ const ownerId = application?.owner?.id;
+
+ if (message.author.id !== ownerId) return;
+
+ const parameters = message.content.split(" ").slice(1);
+
+ if (parameters.length < 3) {
+ await message.reply(
+ "❌ Usage: `uma!start <role_mention> <channel_mention_or_category_id> <update_channel_id> [action]`\nExample: `uma!start @Participant #general 1415599617214513254 execute`",
+ );
+
+ return;
+ }
+
+ const roleMention = parameters[0];
+ const channelOrCategory = parameters[1];
+ const updateChannelId = parameters[2];
+ const action = parameters[3] || "execute";
+ const roleMatch = roleMention.match(/<@&(\d+)>/);
+
+ if (!roleMatch) {
+ await message.reply(
+ "❌ Please mention a role. Example: `@Participant`",
+ );
+
+ return;
+ }
+
+ const roleId = roleMatch[1];
+ let channelId: string | undefined;
+ let categoryId: string | undefined;
+ const channelMatch = channelOrCategory.match(/<#(\d+)>/);
+
+ if (channelMatch) {
+ channelId = channelMatch[1];
+ } else {
+ categoryId = channelOrCategory;
+ }
+
+ if (action !== "preview" && action !== "execute") {
+ await message.reply("❌ Action must be either `preview` or `execute`");
+
+ return;
+ }
+
+ if (action === "preview") {
+ await message.reply(
+ "📋 Preview mode - this would check the specified channel(s) for users who have sent messages.",
+ );
+
+ return;
+ }
+
+ await message.reply(
+ "🚀 Bulk role operation started! Check the progress channel for updates.",
+ );
+
+ executeBulkRoleAssignment(
+ client,
+ roleId,
+ updateChannelId,
+ channelId,
+ categoryId,
+ ).catch((error) => {
+ console.error("Bulk role assignment failed:", error);
+ sendProgressUpdate(
+ client,
+ "❌ Bulk role assignment failed due to an error",
+ updateChannelId,
+ );
+ });
+ }
+ });
+};
diff --git a/packages/gateway/src/commands/utilities.ts b/packages/gateway/src/commands/utilities.ts
new file mode 100644
index 0000000..38d7bc2
--- /dev/null
+++ b/packages/gateway/src/commands/utilities.ts
@@ -0,0 +1,615 @@
+import { Client, ChannelType } from "discord.js";
+import { GUILD_ID } from "../constants";
+
+export const AUDIT_LOG_GUILD_ID = "1419211292396224575";
+export const AUDIT_LOG_CHANNEL_ID = "1419211778793144411";
+
+export const sendProgressUpdate = async (
+ client: Client,
+ message: string,
+ channelId: string,
+): Promise<void> => {
+ try {
+ const channel = client.channels.cache.get(channelId);
+
+ if (channel && "send" in channel) await (channel as any).send(message);
+ } catch (error) {
+ console.error("Failed to send progress update:", error);
+ }
+};
+
+export const sendAuditLog = async (
+ client: Client,
+ embed: any,
+ additionalContent?: string,
+ customChannelId?: string,
+): Promise<void> => {
+ try {
+ const channelId = customChannelId || AUDIT_LOG_CHANNEL_ID;
+ const channel = client.channels.cache.get(channelId);
+
+ if (channel && "send" in channel) {
+ await (channel as any).send({ embeds: [embed] });
+
+ if (additionalContent) {
+ const maxLength = 1900;
+ const codeBlockStart = "```\n";
+ const codeBlockEnd = "\n```";
+ const availableLength =
+ maxLength - codeBlockStart.length - codeBlockEnd.length;
+
+ if (additionalContent.length <= availableLength) {
+ await (channel as any).send(
+ `${codeBlockStart}${additionalContent}${codeBlockEnd}`,
+ );
+ } else {
+ const chunks = [];
+ let remaining = additionalContent;
+
+ while (remaining.length > 0) {
+ if (remaining.length <= availableLength) {
+ chunks.push(remaining);
+
+ break;
+ }
+
+ let breakPoint = availableLength;
+ const lastNewline = remaining.lastIndexOf("\n", availableLength);
+ const lastSpace = remaining.lastIndexOf(" ", availableLength);
+
+ if (lastNewline > availableLength * 0.8) {
+ breakPoint = lastNewline;
+ } else if (lastSpace > availableLength * 0.8) {
+ breakPoint = lastSpace;
+ }
+
+ chunks.push(remaining.substring(0, breakPoint));
+
+ remaining = remaining.substring(breakPoint).trim();
+ }
+
+ for (let i = 0; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ const header =
+ chunks.length > 1 ? `Part ${i + 1}/${chunks.length}:\n` : "";
+
+ await (channel as any).send(
+ `${codeBlockStart}${header}${chunk}${codeBlockEnd}`,
+ );
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Failed to send audit log:", error);
+ }
+};
+
+export const getChannelName = (channel: any): string => {
+ return channel.name || channel.id || "Unknown Channel";
+};
+
+export const fetchMessagesFromChannel = async (
+ client: Client,
+ channel: any,
+ userIds: Set<string>,
+ updateChannelId: string,
+): Promise<{ messageCount: number; batchCount: number }> => {
+ let lastMessageId: string | undefined;
+ let hasMoreMessages = true;
+ let messageCount = 0;
+ let batchCount = 0;
+
+ while (hasMoreMessages) {
+ try {
+ const messages = await channel.messages.fetch({
+ limit: 100,
+ before: lastMessageId,
+ });
+
+ if (messages.size === 0) {
+ hasMoreMessages = false;
+
+ break;
+ }
+
+ for (const message of messages.values())
+ if (message.author && message.author.id) userIds.add(message.author.id);
+
+ messageCount += messages.size;
+ batchCount += 1;
+
+ if (batchCount % 10 === 0)
+ await sendProgressUpdate(
+ client,
+ `📊 Progress for ${getChannelName(channel)}: ${messageCount} messages processed, ${userIds.size} unique users found`,
+ updateChannelId,
+ );
+
+ lastMessageId = messages.last()?.id;
+
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ } catch (error) {
+ console.error(
+ `Error fetching messages from channel ${channel.id}:`,
+ error,
+ );
+
+ hasMoreMessages = false;
+ }
+ }
+
+ return { messageCount, batchCount };
+};
+
+export const fetchForumPosts = async (
+ client: Client,
+ forumChannel: any,
+ userIds: Set<string>,
+ updateChannelId: string,
+): Promise<{ messageCount: number; batchCount: number }> => {
+ let totalMessageCount = 0;
+ let totalBatchCount = 0;
+
+ try {
+ const threads = await forumChannel.threads.fetchActive();
+ const archivedThreads = await forumChannel.threads.fetchArchived();
+
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ await sendProgressUpdate(
+ client,
+ `🔍 Found ${allThreads.length} forum posts in ${getChannelName(forumChannel)}`,
+ updateChannelId,
+ );
+
+ for (const thread of allThreads) {
+ try {
+ const { messageCount, batchCount } = await fetchMessagesFromChannel(
+ client,
+ thread,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += messageCount;
+ totalBatchCount += batchCount;
+
+ await sendProgressUpdate(
+ client,
+ `📝 Processed forum post "${getChannelName(thread)}": ${messageCount} messages`,
+ updateChannelId,
+ );
+ } catch (error) {
+ console.error(`Error processing forum post ${thread.id}:`, error);
+ }
+ }
+ } catch (error) {
+ console.error(`Error fetching forum posts from ${forumChannel.id}:`, error);
+ }
+
+ return { messageCount: totalMessageCount, batchCount: totalBatchCount };
+};
+
+export const fetchChannelThreads = async (
+ client: Client,
+ textChannel: any,
+ userIds: Set<string>,
+ updateChannelId: string,
+): Promise<{ messageCount: number; batchCount: number }> => {
+ let totalMessageCount = 0;
+ let totalBatchCount = 0;
+
+ try {
+ const threads = await textChannel.threads.fetchActive();
+ const archivedThreads = await textChannel.threads.fetchArchived();
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ if (allThreads.length > 0) {
+ await sendProgressUpdate(
+ client,
+ `🔍 Found ${allThreads.length} threads in ${getChannelName(textChannel)}`,
+ updateChannelId,
+ );
+
+ for (const thread of allThreads) {
+ try {
+ const { messageCount, batchCount } = await fetchMessagesFromChannel(
+ client,
+ thread,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += messageCount;
+ totalBatchCount += batchCount;
+
+ await sendProgressUpdate(
+ client,
+ `🧵 Processed thread "${getChannelName(thread)}": ${messageCount} messages`,
+ updateChannelId,
+ );
+ } catch (error) {
+ console.error(`Error processing thread ${thread.id}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error(`Error fetching threads from ${textChannel.id}:`, error);
+ }
+
+ return { messageCount: totalMessageCount, batchCount: totalBatchCount };
+};
+
+export const fetchMessageAuthors = async (
+ client: Client,
+ channelId: string,
+ channelName: string | undefined,
+ updateChannelId: string,
+): Promise<Set<string>> => {
+ const userIds = new Set<string>();
+ let totalMessageCount = 0;
+
+ await sendProgressUpdate(
+ client,
+ `🔍 Starting comprehensive analysis of ${channelName || `channel ${channelId}`} ...`,
+ updateChannelId,
+ );
+
+ const channel = client.channels.cache.get(channelId);
+
+ if (!channel) {
+ await sendProgressUpdate(
+ client,
+ `❌ Channel ${channelId} not found`,
+ updateChannelId,
+ );
+
+ return userIds;
+ }
+
+ const channelDisplayName = channelName || getChannelName(channel);
+
+ switch (channel.type) {
+ case ChannelType.GuildText: {
+ await sendProgressUpdate(
+ client,
+ `📝 Processing text channel: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: channelMessages } = await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += channelMessages;
+
+ const { messageCount: threadMessages } = await fetchChannelThreads(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += threadMessages;
+
+ break;
+ }
+
+ case ChannelType.GuildForum: {
+ await sendProgressUpdate(
+ client,
+ `📋 Processing forum channel: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: forumMessages } = await fetchForumPosts(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += forumMessages;
+
+ break;
+ }
+
+ case ChannelType.PublicThread:
+ case ChannelType.PrivateThread:
+ case ChannelType.AnnouncementThread: {
+ await sendProgressUpdate(
+ client,
+ `🧵 Processing thread: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: threadMessageCount } =
+ await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += threadMessageCount;
+
+ break;
+ }
+
+ case ChannelType.GuildAnnouncement: {
+ await sendProgressUpdate(
+ client,
+ `📢 Processing announcement channel: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: announcementMessages } =
+ await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += announcementMessages;
+
+ const { messageCount: announcementThreadMessages } =
+ await fetchChannelThreads(client, channel, userIds, updateChannelId);
+
+ totalMessageCount += announcementThreadMessages;
+
+ break;
+ }
+
+ case ChannelType.GuildVoice:
+ case ChannelType.GuildStageVoice: {
+ await sendProgressUpdate(
+ client,
+ `🔇 Voice channel ${channelDisplayName} has no text messages`,
+ updateChannelId,
+ );
+
+ break;
+ }
+
+ default:
+ await sendProgressUpdate(
+ client,
+ `❓ Unknown channel type for ${channelDisplayName}, attempting to fetch messages ...`,
+ updateChannelId,
+ );
+
+ if (channel.isTextBased()) {
+ const { messageCount: unknownMessages } =
+ await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += unknownMessages;
+ }
+
+ break;
+ }
+
+ await sendProgressUpdate(
+ client,
+ `✅ Completed comprehensive analysis of ${channelDisplayName}: ${totalMessageCount} messages processed, ${userIds.size} unique users found`,
+ updateChannelId,
+ );
+
+ return userIds;
+};
+
+export const getAllChannelsInCategory = async (
+ client: Client,
+ categoryId: string,
+): Promise<{ channels: string[]; channelNames: { [key: string]: string } }> => {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) return { channels: [], channelNames: {} };
+
+ const channels: string[] = [];
+ const channelNames: { [key: string]: string } = {};
+
+ const categoryChannels = guild.channels.cache.filter(
+ (channel) => channel.parentId === categoryId,
+ );
+
+ for (const channel of categoryChannels.values()) {
+ channels.push(channel.id);
+
+ channelNames[channel.id] = `#${getChannelName(channel)}`;
+
+ if (
+ channel.type === ChannelType.GuildText ||
+ channel.type === ChannelType.GuildAnnouncement
+ ) {
+ try {
+ const threads = await channel.threads.fetchActive();
+ const archivedThreads = await channel.threads.fetchArchived();
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ for (const thread of allThreads) {
+ channels.push(thread.id);
+
+ channelNames[thread.id] = `🧵 ${getChannelName(thread)}`;
+ }
+ } catch (error) {
+ console.error(
+ `Error fetching threads for channel ${channel.id}:`,
+ error,
+ );
+ }
+ }
+
+ if (channel.type === ChannelType.GuildForum) {
+ try {
+ const threads = await channel.threads.fetchActive();
+ const archivedThreads = await channel.threads.fetchArchived();
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ for (const thread of allThreads) {
+ channels.push(thread.id);
+
+ channelNames[thread.id] = `📝 ${getChannelName(thread)}`;
+ }
+ } catch (error) {
+ console.error(
+ `Error fetching forum posts for channel ${channel.id}:`,
+ error,
+ );
+ }
+ }
+ }
+
+ return { channels, channelNames };
+};
+
+export const executeBulkRoleAssignment = async (
+ client: Client,
+ roleId: string,
+ customUpdateChannelId: string,
+ channelId?: string,
+ categoryId?: string,
+): Promise<void> => {
+ try {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) {
+ await sendProgressUpdate(
+ client,
+ "❌ Guild not found",
+ customUpdateChannelId,
+ );
+
+ return;
+ }
+
+ const role = guild.roles.cache.get(roleId);
+
+ if (!role) {
+ await sendProgressUpdate(
+ client,
+ "❌ Role not found",
+ customUpdateChannelId,
+ );
+
+ return;
+ }
+
+ let channelsToCheck: string[] = [];
+ let channelNames: { [key: string]: string } = {};
+
+ if (channelId) {
+ channelsToCheck = [channelId];
+
+ const channel = guild.channels.cache.get(channelId);
+
+ if (channel) channelNames[channelId] = `#${getChannelName(channel)}`;
+ } else if (categoryId) {
+ const result = await getAllChannelsInCategory(client, categoryId);
+
+ channelsToCheck = result.channels;
+ channelNames = result.channelNames;
+ }
+
+ await sendProgressUpdate(
+ client,
+ `🚀 Starting comprehensive bulk role operation: ${channelsToCheck.length} channel(s)/thread(s) to process`,
+ customUpdateChannelId,
+ );
+
+ const userIds = new Set<string>();
+
+ for (const channelId of channelsToCheck) {
+ const channelUserIds = await fetchMessageAuthors(
+ client,
+ channelId,
+ channelNames[channelId],
+ customUpdateChannelId,
+ );
+
+ channelUserIds.forEach((userId) => userIds.add(userId));
+ }
+
+ const uniqueUserIds = Array.from(userIds);
+
+ await sendProgressUpdate(
+ client,
+ `🎯 Starting role assignment phase: ${uniqueUserIds.length} users to process\n📝 Note: Some users may have left the guild since sending messages`,
+ customUpdateChannelId,
+ );
+
+ let successCount = 0;
+ let errorCount = 0;
+ let notInGuildCount = 0;
+
+ for (let i = 0; i < uniqueUserIds.length; i++) {
+ const userId = uniqueUserIds[i];
+
+ try {
+ const member = await guild.members.fetch(userId);
+ const currentRoles = member.roles.cache.map((role) => role.id);
+
+ if (!currentRoles.includes(roleId)) {
+ await member.roles.add(role);
+
+ successCount += 1;
+ } else {
+ successCount += 1;
+ }
+
+ if ((i + 1) % 25 === 0)
+ await sendProgressUpdate(
+ client,
+ `📈 Role assignment progress: ${i + 1}/${uniqueUserIds.length} users processed (${successCount} successful, ${errorCount} errors, ${notInGuildCount} not in guild)`,
+ customUpdateChannelId,
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ } catch (error: any) {
+ console.error(`Error processing user ${userId}:`, error);
+
+ if (error.code === 10007 || error.message?.includes("Unknown Member")) {
+ notInGuildCount += 1;
+
+ console.log(`User ${userId} is no longer in the guild, skipping ...`);
+ } else {
+ errorCount += 1;
+ }
+ }
+ }
+
+ await sendProgressUpdate(
+ client,
+ `🏁 Bulk role operation completed!\n\n📊 **Final Results:**\n- ✅ **${successCount}** users processed successfully\n- ❌ **${errorCount}** errors occurred\n- 🚪 **${notInGuildCount}** users no longer in guild\n\n💡 **Note:** The "successful" count includes users who already had the role. Users who left the guild are automatically skipped.`,
+ customUpdateChannelId,
+ );
+ } catch (error) {
+ console.error("Error in bulk role assignment:", error);
+ await sendProgressUpdate(
+ client,
+ "❌ An error occurred during bulk role assignment",
+ customUpdateChannelId,
+ );
+ }
+};
diff --git a/packages/gateway/src/constants.ts b/packages/gateway/src/constants.ts
new file mode 100644
index 0000000..77ef6cd
--- /dev/null
+++ b/packages/gateway/src/constants.ts
@@ -0,0 +1 @@
+export const GUILD_ID = "1406422617724026901";
diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts
new file mode 100644
index 0000000..1e1184a
--- /dev/null
+++ b/packages/gateway/src/index.ts
@@ -0,0 +1,21 @@
+import { GatewayIntentBits, Client } from "discord.js";
+import dotenv from "dotenv";
+import { handleCommands } from "./commands";
+import { handleListeners } from "./listeners";
+
+dotenv.config({ path: "../../.dev.vars" });
+console.log("Discord Token loaded:", process.env.DISCORD_TOKEN ? "Yes" : "No");
+console.log("Token length:", process.env.DISCORD_TOKEN?.length || 0);
+
+const client = new Client({
+ intents: [
+ GatewayIntentBits.Guilds,
+ GatewayIntentBits.GuildMembers,
+ GatewayIntentBits.GuildMessages,
+ GatewayIntentBits.MessageContent,
+ ],
+});
+
+handleCommands(client);
+handleListeners(client);
+client.login(process.env.DISCORD_TOKEN);
diff --git a/packages/gateway/src/listeners/announcementReaction.ts b/packages/gateway/src/listeners/announcementReaction.ts
new file mode 100644
index 0000000..eba8f25
--- /dev/null
+++ b/packages/gateway/src/listeners/announcementReaction.ts
@@ -0,0 +1,17 @@
+import { Client, Events, Message } from "discord.js";
+
+const ANNOUNCEMENT_CHANNEL_ID = "1406591215608270981";
+
+export const handleAnnouncementReaction = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ if (message.channelId !== ANNOUNCEMENT_CHANNEL_ID) return;
+
+ if (message.author.id === client.user?.id) return;
+
+ try {
+ await message.react("1406426721158303864");
+ } catch (error) {
+ console.error("Failed to add okbuddy reaction to announcement:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/channelDeletion.ts b/packages/gateway/src/listeners/channelDeletion.ts
new file mode 100644
index 0000000..36fef79
--- /dev/null
+++ b/packages/gateway/src/listeners/channelDeletion.ts
@@ -0,0 +1,111 @@
+import { Client, Events } from "discord.js";
+import { GUILD_ID } from "../constants";
+
+const channelDeletionTracker = new Map<
+ string,
+ { count: number; firstDeletion: number }
+>();
+
+export const handleChannelDeletion = (client: Client) => {
+ client.on(Events.ChannelDelete, async (deletedChannel) => {
+ if (
+ !("guildId" in deletedChannel) ||
+ !deletedChannel.guildId ||
+ deletedChannel.guildId !== GUILD_ID
+ )
+ return;
+
+ try {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) return;
+
+ const guildOwner = await guild.fetchOwner();
+ const auditLogs = await guild.fetchAuditLogs({
+ type: 12,
+ limit: 5,
+ });
+ const channelDeletionLog = auditLogs.entries.find(
+ (entry) => entry.target?.id === deletedChannel.id,
+ );
+
+ if (!channelDeletionLog || !channelDeletionLog.executor) {
+ console.log("Could not determine who deleted channel, skipping...");
+
+ return;
+ }
+
+ const executor = channelDeletionLog.executor;
+
+ if (executor.id === guildOwner.id) {
+ console.log(`Channel deleted by server owner, allowing...`);
+
+ return;
+ }
+
+ const now = Date.now();
+ const thirtySeconds = 30 * 1000;
+ let userData = channelDeletionTracker.get(executor.id);
+
+ if (!userData) {
+ userData = { count: 1, firstDeletion: now };
+
+ channelDeletionTracker.set(executor.id, userData);
+ console.log(
+ `User ${executor.tag} (${executor.id}) deleted first channel`,
+ );
+ } else {
+ if (now - userData.firstDeletion <= thirtySeconds) {
+ userData.count += 1;
+
+ console.log(
+ `User ${executor.tag} (${executor.id}) deleted channel ${userData.count}/2 within 30 seconds`,
+ );
+
+ if (userData.count > 2) {
+ console.log(
+ `User ${executor.tag} (${executor.id}) exceeded channel deletion limit, resetting roles...`,
+ );
+
+ try {
+ const member = await guild.members.fetch(executor.id);
+
+ if (member) {
+ const rolesToRemove = member.roles.cache.filter(
+ (role) => role.id !== guild.id,
+ );
+
+ if (rolesToRemove.size > 0) {
+ await member.roles.set([]);
+ console.log(
+ `Reset ${rolesToRemove.size} roles for user ${executor.tag}`,
+ );
+ }
+ }
+ } catch (error) {
+ console.error(
+ `Failed to reset roles for user ${executor.tag}:`,
+ error,
+ );
+ }
+
+ channelDeletionTracker.delete(executor.id);
+ }
+ } else {
+ userData.count = 1;
+ userData.firstDeletion = now;
+
+ console.log(
+ `User ${executor.tag} (${executor.id}) deleted channel, resetting counter (outside 30s window)`,
+ );
+ }
+ }
+
+ for (const [userId, data] of channelDeletionTracker.entries())
+ if (now - data.firstDeletion > thirtySeconds)
+ channelDeletionTracker.delete(userId);
+ } catch (error) {
+ console.error("Error in channel deletion monitoring:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/clientReady.ts b/packages/gateway/src/listeners/clientReady.ts
new file mode 100644
index 0000000..2933916
--- /dev/null
+++ b/packages/gateway/src/listeners/clientReady.ts
@@ -0,0 +1,118 @@
+import { Client, Events } from "discord.js";
+import { ROLEPLAY_UMAGRAM_CHANNEL_ID } from "./constants";
+
+export const handleClientReady = (client: Client) => {
+ client.once(Events.ClientReady, async (readyClient) => {
+ console.log(`Gateway client ready! Logged in as ${readyClient.user.tag}`);
+
+ await readyClient.user.setActivity("r/okbuddyumamusume", { type: 3 });
+
+ try {
+ const channel = client.channels.cache.get(ROLEPLAY_UMAGRAM_CHANNEL_ID);
+
+ if (channel && channel.isTextBased()) {
+ console.log(
+ "Adding hearts to existing messages in roleplay-umagram channel and threads...",
+ );
+
+ let totalMessageCount = 0;
+ let totalHeartCount = 0;
+ const processChannelMessages = async (
+ targetChannel: any,
+ channelName: string,
+ ) => {
+ let lastMessageId: string | undefined;
+ let messageCount = 0;
+ let heartCount = 0;
+
+ while (true) {
+ const messages = await targetChannel.messages.fetch({
+ limit: 100,
+ before: lastMessageId,
+ });
+
+ if (messages.size === 0) break;
+
+ for (const message of messages.values()) {
+ messageCount += 1;
+
+ const existingReaction = message.reactions.cache.get("❤️");
+
+ if (
+ existingReaction &&
+ existingReaction.users.cache.has(client.user!.id)
+ )
+ continue;
+
+ try {
+ await message.react("❤️");
+
+ heartCount += 1;
+
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ } catch (error) {
+ console.error(
+ `Failed to heart message ${message.id} in ${channelName}:`,
+ error,
+ );
+ }
+ }
+
+ lastMessageId = messages.last()?.id;
+
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+
+ console.log(
+ `${channelName}: Processed ${messageCount} messages, added ${heartCount} hearts`,
+ );
+
+ return { messageCount, heartCount };
+ };
+
+ const mainResult = await processChannelMessages(
+ channel,
+ "Main Channel",
+ );
+
+ totalMessageCount += mainResult.messageCount;
+ totalHeartCount += mainResult.heartCount;
+
+ try {
+ const activeThreads = await (channel as any).threads?.fetchActive();
+ const archivedThreads = await (
+ channel as any
+ ).threads?.fetchArchived();
+ const allThreads = [
+ ...activeThreads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ console.log(`Found ${allThreads.length} threads to process`);
+
+ for (const thread of allThreads) {
+ try {
+ const threadResult = await processChannelMessages(
+ thread,
+ `Thread: ${thread.name}`,
+ );
+
+ totalMessageCount += threadResult.messageCount;
+ totalHeartCount += threadResult.heartCount;
+ } catch (error) {
+ console.error(`Error processing thread ${thread.name}:`, error);
+ }
+ }
+ } catch (error) {
+ console.error("Error fetching threads:", error);
+ }
+
+ console.log(
+ `Completed: Processed ${totalMessageCount} messages total, added ${totalHeartCount} hearts total`,
+ );
+ }
+ } catch (error) {
+ console.error("Error adding hearts to existing messages:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/constants.ts b/packages/gateway/src/listeners/constants.ts
new file mode 100644
index 0000000..32cdd69
--- /dev/null
+++ b/packages/gateway/src/listeners/constants.ts
@@ -0,0 +1 @@
+export const ROLEPLAY_UMAGRAM_CHANNEL_ID = "1419523288001937458";
diff --git a/packages/gateway/src/listeners/index.ts b/packages/gateway/src/listeners/index.ts
new file mode 100644
index 0000000..bd97bcd
--- /dev/null
+++ b/packages/gateway/src/listeners/index.ts
@@ -0,0 +1,20 @@
+import { Client } from "discord.js";
+import { handleIqdbModeration } from "./iqdbModeration";
+import { handleRoleplayUmagram } from "./roleplayUmagram";
+import { handleAnnouncementReaction } from "./announcementReaction";
+import { handleRoleProtection } from "./roleProtection";
+import { handleChannelDeletion } from "./channelDeletion";
+import { handleMessageDeletion } from "./messageDeletion";
+import { handleMessageEdit } from "./messageEdit";
+import { handleClientReady } from "./clientReady";
+
+export const handleListeners = (client: Client) => {
+ handleClientReady(client);
+ handleIqdbModeration(client);
+ handleRoleplayUmagram(client);
+ handleAnnouncementReaction(client);
+ handleRoleProtection(client);
+ handleChannelDeletion(client);
+ handleMessageDeletion(client);
+ handleMessageEdit(client);
+};
diff --git a/packages/gateway/src/listeners/iqdbModeration.ts b/packages/gateway/src/listeners/iqdbModeration.ts
new file mode 100644
index 0000000..c23ab8d
--- /dev/null
+++ b/packages/gateway/src/listeners/iqdbModeration.ts
@@ -0,0 +1,210 @@
+import { Client, Events, Message } from "discord.js";
+import { sendAuditLog } from "../commands/utilities";
+
+const IQDB_MODERATION_CHANNEL_IDS = [
+ "1410333697701314791",
+ "1420297845998620733",
+];
+
+export const handleIqdbModeration = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ if (!IQDB_MODERATION_CHANNEL_IDS.includes(message.channelId)) return;
+
+ const imageAttachments = message.attachments.filter((attachment) =>
+ attachment.contentType?.startsWith("image/"),
+ );
+
+ if (imageAttachments.size === 0) return;
+
+ try {
+ console.log(
+ `Processing ${imageAttachments.size} image(s) for iqdb moderation...`,
+ );
+
+ for (const attachment of imageAttachments.values()) {
+ try {
+ const { searchPic } = await import("iqdb-client");
+ const result = await searchPic(attachment.url, { lib: "www" });
+ const matches =
+ result.data?.filter(
+ (item) => item.similarity !== null && item.similarity > 0.6,
+ ) || [];
+
+ if (matches.length === 0) {
+ console.log("No significant matches found in iqdb");
+
+ continue;
+ }
+
+ for (const match of matches) {
+ if (match.sourceUrl) {
+ try {
+ let booruType = "";
+ if (match.sourceUrl.includes("danbooru.donmai.us")) {
+ booruType = "danbooru";
+ } else if (match.sourceUrl.includes("yande.re")) {
+ booruType = "yande.re";
+ } else if (match.sourceUrl.includes("gelbooru.com")) {
+ booruType = "gelbooru";
+ } else if (match.sourceUrl.includes("konachan.com")) {
+ booruType = "konachan";
+ }
+
+ if (booruType) {
+ console.log(
+ `Found match on ${booruType}, checking for prohibited tags...`,
+ );
+
+ try {
+ const postId = match.sourceUrl.match(/\/(\d+)/)?.[1];
+
+ if (!postId) continue;
+
+ let tags: string[] = [];
+
+ if (booruType === "danbooru") {
+ const response = await fetch(
+ `https://danbooru.donmai.us/posts/${postId}.json`,
+ );
+
+ if (response.ok) {
+ const postData = await response.json();
+
+ tags = postData.tag_string?.split(" ") || [];
+ }
+ } else if (booruType === "yande.re") {
+ const response = await fetch(
+ `https://yande.re/post.json?tags=id:${postId}`,
+ );
+
+ if (response.ok) {
+ const postData = await response.json();
+
+ if (postData.length > 0)
+ tags = postData[0].tags?.split(" ") || [];
+ }
+ } else if (booruType === "gelbooru") {
+ const response = await fetch(
+ `https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&id=${postId}`,
+ );
+
+ if (response.ok) {
+ const postData = await response.json();
+
+ if (postData.post)
+ tags = postData.post.tags?.split(" ") || [];
+ }
+ } else if (booruType === "konachan") {
+ const response = await fetch(
+ `https://konachan.com/post.json?tags=id:${postId}`,
+ );
+
+ if (response.ok) {
+ const postData = await response.json();
+
+ if (postData.length > 0)
+ tags = postData[0].tags?.split(" ") || [];
+ }
+ }
+
+ console.log(
+ `Retrieved ${tags.length} tags from ${booruType}:`,
+ tags.slice(0, 10),
+ );
+
+ const prohibitedTags = [
+ /^(loli|shota)$/i,
+ /^(loli|shota)_/i,
+ /_(loli|shota)$/i,
+ /_(loli|shota)_/i,
+ ];
+ const foundProhibitedTags = tags.filter((tag) =>
+ prohibitedTags.some((prohibited) => {
+ if (prohibited instanceof RegExp) {
+ return prohibited.test(tag);
+ } else {
+ return tag
+ .toLowerCase()
+ .includes((prohibited as string).toLowerCase());
+ }
+ }),
+ );
+
+ if (foundProhibitedTags.length > 0) {
+ console.log(
+ `Prohibited tags detected: ${foundProhibitedTags.join(", ")}, deleting message...`,
+ );
+
+ await message.delete();
+
+ const { EmbedBuilder } = await import("discord.js");
+ const embed = new EmbedBuilder()
+ .setTitle("🚫 Image Deleted - Prohibited Tags Detected")
+ .setColor("#ff0000")
+ .addFields(
+ {
+ name: "Channel",
+ value: `<#${message.channelId}>`,
+ inline: true,
+ },
+ {
+ name: "Author",
+ value: `<@${message.author.id}>`,
+ inline: true,
+ },
+ {
+ name: "Message ID",
+ value: `[${message.id}](https://discord.com/channels/${message.guildId}/${message.channelId}/${message.id})`,
+ inline: true,
+ },
+ {
+ name: "Booru Source",
+ value: `[${booruType}](${match.sourceUrl})`,
+ inline: true,
+ },
+ {
+ name: "Similarity",
+ value: `${(match.similarity! * 100).toFixed(1)}%`,
+ inline: true,
+ },
+ {
+ name: "Prohibited Tags",
+ value: foundProhibitedTags.join(", "),
+ inline: false,
+ },
+ )
+ .setTimestamp()
+ .setFooter({
+ text: `Guild: ${message.guild?.name || "Unknown"}`,
+ });
+
+ await sendAuditLog(
+ client,
+ embed,
+ undefined,
+ "1406422619934167106",
+ );
+
+ return;
+ }
+ } catch (error) {
+ console.error(
+ `Error fetching tags from ${booruType}:`,
+ error,
+ );
+ }
+ }
+ } catch (error) {
+ console.error("Error processing booru match:", error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Error processing image attachment:", error);
+ }
+ }
+ } catch (error) {
+ console.error("Error in iqdb moderation:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/messageDeletion.ts b/packages/gateway/src/listeners/messageDeletion.ts
new file mode 100644
index 0000000..05431e7
--- /dev/null
+++ b/packages/gateway/src/listeners/messageDeletion.ts
@@ -0,0 +1,55 @@
+import { Client, Events, EmbedBuilder } from "discord.js";
+import { GUILD_ID } from "../constants";
+import { sendAuditLog } from "../commands/utilities";
+
+export const handleMessageDeletion = (client: Client) => {
+ client.on(Events.MessageDelete, async (deletedMessage) => {
+ if (deletedMessage.guildId !== GUILD_ID) return;
+
+ if (deletedMessage.author?.bot) return;
+
+ try {
+ const channel = deletedMessage.channel;
+ const author = deletedMessage.author;
+ const content = deletedMessage.content || "*No text content*";
+ const embed = new EmbedBuilder()
+ .setTitle("🗑️ Message Deleted")
+ .setColor("#ff4444")
+ .addFields(
+ {
+ name: "Channel",
+ value: channel.isTextBased() ? `<#${channel.id}>` : "Unknown",
+ inline: true,
+ },
+ {
+ name: "Author",
+ value: author ? `<@${author.id}>` : "Unknown",
+ inline: true,
+ },
+ {
+ name: "Message ID",
+ value: `[${deletedMessage.id}](https://discord.com/channels/${deletedMessage.guildId}/${channel.id}/${deletedMessage.id})`,
+ inline: true,
+ },
+ )
+ .setTimestamp()
+ .setFooter({
+ text: `Guild: ${deletedMessage.guild?.name || "Unknown"}`,
+ });
+
+ if (content.length <= 1024) {
+ embed.addFields({ name: "Content", value: content, inline: false });
+ await sendAuditLog(client, embed);
+ } else {
+ embed.addFields({
+ name: "Content",
+ value: "*Content too long, see message below*",
+ inline: false,
+ });
+ await sendAuditLog(client, embed, content);
+ }
+ } catch (error) {
+ console.error("Error logging message deletion:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/messageEdit.ts b/packages/gateway/src/listeners/messageEdit.ts
new file mode 100644
index 0000000..d1fd2a8
--- /dev/null
+++ b/packages/gateway/src/listeners/messageEdit.ts
@@ -0,0 +1,86 @@
+import { Client, Events, EmbedBuilder } from "discord.js";
+import { GUILD_ID } from "../constants";
+import { sendAuditLog } from "../commands/utilities";
+
+export const handleMessageEdit = (client: Client) => {
+ client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
+ if (newMessage.guildId !== GUILD_ID) return;
+
+ if (newMessage.author?.bot) return;
+
+ if (oldMessage.content === newMessage.content) return;
+
+ try {
+ const channel = newMessage.channel;
+ const author = newMessage.author;
+ const oldContent = oldMessage.content || "*No text content*";
+ const newContent = newMessage.content || "*No text content*";
+ const embed = new EmbedBuilder()
+ .setTitle("✏️ Message Edited")
+ .setColor("#ffaa00")
+ .addFields(
+ {
+ name: "Channel",
+ value: channel.isTextBased() ? `<#${channel.id}>` : "Unknown",
+ inline: true,
+ },
+ {
+ name: "Author",
+ value: author ? `<@${author.id}>` : "Unknown",
+ inline: true,
+ },
+ {
+ name: "Message ID",
+ value: `[${newMessage.id}](https://discord.com/channels/${newMessage.guildId}/${channel.id}/${newMessage.id})`,
+ inline: true,
+ },
+ )
+ .setTimestamp()
+ .setFooter({ text: `Guild: ${newMessage.guild?.name || "Unknown"}` });
+ const maxFieldLength = 1024;
+ const needsSeparateContent =
+ oldContent.length > maxFieldLength ||
+ newContent.length > maxFieldLength;
+
+ if (needsSeparateContent) {
+ embed.addFields(
+ {
+ name: "Before",
+ value:
+ oldContent.length > maxFieldLength
+ ? "*Content too long, see message below*"
+ : oldContent,
+ inline: false,
+ },
+ {
+ name: "After",
+ value:
+ newContent.length > maxFieldLength
+ ? "*Content too long, see message below*"
+ : newContent,
+ inline: false,
+ },
+ );
+
+ let additionalContent = "";
+
+ if (oldContent.length > maxFieldLength) {
+ additionalContent += `BEFORE:\n${oldContent}\n\n`;
+ }
+ if (newContent.length > maxFieldLength) {
+ additionalContent += `AFTER:\n${newContent}`;
+ }
+
+ await sendAuditLog(client, embed, additionalContent);
+ } else {
+ embed.addFields(
+ { name: "Before", value: oldContent, inline: false },
+ { name: "After", value: newContent, inline: false },
+ );
+ await sendAuditLog(client, embed);
+ }
+ } catch (error) {
+ console.error("Error logging message edit:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/roleProtection.ts b/packages/gateway/src/listeners/roleProtection.ts
new file mode 100644
index 0000000..ff6ee38
--- /dev/null
+++ b/packages/gateway/src/listeners/roleProtection.ts
@@ -0,0 +1,119 @@
+import { Client, Events } from "discord.js";
+import { GUILD_ID } from "../constants";
+
+const PROTECTED_ROLE_ID = "1406422617724026909";
+const COLOR_ROLE_IDS = [
+ "1407075059830624406", // Nice Nature Red
+ "1407075160250650664", // Taiki Shuttle Green
+ "1407075256904187997", // Mejiro McQueen Purple
+ "1407075372427640952", // Gold Ship Grey
+ "1407075670177091664", // Grass Wonder Gold
+ "1407078154555752589", // Agnes Tachyon Dark Purple
+ "1407345006108475476", // Special Week Salmon
+ "1408246546708959403", // Biwahaya Hide Linen
+ "1408247166413176943", // Symboli Rudolf Celeste
+ "1411128003924332764", // King Halo Dark Blue
+ "1413582797284708474", // Matikanetannhauser Lemon
+ "1414435043761324042", // Silence Suzuka Sea Green
+ "1414454914138116158", // Haru Urara Pink
+ "1414455824524247161", // TM Opera O Orange
+ "1414456352167825490", // Oguri Cap Buttermilk
+ "1414541675396862012", // Kitasan Black Sable
+ "1415083621152460832", // Tokai Teio Royal Blue
+ "1415520343690575883", // Aston Machan Sienna
+ "1415539100315942962", // Super Creek Baby Blue
+ "1415539544232824913", // Sakura Bakushin O Lilac
+ "1415567915578818723", // El Condor Pasa Biscotti
+ "1415592658906124338", // Still in Love Crimson
+ "1415593126273224795", // Mayano Top Gun Navy Blue
+ "1415797242845200475", // Mr. C.B. Forest Green
+ "1416583306698297354", // Seuin Sky Mint
+ "1416583690217328660", // Neo Universe Pastel Yellow
+ "1416595046249267364", // Manhattan Cafe Jet Black
+];
+
+export const handleRoleProtection = (client: Client) => {
+ client.on(Events.GuildMemberUpdate, async (oldMember, newMember) => {
+ if (newMember.guild.id !== GUILD_ID) return;
+
+ const oldRoles = oldMember.roles.cache;
+ const newRoles = newMember.roles.cache;
+ const addedRoles = newRoles.filter((role) => !oldRoles.has(role.id));
+
+ if (addedRoles.size === 0) return;
+
+ try {
+ const protectedRole = newMember.guild.roles.cache.get(PROTECTED_ROLE_ID);
+
+ if (!protectedRole) {
+ console.error("Protected role not found");
+
+ return;
+ }
+
+ const guildOwner = await newMember.guild.fetchOwner();
+
+ for (const role of addedRoles.values()) {
+ if (role.id === PROTECTED_ROLE_ID) continue;
+
+ if (COLOR_ROLE_IDS.includes(role.id)) continue;
+
+ if (role.position > protectedRole.position) {
+ try {
+ const auditLogs = await newMember.guild.fetchAuditLogs({
+ type: 25, // MEMBER_ROLE_UPDATE
+ limit: 10,
+ });
+
+ const relevantLog = auditLogs.entries.find(
+ (entry) =>
+ entry.target?.id === newMember.id &&
+ entry.changes?.some(
+ (change) =>
+ change.key === "$add" &&
+ change.new?.some((r: any) => r.id === role.id),
+ ),
+ );
+
+ if (relevantLog && relevantLog.executor?.id === guildOwner.id) {
+ console.log(
+ `High role ${role.name} (${role.id}) added to ${newMember.user.tag} by server owner, allowing...`,
+ );
+
+ continue;
+ }
+
+ console.log(
+ `High role ${role.name} (${role.id}) added to ${newMember.user.tag} (${newMember.id}) by unauthorized user, removing...`,
+ );
+
+ try {
+ await newMember.roles.remove(role);
+ } catch (error) {
+ console.error(
+ `Failed to remove high role ${role.name} from ${newMember.user.tag}:`,
+ error,
+ );
+ }
+ } catch (auditError) {
+ console.error("Error checking audit log:", auditError);
+ console.log(
+ `High role ${role.name} (${role.id}) added to ${newMember.user.tag} (${newMember.id}), removing due to audit log error...`,
+ );
+
+ try {
+ await newMember.roles.remove(role);
+ } catch (error) {
+ console.error(
+ `Failed to remove high role ${role.name} from ${newMember.user.tag}:`,
+ error,
+ );
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Error in role protection handler:", error);
+ }
+ });
+};
diff --git a/packages/gateway/src/listeners/roleplayUmagram.ts b/packages/gateway/src/listeners/roleplayUmagram.ts
new file mode 100644
index 0000000..6588a03
--- /dev/null
+++ b/packages/gateway/src/listeners/roleplayUmagram.ts
@@ -0,0 +1,47 @@
+import { Client, Events, Message } from "discord.js";
+import { ROLEPLAY_UMAGRAM_CHANNEL_ID } from "./constants";
+
+export const handleRoleplayUmagram = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ const isMainChannel = message.channelId === ROLEPLAY_UMAGRAM_CHANNEL_ID;
+ const isThreadInChannel =
+ message.channel?.isThread() &&
+ message.channel.parentId === ROLEPLAY_UMAGRAM_CHANNEL_ID;
+
+ if (!isMainChannel && !isThreadInChannel) return;
+
+ try {
+ if (message.author.id === client.user?.id) return;
+
+ try {
+ await message.react("❤️");
+ } catch (error) {
+ console.error("Failed to add heart reaction:", error);
+ }
+
+ if (isMainChannel && !message.reference) {
+ const hasAttachments = message.attachments.size > 0;
+ const hasTextContent =
+ message.content && message.content.trim().split(/\s+/).length > 1;
+
+ if (!hasAttachments || !hasTextContent) {
+ await message.delete();
+
+ const errorMessage = await (message.channel as any).send(
+ `${message.author}, to participate in <#${ROLEPLAY_UMAGRAM_CHANNEL_ID}>, you can either:\n\n- **Post**: Send a message with both a brief caption **and** an image attachment\n- **Reply**: Reply to someone else's post (no image needed)\n - Reply with a thread (suggested)\n - Reply directly with a message\n\nThis message will be deleted in 30 seconds.`,
+ );
+
+ setTimeout(async () => {
+ try {
+ await errorMessage.delete();
+ } catch (error) {
+ console.error("Failed to delete error message:", error);
+ }
+ }, 30000);
+ }
+ }
+ } catch (error) {
+ console.error("Error in roleplay-umagram moderation:", error);
+ }
+ });
+};
diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json
new file mode 100644
index 0000000..c2767fd
--- /dev/null
+++ b/packages/gateway/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "allowJs": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": false,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
+}