diff options
Diffstat (limited to 'packages')
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" + ] +} |