WIP: [#1]: Athena Rewrite #1

Draft
nxbat wants to merge 14 commits from athena-rewrite into dev
46 changed files with 800 additions and 906 deletions
+1
View File
@@ -1 +1,2 @@
registry=https://npm.hep.gg
@clynamic:registry=https://npm.clynamic.net @clynamic:registry=https://npm.clynamic.net
+33 -264
View File
@@ -1,15 +1,14 @@
{ {
"name": "discordbot-ng", "name": "hexerade",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "discordbot-ng",
"dependencies": { "dependencies": {
"@clynamic/dmark": "0.0.11", "@clynamic/dmark": "0.0.11",
"@redis/client": "5.1.0", "@redis/client": "5.1.0",
"athena-prime": "4.2.0",
"body-parser": "2.2.2", "body-parser": "2.2.2",
"discord.js": "14.26.4",
"dotenv": "16.5.0", "dotenv": "16.5.0",
"express": "5.1.0", "express": "5.1.0",
"express-session": "1.19.0", "express-session": "1.19.0",
@@ -37,145 +36,6 @@
"integrity": "sha512-efrxWJ6ONQ6kcCLwPfoIonrMznvYYm9fZ1v+DgDY5BzQ8EDrgbCJr3ri9dVS7VUt9Rh8yZTXtaMDI1jf3Gb0Aw==", "integrity": "sha512-efrxWJ6ONQ6kcCLwPfoIonrMznvYYm9fZ1v+DgDY5BzQ8EDrgbCJr3ri9dVS7VUt9Rh8yZTXtaMDI1jf3Gb0Aw==",
"license": "UNLICENSED" "license": "UNLICENSED"
}, },
"node_modules/@discordjs/builders": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/formatters": "^0.6.2",
"@discordjs/util": "^1.2.0",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/collection": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
"engines": {
"node": ">=16.11.0"
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.2.0",
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.40",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "6.24.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@discordjs/util": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
"license": "Apache-2.0",
"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"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
@@ -899,39 +759,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sapphire/shapeshift": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v16"
}
},
"node_modules/@sapphire/snowflake": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@stylistic/eslint-plugin": { "node_modules/@stylistic/eslint-plugin": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz",
@@ -1031,6 +858,7 @@
"version": "22.15.21", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -1068,15 +896,6 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
@@ -1284,16 +1103,6 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
@@ -1394,6 +1203,22 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}, },
"node_modules/athena-prime": {
"version": "4.2.0",
"resolved": "https://npm.hep.gg/athena-prime/-/athena-prime-4.2.0.tgz",
"integrity": "sha512-CbIPwdupVGfGRR/ZRcc9BAC/nBnRQS9sRalzWtONYQ4eNhCDJVykPLQM2N7abndvcpLctWd1cD3awpA9NDy30Q==",
"license": "NOLICENSE",
"dependencies": {
"discord-api-types": "^0.38.48",
"eventemitter3": "^5.0.1"
},
"engines": {
"node": ">=22.15.0"
},
"optionalDependencies": {
"ws": "^8.21.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1838,41 +1663,14 @@
} }
}, },
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.38.42", "version": "0.38.53",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", "resolved": "https://npm.hep.gg/discord-api-types/-/discord-api-types-0.38.53.tgz",
"integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", "integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"scripts/actions/documentation" "scripts/actions/documentation"
] ]
}, },
"node_modules/discord.js": {
"version": "14.26.4",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz",
"integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.14.1",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.2",
"@discordjs/rest": "^2.6.1",
"@discordjs/util": "^1.2.0",
"@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "6.24.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.5.0", "version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
@@ -2203,6 +2001,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://npm.hep.gg/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/expand-template": { "node_modules/expand-template": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -2303,7 +2107,8 @@
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
@@ -2923,29 +2728,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
},
"node_modules/magic-bytes.js": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
"license": "MIT"
},
"node_modules/markdown-it": { "node_modules/markdown-it": {
"version": "14.2.0", "version": "14.2.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
@@ -4408,18 +4196,6 @@
"typescript": ">=4.8.4" "typescript": ">=4.8.4"
} }
}, },
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.22.4", "version": "4.22.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
@@ -4527,19 +4303,11 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/undici": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
@@ -4704,6 +4472,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
+3 -1
View File
@@ -11,11 +11,12 @@
"typescript": "5.8.3", "typescript": "5.8.3",
"typescript-eslint": "8.32.1" "typescript-eslint": "8.32.1"
}, },
"dependencies": { "dependencies": {
"@clynamic/dmark": "0.0.11", "@clynamic/dmark": "0.0.11",
"@redis/client": "5.1.0", "@redis/client": "5.1.0",
"athena-prime": "4.2.0",
"body-parser": "2.2.2", "body-parser": "2.2.2",
"discord.js": "14.26.4",
"dotenv": "16.5.0", "dotenv": "16.5.0",
"express": "5.1.0", "express": "5.1.0",
"express-session": "1.19.0", "express-session": "1.19.0",
@@ -24,6 +25,7 @@
"sqlite": "5.1.1", "sqlite": "5.1.1",
"sqlite3": "6.0.1" "sqlite3": "6.0.1"
}, },
"scripts": { "scripts": {
"dev": "tsx watch --enable-source-maps src/index.ts", "dev": "tsx watch --enable-source-maps src/index.ts",
"start": "node dist/index.js", "start": "node dist/index.js",
+46
View File
@@ -0,0 +1,46 @@
// External Imports
import { CommandClient, Constants, LRUCollection, Member, User } from "athena-prime";
// Internal Imports
import config from "./config";
import events from "./events";
import commands from "./commands";
// ---------------
const client = new CommandClient({
token: `Bot ${config.DISCORD_TOKEN}`,
options: {
intents: [
Constants.GatewayIntentBits.Guilds,
Constants.GatewayIntentBits.GuildMembers,
Constants.GatewayIntentBits.GuildMessages,
Constants.GatewayIntentBits.GuildModeration,
Constants.GatewayIntentBits.GuildVoiceStates,
Constants.GatewayIntentBits.MessageContent,
],
largeBotOptimizations: true,
disableEvents: {
PRESENCE_UPDATE: true,
},
// TODO: Move caching to Redis.
cache: {
users: () => new LRUCollection(User, 1024),
members: (guild) => new LRUCollection(Member, 1024),
guildEmojis: false,
guildStickers: false,
transientMembers: false,
userSweepIntervalMs: 3_600_000 // 1hr.
}
}
});
commands.forEach(command => client.registerCommand(command));
events.forEach(event => client.registerEvent(event));
export default client;
+49
View File
@@ -0,0 +1,49 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// Internal Imports
import { Database } from '../../shared/Database';
import { CreateDefaultEmbed, SetSeverity } from '../../utils';
// ---------------
@SlashCommand(
new CommandBuilder('database', "Essential database functions. THESE ARE POTENTIALLY DESTRUCTIVE ACTIONS, BE SURE!")
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.setMemberPermission(Constants.PermissionFlagsBits.Administrator)
.addSubcommand('purge-user', 'Purge user messages from the database.', builder =>
builder.addUserOption('user', 'The user to purge.', true)
)
)
class DatabaseCommand extends Command<CommandClient> {
cooldown = 60;
userPermissions = [Constants.PermissionFlagsBits.Administrator];
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild() || !interaction.subcommand) return;
await interaction.defer();
switch (interaction.subcommand.at(0)) {
case 'purge-user':
const user = interaction.getRequiredMember('user');
await Database.purgeMessagesFrom(user.id);
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('success'),
description: `Executed user-purge on ${user.mention}.`
}]
});
return;
}
}
}
export default new DatabaseCommand('database');
+68
View File
@@ -0,0 +1,68 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// Internal Imports
import { Database } from '../../shared/Database';
import { CreateDefaultEmbed, SetSeverity } from '../../utils';
// ---------------
@SlashCommand(
new CommandBuilder('rename-general', 'Rename the general channel.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.setMemberPermission(Constants.PermissionFlagsBits.ManageChannels)
.addStringOption({ name: 'name', description: 'The new general chat name.', required: true, max_length: 64 })
)
class RenameGeneralCommand extends Command<CommandClient> {
cooldown = 360;
userPermissions = [Constants.PermissionFlagsBits.ManageChannels];
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild()) return;
await interaction.defer();
const settings = await Database.GetOrCreateSettings(interaction.guild.id);
if (!settings.general_chat_id) {
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('warning'),
description: 'No general channel has been configured. Please check your configuration and try again.'
}]
});
return;
}
const channel = context.getChannel(settings.general_chat_id);
if (!channel || !channel.isTextBased()) {
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('error'),
description: 'There was an issue trying to locate the configured general channel role. Please check your configuration and try again.'
}]
});
return;
}
const name = interaction.getRequiredString('name');
await context.editChannel(settings.general_chat_id, { name: name });
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('success'),
description: `Successfully updated ${channel.mention}`
}]
});
}
}
export default new RenameGeneralCommand('rename-general');
-136
View File
@@ -1,136 +0,0 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, Guild, GuildMember, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles, User } from 'discord.js';
import { Database } from '../shared/Database';
import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../utils';
export default {
name: 'ban',
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Bans a user.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
.addStringOption(option =>
option
.setName('user')
.setDescription('The discord user mention, or ID, to ban.')
.setRequired(true)
)
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason for the ban')
.setRequired(false)
.setMaxLength(400)
)
.addNumberOption(option =>
option
.setName('hours')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('minutes')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('seconds')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('delete-message-days')
.setDescription('How far back to delete messages (in days, default: 0 days).')
.setRequired(false)
.setMinValue(0)
.setMaxValue(7)
)
.addBooleanOption(option =>
option
.setName('full-ban')
.setDescription('Whether or not to prevent the user from joining on known alts (and ban all existing alts).')
.setRequired(false)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await deferInteraction(interaction);
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
const input = interaction.options.getString('user', true);
const idToUse = getIdFromInput(input);
const reason = interaction.options.getString('reason') ?? '';
const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6;
const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000;
const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000;
const duration = hours + minutes + seconds;
const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400;
const fullBan = interaction.options.getBoolean('full-ban') ?? false;
let banMember: GuildMember | null = null;
try {
banMember = await interaction.guild.members.fetch(idToUse);
} catch (e) {
// Member not in server.
}
const member = await interaction.guild.members.fetch(interaction.user.id);
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
return await interaction.editReply('You do not have permission to ban this user.');
}
if (banMember && !banMember.bannable) {
return await interaction.editReply('I do not have permission to ban this user.');
}
const expiresAt = new Date(Date.now() + duration);
await Database.putBan(idToUse, duration > 0 ? expiresAt : null, fullBan);
try {
await interaction.guild.bans.create(idToUse, {
reason: (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(),
deleteMessageSeconds: deleteMessageDays
});
} catch (e) {
console.error(e);
return await interaction.editReply("Error banning user (couldn't ban).");
}
if (fullBan) {
const alts = await comprehensiveAltLookupFromDiscord(idToUse, interaction.guild);
await removeAllAlts([alts], interaction.guild, interaction.user, fullBan, reason, deleteMessageDays, duration, expiresAt);
}
await interaction.editReply(`<@${idToUse}> (${idToUse}) has been ${fullBan ? 'full banned' : 'banned'}.`);
}
};
async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, fullBan: boolean, reason: string, deleteMessageDays: number, duration: number, expiresAt: Date) {
for (const data of altData) {
if (data.type == 'discord') {
try {
if (!data.banned) {
await guild.members.kick(data.thisId as string, (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${moderator.username} (${moderator.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim());
}
} catch (e) {
console.error(e);
}
}
await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt);
}
}
+54 -39
View File
@@ -1,48 +1,63 @@
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; // External Imports
import { AutocompleteInteraction, Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// Internal Imports
import { Database } from '../shared/Database'; import { Database } from '../shared/Database';
import { KnowledgebaseItem } from '../types'; import { CreateDefaultEmbed, SetSeverity } from '../utils';
export default { // ---------------
name: 'cite',
data: new SlashCommandBuilder()
.setName('cite')
.setDescription('Cite content from the knowledgebase.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addIntegerOption(option =>
option
.setName('name')
.setDescription('The name of the entry.')
.setRequired(true)
.setAutocomplete(true)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
await interaction.deferReply(); @SlashCommand(
new CommandBuilder('cite', 'Cite a knowledgebase article.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.addIntegerOption({ name: 'article', description: 'The article to cite.', required: true, autocomplete: true })
)
class CiteCommand extends Command<CommandClient> {
cooldown = 5; // 5s.
const id = interaction.options.getInteger('name', true); async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild()) return;
await interaction.defer();
const item = await Database.getFromKnowledgebase(id); const article = interaction.getRequiredInteger('article');
const item = await Database.getFromKnowledgebase(article);
if (!item) return interaction.editReply('Knowledgebase item not found.'); if (!item) {
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('error'),
return interaction.editReply(item.content); description: 'There was an issue locating the requested knowledgebase article.'
}, }]
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) { });
if (!interaction.guild) return interaction.respond([]);
const items: KnowledgebaseItem[] = await Database.getAllKnowledgebaseItems(interaction.guild.id); return;
const value = interaction.options.getFocused();
const toRespond = items.filter(i => !value ? true : i.content.includes(value));
if (toRespond.length > 25) toRespond.length = 25;
interaction.respond(toRespond.map(p => ({
name: p.name,
value: p.id
})));
} }
};
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
description: item.content
}]
});
}
async handleAutocomplete(context: CommandClient<any, any>, interaction: AutocompleteInteraction, data: any) {
if (!interaction.guild) return await interaction.acknowledge([]);
const items = await Database.getAllKnowledgebaseItems(interaction.guild.id);
const value = interaction.focused();
if(!value) return await interaction.acknowledge([]);
const response = items.filter(item => item.content.includes(value.value as string));
if (response.length > 25) response.length = 25;
return await interaction.acknowledge(response.map(res => ({ name: res.name, value: res.id })));
}
}
export default new CiteCommand('cite');
-48
View File
@@ -1,48 +0,0 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { Database } from '../shared/Database';
import { getIdFromInput } from '../utils';
export default {
name: 'database',
data: new SlashCommandBuilder()
.setName('database')
.setDescription('Database access.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommandGroup(subcommandGroup =>
subcommandGroup
.setName('messages')
.setDescription('Access to messages in the database.')
.addSubcommand(subcommand =>
subcommand
.setName('purge')
.setDescription("Purge a user's messages from the database.")
.addStringOption(option =>
option
.setName('user')
.setDescription('The discord user id, or mention, of the user.')
.setRequired(true)
)
)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
const group = interaction.options.getSubcommandGroup(true);
const subcommand = interaction.options.getSubcommand(true);
if (group == 'messages') {
if (subcommand == 'purge') {
const discordUserInput = interaction.options.getString('discord-user', true);
const idToUse = getIdFromInput(discordUserInput);
await Database.purgeMessagesFrom(idToUse);
return interaction.editReply('Messages purged.');
}
}
}
};
+22
View File
@@ -0,0 +1,22 @@
import database from "./admin/database";
import renameChannel from "./admin/rename-channel";
import ban from "./moderation/ban";
import hardban from "./moderation/hardban";
import about from "./utilities/about";
import nameSync from "./utilities/name-sync";
export default [
// Admin
database,
renameChannel,
// Moderation
ban,
hardban,
// Utilities
about,
nameSync
];
+60
View File
@@ -0,0 +1,60 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// Internal Imports
import { Database } from '../../shared/Database';
import { CreateDefaultEmbed, SetSeverity } from '../../utils';
// ---------------
@SlashCommand(
new CommandBuilder('ban', 'Ban a user, optionally cleanup 1 day worth of messages.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.setMemberPermission(Constants.PermissionFlagsBits.BanMembers)
.addUserOption('user', 'The user to ban.', true)
.addStringOption({ name: 'duration', description: '(optional): The duration of the ban. Formatted as 7d12h30m30s.', required: false })
.addStringOption({ name: 'reason', description: '(optional): The reason for banning the user.', required: false })
.addBooleanOption('clean', "(optional): Clean the user's messages upwards of a day.", false)
.addBooleanOption('purge', "(optional): Purge the user's messages from the database.", false)
)
class BanCommand extends Command<CommandClient> {
cooldown = 5;
userPermissions = [Constants.PermissionFlagsBits.BanMembers];
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild()) return;
await interaction.defer();
const user = interaction.getRequiredMember('user');
const duration = interaction.getString('duration');
const reason = interaction.getString('reason') || 'No reason specified.';
const clean = interaction.getBoolean('clean') || false;
const purge = interaction.getBoolean('purge') || false;
// Nix -> Tarrgon: Could you pretty please hook up the database for this 🥺
await context.banGuildMember(interaction.guild.id, user.id, clean ? 1 : undefined, reason);
if (purge) await Database.purgeMessagesFrom(user.id);
interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('success'),
description: 'Executed ban action.',
fields: [
{ name: 'User', value: `${user.username} (${user.mention})`, inline: false },
{ name: 'Duration', value: `${duration || 'Permanent'}`, inline: false },
{ name: 'Reason', value: `${user.username} (${user.mention})`, inline: false },
{ name: 'Clean', value: `${clean ? 'Yes' : 'No'}`, inline: true },
{ name: 'Purge', value: `${purge ? 'Yes' : 'No'}`, inline: true },
]
}]
});
}
}
export default new BanCommand('ban');
+34
View File
@@ -0,0 +1,34 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// ---------------
@SlashCommand(
new CommandBuilder('hard-ban', 'Permanently bans a user, and all known alternate accounts.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.setMemberPermission(Constants.PermissionFlagsBits.BanMembers)
.addUserOption('user', 'The user to ban.', true)
.addStringOption({ name: 'reason', description: '(optional): The reason for banning the user.', required: false })
.addBooleanOption('clean', "(optional): Clean the user's messages upwards of a day.", false)
.addBooleanOption('purge', "(optional): Purge the user's messages from the database.", false)
)
class HardBanCommand extends Command<CommandClient> {
cooldown = 5;
userPermissions = [Constants.PermissionFlagsBits.BanMembers];
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, fetchedData?: any) {
if (!interaction.inGuild()) return;
await interaction.defer();
const user = interaction.getRequiredMember('user');
const reason = interaction.getString('reason') || 'No reason specified.';
const clean = interaction.getBoolean('clean') || false;
const purge = interaction.getBoolean('purge') || false;
// Nix -> Tarrgon: Please implement the hard-ban here :3
}
}
export default new HardBanCommand('hardban');
-36
View File
@@ -1,36 +0,0 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, SlashCommandBuilder } from 'discord.js';
import { config } from '../config';
import { syncName } from '../utils';
export default {
name: 'name-sync',
data: new SlashCommandBuilder()
.setName('name-sync')
.setDescription('Sync your discord nickname to your e621 name.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM)
.addIntegerOption(option =>
option
.setName('id')
.setDescription('The id of the e621 user to sync your nickname to.')
.setRequired(false)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
const id = interaction.options.getInteger('id');
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
if (!guild) {
return interaction.editReply('An error has occurred. Please try again later.');
}
const member = await guild.members.fetch(interaction.user.id);
if (!member || !guild.members.me) {
return interaction.editReply('An error has occurred. Please try again later.');
}
await syncName(interaction, member, id);
}
};
+1 -1
View File
@@ -39,7 +39,7 @@ export default {
.addComponents(button); .addComponents(button);
await interaction.channel.send({ components: [row], content }); await interaction.channel.send({ components: [row], content });
await Database.updateGuildSettings(interaction.guildId, 'private_help_channel_id', interaction.channelId); await Database.updateSettings(interaction.guildId, 'private_help_channel_id', interaction.channelId);
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' }); interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' });
} }
}; };
-51
View File
@@ -1,51 +0,0 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, RateLimitError, SlashCommandBuilder } from 'discord.js';
import { msToHuman } from '../utils';
import { Database } from '../shared/Database';
export default {
name: 'rename',
data: new SlashCommandBuilder()
.setName('rename')
.setDescription('Rename the general channel.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
.addStringOption(option =>
option
.setName('new-name')
.setDescription('The new name of the general channel.')
.setRequired(true)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
if (!guildSettings || !guildSettings.general_chat_id) {
return interaction.reply('No general chat id found.');
}
const name = interaction.options.getString('new-name', true);
if (name.length > 100) {
return interaction.reply('Name must be less than 100 characters in length.');
}
const channel = await interaction.guild!.channels.fetch(guildSettings.general_chat_id)!;
if (!channel) {
return interaction.reply('No general chat id found.');
}
try {
await channel.setName(name);
interaction.reply(`Renamed general to ${channel.name}`);
} catch (e: any) {
if (e instanceof RateLimitError) {
return interaction.reply(`Name change limited. Try again in ${msToHuman(e.retryAfter)}`);
}
console.error(e);
return interaction.reply('An error has occurred.');
}
}
};
+14 -14
View File
@@ -139,75 +139,75 @@ export default {
const generalChannel = interaction.options.getChannel('general-channel'); const generalChannel = interaction.options.getChannel('general-channel');
if (generalChannel) { if (generalChannel) {
await Database.updateGuildSettings(interaction.guildId, 'general_chat_id', generalChannel.id); await Database.updateSettings(interaction.guildId, 'general_chat_id', generalChannel.id);
response.push(`**general_chat_id** has been set to: ${generalChannel}.`); response.push(`**general_chat_id** has been set to: ${generalChannel}.`);
} }
const ticketsChannel = interaction.options.getChannel('tickets-channel'); const ticketsChannel = interaction.options.getChannel('tickets-channel');
if (ticketsChannel) { if (ticketsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'tickets_channel_id', ticketsChannel.id); await Database.updateSettings(interaction.guildId, 'tickets_channel_id', ticketsChannel.id);
response.push(`**tickets_channel_id** has been set to: ${ticketsChannel}.`); response.push(`**tickets_channel_id** has been set to: ${ticketsChannel}.`);
} }
const appealsChannel = interaction.options.getChannel('appeals-channel'); const appealsChannel = interaction.options.getChannel('appeals-channel');
if (appealsChannel) { if (appealsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'appeals_channel_id', appealsChannel.id); await Database.updateSettings(interaction.guildId, 'appeals_channel_id', appealsChannel.id);
response.push(`**appeals_channel_id** has been set to ${appealsChannel}.`); response.push(`**appeals_channel_id** has been set to ${appealsChannel}.`);
} }
const eventLogsChannel = interaction.options.getChannel('event-logs-channel'); const eventLogsChannel = interaction.options.getChannel('event-logs-channel');
if (eventLogsChannel) { if (eventLogsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'event_logs_channel_id', eventLogsChannel.id); await Database.updateSettings(interaction.guildId, 'event_logs_channel_id', eventLogsChannel.id);
response.push(`**event_logs_channel_id** has been set to: ${eventLogsChannel}.`); response.push(`**event_logs_channel_id** has been set to: ${eventLogsChannel}.`);
} }
const discordLogsChannel = interaction.options.getChannel('discord-logs-channel'); const discordLogsChannel = interaction.options.getChannel('discord-logs-channel');
if (discordLogsChannel) { if (discordLogsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'discord_logs_channel_id', discordLogsChannel.id); await Database.updateSettings(interaction.guildId, 'discord_logs_channel_id', discordLogsChannel.id);
response.push(`**discord_logs_channel_id** has been set to: ${discordLogsChannel}.`); response.push(`**discord_logs_channel_id** has been set to: ${discordLogsChannel}.`);
} }
const auditLogsChannel = interaction.options.getChannel('audit-logs-channel'); const auditLogsChannel = interaction.options.getChannel('audit-logs-channel');
if (auditLogsChannel) { if (auditLogsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'audit_logs_channel_id', auditLogsChannel.id); await Database.updateSettings(interaction.guildId, 'audit_logs_channel_id', auditLogsChannel.id);
response.push(`**audit_logs_channel_id** has been set to: ${auditLogsChannel}.`); response.push(`**audit_logs_channel_id** has been set to: ${auditLogsChannel}.`);
} }
const voiceLogsChannel = interaction.options.getChannel('voice-logs-channel'); const voiceLogsChannel = interaction.options.getChannel('voice-logs-channel');
if (voiceLogsChannel) { if (voiceLogsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'voice_logs_channel_id', voiceLogsChannel.id); await Database.updateSettings(interaction.guildId, 'voice_logs_channel_id', voiceLogsChannel.id);
response.push(`**voice_logs_channel_id** has been set to: ${voiceLogsChannel}.`); response.push(`**voice_logs_channel_id** has been set to: ${voiceLogsChannel}.`);
} }
const newMemberLogsChannel = interaction.options.getChannel('new-member-channel'); const newMemberLogsChannel = interaction.options.getChannel('new-member-channel');
if (newMemberLogsChannel) { if (newMemberLogsChannel) {
await Database.updateGuildSettings(interaction.guildId, 'new_member_channel_id', newMemberLogsChannel.id); await Database.updateSettings(interaction.guildId, 'new_member_channel_id', newMemberLogsChannel.id);
response.push(`**new_member_channel_id** has been set to: ${newMemberLogsChannel}.`); response.push(`**new_member_channel_id** has been set to: ${newMemberLogsChannel}.`);
} }
const moderatorChannel = interaction.options.getChannel('moderator-channel'); const moderatorChannel = interaction.options.getChannel('moderator-channel');
if (moderatorChannel) { if (moderatorChannel) {
await Database.updateGuildSettings(interaction.guildId, 'moderator_channel_id', moderatorChannel.id); await Database.updateSettings(interaction.guildId, 'moderator_channel_id', moderatorChannel.id);
response.push(`**moderator_channel_id** has been set to": ${moderatorChannel}.`); response.push(`**moderator_channel_id** has been set to": ${moderatorChannel}.`);
} }
const adminRole = interaction.options.getRole('admin-role'); const adminRole = interaction.options.getRole('admin-role');
if (adminRole) { if (adminRole) {
await Database.updateGuildSettings(interaction.guildId, 'admin_role_id', adminRole.id); await Database.updateSettings(interaction.guildId, 'admin_role_id', adminRole.id);
response.push(`**admin_role_id** has been set to: ${adminRole}.`); response.push(`**admin_role_id** has been set to: ${adminRole}.`);
} }
const privateHelperRole = interaction.options.getRole('private-helper-role'); const privateHelperRole = interaction.options.getRole('private-helper-role');
if (privateHelperRole) { if (privateHelperRole) {
await Database.updateGuildSettings(interaction.guildId, 'private_help_role_id', privateHelperRole.id); await Database.updateSettings(interaction.guildId, 'private_help_role_id', privateHelperRole.id);
response.push(`**private_help_role_id** has been set to: ${privateHelperRole}.`); response.push(`**private_help_role_id** has been set to: ${privateHelperRole}.`);
} }
const devWatchRole = interaction.options.getRole('devwatch-role'); const devWatchRole = interaction.options.getRole('devwatch-role');
if (devWatchRole) { if (devWatchRole) {
await Database.updateGuildSettings(interaction.guildId, 'devwatch_role_id', devWatchRole.id); await Database.updateSettings(interaction.guildId, 'devwatch_role_id', devWatchRole.id);
response.push(`**devwatch_role_id** has been set to ${devWatchRole}.`); response.push(`**devwatch_role_id** has been set to ${devWatchRole}.`);
} }
@@ -264,13 +264,13 @@ export default {
const githubReleaseChannel = interaction.options.getChannel('github-release-channel'); const githubReleaseChannel = interaction.options.getChannel('github-release-channel');
if (githubReleaseChannel) { if (githubReleaseChannel) {
await Database.updateGuildSettings(interaction.guildId, 'github_release_channel', githubReleaseChannel.id); await Database.updateSettings(interaction.guildId, 'github_release_channel', githubReleaseChannel.id);
response.push(`**github_release_channel** has been set to ${githubReleaseChannel}.`); response.push(`**github_release_channel** has been set to ${githubReleaseChannel}.`);
} }
const siteBreakerRole = interaction.options.getRole('site-breaker-role'); const siteBreakerRole = interaction.options.getRole('site-breaker-role');
if (siteBreakerRole) { if (siteBreakerRole) {
await Database.updateGuildSettings(interaction.guildId, 'site_breaker_role_id', siteBreakerRole.id); await Database.updateSettings(interaction.guildId, 'site_breaker_role_id', siteBreakerRole.id);
response.push(`**site_breaker_role_id** has been set to ${siteBreakerRole}.`); response.push(`**site_breaker_role_id** has been set to ${siteBreakerRole}.`);
} }
+46
View File
@@ -0,0 +1,46 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// Internal Imports
import { CreateDefaultEmbed } from '../../utils';
// ---------------
@SlashCommand(
new CommandBuilder('about', 'View information about Hexerade!')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
)
class AboutCommand extends Command<CommandClient> {
cooldown = 5; // 5s.
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild()) return;
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
description: "I'm the very cool bot for the e621 Discord.",
fields: [
{ name: 'Members', value: `${interaction.guild.memberCount}`, inline: true },
{ name: 'Boosts (Tier)', value: `${interaction.guild.premiumSubscriptionCount} (${interaction.guild.premiumTier})`, inline: true },
{ name: 'Developer', value: 'tarrgon.', inline: false },
{ name: 'Contributor', value: 'nxbat', inline: false },
{ name: 'Terms of Use', value: 'https://e621.net/terms_of_use', inline: false },
{ name: 'Privacy Policy', value: 'https://e621.net/static/privacy/discordbot', inline: false },
],
thumbnail: {
url: context.user.avatarURL
},
}]
});
}
}
export default new AboutCommand('about');
+63
View File
@@ -0,0 +1,63 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, Member, SlashCommand } from 'athena-prime';
// Internal Imports
import { Database } from '../../shared/Database';
import { E621User } from '../../types';
import { CreateDefaultEmbed, getE621User, SetSeverity } from '../../utils';
// ---------------
async function fetchUser(member: Member, id: number | null): Promise<E621User | null> {
const syncedIds = await Database.getE621Ids(member.id);
if (!id || !syncedIds.includes(id))
return await getE621User(syncedIds[0]);
else
return await getE621User(id);
}
@SlashCommand(
new CommandBuilder('name-sync', 'Sync your Discord username with your e621 username.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.addNumberOption({ name: 'id', description: 'The ID of the user to sync your name to.', required: false, max_value: 16 })
)
class NameSyncCommand extends Command<CommandClient> {
cooldown = 360; // 5m.
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild()) return;
await interaction.defer();
const id = interaction.getNumber('id');
const user = await fetchUser(interaction.member, id);
if (!user) {
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('error'),
description: 'I was unable to get a user entry for your account or provided id. Please try again, or contact an administrator.'
}]
});
return;
}
await interaction.member.edit({ nick: user.name }, 'Member requested a name-sync.');
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('success'),
description: `Successfully updated your display name. Hello \`${user.name}\`!`
}]
});
}
}
export default new NameSyncCommand('name-sync');
+4 -9
View File
@@ -1,10 +1,11 @@
import dotenv from 'dotenv'; // External Imports
import 'dotenv/config';
dotenv.config(); // ---------------
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, RELEASE_SECRET, DATABASE_SECRET, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, PRIVACY_POLICY_URL, GIT_REPO_BASE_URL, REDIS_URL, PORT, DEBUG } = process.env; const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, RELEASE_SECRET, DATABASE_SECRET, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, PRIVACY_POLICY_URL, GIT_REPO_BASE_URL, REDIS_URL, PORT, DEBUG } = process.env;
export const config = { export default {
DISCORD_TOKEN, DISCORD_TOKEN,
DISCORD_CLIENT_ID, DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_CLIENT_SECRET,
@@ -21,9 +22,3 @@ export const config = {
DEV_MODE: process.env.npm_lifecycle_event == 'dev', DEV_MODE: process.env.npm_lifecycle_event == 'dev',
DEBUG: DEBUG == 'true' DEBUG: DEBUG == 'true'
}; };
for (const [key, val] of Object.entries(config)) {
if (val === undefined) {
throw new Error(`${key} is undefined in config`);
}
}
+17
View File
@@ -0,0 +1,17 @@
// External Imports
import { CommandClient, Event, Guild, User } from 'athena-prime';
// Internal Imports
import { Database } from '../shared/Database';
// ---------------
class GuildBanRemoveEvent extends Event<CommandClient> {
event: string = 'guildBanRemove' as const;
async handle(context: CommandClient<any, any>, guild: Guild, user: User) {
await Database.deleteBan(user.id);
}
}
export default new GuildBanRemoveEvent('guildBanRemove');
+17
View File
@@ -0,0 +1,17 @@
// External Imports
import { CommandClient, Event, Guild } from 'athena-prime';
// Internal Imports
import { Database } from '../shared/Database';
// ---------------
class GuildCreateEvent extends Event<CommandClient> {
event: string = 'guildCreate' as const;
async handle(context: CommandClient<any, any>, guild: Guild) {
await Database.GetOrCreateSettings(guild.id);
}
}
export default new GuildCreateEvent('guildCreate');
+24
View File
@@ -0,0 +1,24 @@
// External Imports
import { CommandClient, Event, Guild, Member } from 'athena-prime';
// Internal Imports
import { Database } from '../shared/Database';
import { getE621Alts } from '../utils';
// ---------------
class GuildMemberAddEvent extends Event<CommandClient> {
event: string = 'guildMemberAdd' as const;
async handle(context: CommandClient<any, any>, guild: Guild, member: Member) {
const settings = await Database.GetOrCreateSettings(guild.id);
// TODO: Seperate this into its own function and create a handleWelcomeMessage() function.
const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`;
if (!settings.moderator_channel_id || !content.includes('[BANNED]')) return;
context.createMessage(settings.moderator_channel_id, content);
}
}
export default new GuildMemberAddEvent('guildMemberAdd');
-94
View File
@@ -1,94 +0,0 @@
import { APIEmbedField, APIRole, AuditLogEvent, EmbedBuilder, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js';
import { Database } from '../shared/Database';
import { formatChanges, formatExtras, formatSnowflake, getTargetType } from '../utils';
const IGNORED_ACTIONS = [
AuditLogEvent.MemberMove,
// Handled by automod.
AuditLogEvent.AutoModerationFlagToChannel
];
export async function handleAuditLogCreate(entry: GuildAuditLogsEntry, guild: Guild) {
if (!await shouldLog(entry, guild)) return;
const settings = await Database.getGuildSettings(guild.id);
if (!settings || !settings.audit_logs_channel_id) return;
const channel = await guild.channels.fetch(settings.audit_logs_channel_id);
if (!channel || !channel.isSendable()) return;
const fields: APIEmbedField[] = [
{
name: 'Actor',
value: `<@${entry.executorId}>`,
inline: true
}
];
if (entry.targetId) {
const targetType = getTargetType(entry.action);
fields.push({
name: 'Target',
value: formatSnowflake(entry.targetId, targetType),
inline: true
});
}
if (entry.reason) {
fields.push({
name: 'Reason',
value: entry.reason,
inline: true
});
}
if (entry.changes && entry.changes.length > 0) {
fields.push({
name: 'Changes',
value: formatChanges(entry),
inline: false
});
}
if (entry.extra) {
fields.push({
name: 'Options',
value: formatExtras(entry, guild),
inline: false
});
}
const embed = new EmbedBuilder()
.setTitle(Object.keys(AuditLogEvent)[Object.values(AuditLogEvent).indexOf(entry.action)])
.setTimestamp(Number(SnowflakeUtil.decode(entry.id).timestamp))
.addFields(...fields);
channel.send({ embeds: [embed] });
}
async function shouldLog(entry: GuildAuditLogsEntry, guild: Guild): Promise<boolean> {
if (!entry.executorId) return true;
if (IGNORED_ACTIONS.some(a => entry.action == a)) return false;
if (entry.action == AuditLogEvent.MemberRoleUpdate) {
return await shouldLogRoleChanges(entry as GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild);
}
return true;
}
async function shouldLogRoleChanges(entry: GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild: Guild): Promise<boolean> {
for (const change of entry.changes) {
// Get role changes from the log.
const roles = (await Promise.all((change.new! as Pick<APIRole, 'id' | 'name'>[]).map(c => guild.roles.fetch(c.id))));
// Check if role is part of onboarding.
for (const role of roles) {
if (role && !role.flags.has(RoleFlags.InPrompt)) return true;
}
}
return false;
}
-6
View File
@@ -1,6 +0,0 @@
import { GuildBan } from 'discord.js';
import { Database } from '../shared/Database';
export async function handleBanRemove(ban: GuildBan) {
await Database.removeBan(ban.user.id);
}
-10
View File
@@ -1,10 +0,0 @@
import { Guild } from 'discord.js';
import { Database } from '../shared/Database';
export async function handleGuildCreate(guild: Guild) {
try {
if (!await Database.getGuildSettings(guild.id)) await Database.putGuild(guild.id);
} catch (e) {
console.error(e);
}
}
-23
View File
@@ -1,23 +0,0 @@
import { GuildMember, GuildTextBasedChannel } from 'discord.js';
import { Database } from '../shared/Database';
import { getE621Alts } from '../utils';
export async function handleMemberJoin(member: GuildMember) {
const guildSettings = await Database.getGuildSettings(member.guild.id);
if (guildSettings?.new_member_channel_id) {
const channel = await member.guild.channels.fetch(guildSettings.new_member_channel_id) as GuildTextBasedChannel;
if (channel) {
const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`;
channel.send(content).catch(console.error);
if (guildSettings.moderator_channel_id && content.includes('[BANNED]')) {
const modChannel = await member.guild.channels.fetch(guildSettings.moderator_channel_id) as GuildTextBasedChannel;
if (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error);
}
}
}
}
-11
View File
@@ -1,11 +0,0 @@
import { AnyThreadChannel } from 'discord.js';
export async function handleThreadCreate(thread: AnyThreadChannel, newlyCreated: boolean) {
try {
await thread.join();
} catch (e) {
console.error('Failed to join thread:');
console.error(e);
}
}
-46
View File
@@ -1,46 +0,0 @@
import { Guild, GuildMember, GuildTextBasedChannel, time, VoiceBasedChannel, VoiceState } from 'discord.js';
import { Database } from '../shared/Database';
export async function handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState) {
// The logChannel declaration being inside is purposeful, as this event is fired a lot for users talking.
if (newState.channelId != null && oldState.channelId != null && newState.channelId != oldState.channelId) {
const logChannel = await getVoiceLogsChannel(newState.guild);
if (!logChannel) return;
await sendMovedMessage(logChannel, newState.member!, oldState.channel!, newState.channel!);
} else if (oldState.channelId == null && newState.channelId != null) {
const logChannel = await getVoiceLogsChannel(newState.guild);
if (!logChannel) return;
await sendJoinMessage(logChannel, newState.member!, newState.channel!);
} else if (newState.channelId == null && oldState.channelId != null) {
const logChannel = await getVoiceLogsChannel(newState.guild);
if (!logChannel) return;
await sendLeftMessage(logChannel, newState.member!, oldState.channel!);
}
}
async function sendJoinMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) {
await channel.send(`${member} joined ${voiceChannel} at ${time()}`);
}
async function sendLeftMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) {
await channel.send(`${member} left ${voiceChannel} at ${time()}`);
}
async function sendMovedMessage(channel: GuildTextBasedChannel, member: GuildMember, oldVoiceChannel: VoiceBasedChannel, newVoiceChannel: VoiceBasedChannel) {
await channel.send(`${member} moved from ${oldVoiceChannel} to ${newVoiceChannel} at ${time()}`);
}
async function getVoiceLogsChannel(guild: Guild): Promise<GuildTextBasedChannel | undefined> {
const settings = await Database.getGuildSettings(guild.id);
if (!settings || !settings.voice_logs_channel_id) return;
const channel = await guild.channels.fetch(settings.voice_logs_channel_id);
if (!channel || !channel.isSendable()) return;
return channel as GuildTextBasedChannel;
}
+13 -7
View File
@@ -1,7 +1,13 @@
export * from './handle-audit-log-create'; // export * from './handle-audit-log-create';
export * from './handle-ban-remove'; import guildBanRemove from './guildBanRemove';
export * from './handle-guild-create'; import guildCreate from './guildCreate';
export * from './handle-member-join'; import guildMemberAdd from './guildMemberAdd';
export * from './handle-message'; import ready from './ready';
export * from './handle-thread-create'; // export * from './handle-message';
export * from './handle-voice-state-update'; import threadCreate from './threadCreate';
import voiceChannelJoin from './voiceChannelJoin';
import voiceChannelLeft from './voiceChannelLeft';
import voiceChannelSwitch from './voiceChannelSwitch';
export default [guildBanRemove, guildCreate, guildMemberAdd, ready, threadCreate, voiceChannelJoin, voiceChannelLeft, voiceChannelSwitch];
+17
View File
@@ -0,0 +1,17 @@
// External Imports
import { CommandClient, Event, Guild } from 'athena-prime';
// ---------------
class ReadyEvent extends Event<CommandClient> {
event: string = 'ready' as const;
async handle(context: CommandClient<any, any>, guild: Guild) {
await context.deployCommands();
// :3 - This is non-negotiable.
context.setCustomActivity('Hugging my favourite bat: nix :3');
}
}
export default new ReadyEvent('ready');
+14
View File
@@ -0,0 +1,14 @@
// External Imports
import { CommandClient, Event, ThreadChannel } from 'athena-prime';
// ---------------
class ThreadCreateEvent extends Event<CommandClient> {
event: string = 'threadCreate' as const;
async handle(context: CommandClient<any, any>, channel: ThreadChannel) {
await context.joinThread(channel.id);
}
}
export default new ThreadCreateEvent('threadCreate');
+38
View File
@@ -0,0 +1,38 @@
// External Imports
import { CommandClient, Event, Member, GuildVoiceChannel } from "athena-prime";
// Internal Imports
import { Database } from "../shared/Database";
import { SetSeverity } from "../utils";
// ---------------
class VoiceChannelJoinEvent extends Event<CommandClient> {
event: string = 'voiceChannelJoin' as const;
async handle(context: CommandClient<any, any>, member: Member, channel: GuildVoiceChannel) {
const settings = await Database.GetOrCreateSettings(channel.guild.id);
if (!settings.voice_logs_channel_id) return;
await context.createMessage(settings.voice_logs_channel_id, {
embed: {
...SetSeverity('success'),
author: {
name: member.username,
icon_url: member.avatarURL
},
fields: [
{ name: 'Member', value: `${member.username} (${member.mention})`, inline: true },
{ name: 'Channel', value: `${channel.mention}`, inline: true },
{ name: 'Time', value: `<t:${new Date().toISOString()}>`, inline: true },
],
title: 'Joined Voice Channel'
}
});
}
}
export default new VoiceChannelJoinEvent('voiceChannelJoin');
+38
View File
@@ -0,0 +1,38 @@
// External Imports
import { CommandClient, Event, Member, GuildVoiceChannel } from "athena-prime";
// Internal Imports
import { Database } from "../shared/Database";
import { SetSeverity } from "../utils";
// ---------------
class VoiceChannelLeftEvent extends Event<CommandClient> {
event: string = 'voiceChannelLeft' as const;
async handle(context: CommandClient<any, any>, member: Member, channel: GuildVoiceChannel) {
const settings = await Database.GetOrCreateSettings(channel.guild.id);
if (!settings.voice_logs_channel_id) return;
await context.createMessage(settings.voice_logs_channel_id, {
embed: {
author: {
name: member.username,
icon_url: member.avatarURL
},
...SetSeverity('error'),
fields: [
{ name: 'Member', value: `${member.username} (${member.mention})`, inline: true },
{ name: 'Channel', value: `${channel.mention}`, inline: true },
{ name: 'Time', value: `<t:${new Date().toISOString()}>`, inline: true },
],
title: 'Left Voice Channel'
}
});
}
}
export default new VoiceChannelLeftEvent('voiceChannelLeft');
+38
View File
@@ -0,0 +1,38 @@
// External Imports
import { CommandClient, Event, Member, GuildVoiceChannel } from "athena-prime";
// Internal Imports
import { Database } from "../shared/Database";
import { SetSeverity } from "../utils";
// ---------------
class VoiceChannelSwitchEvent extends Event<CommandClient> {
event: string = 'voiceChannelSwitch' as const;
async handle(context: CommandClient<any, any>, member: Member, channel: GuildVoiceChannel, old: GuildVoiceChannel) {
const settings = await Database.GetOrCreateSettings(channel.guild.id);
if (!settings.voice_logs_channel_id) return;
await context.createMessage(settings.voice_logs_channel_id, {
embed: {
...SetSeverity('warning'),
author: {
name: member.username,
icon_url: member.avatarURL
},
fields: [
{ name: 'Member', value: `${member.username} (${member.mention})`, inline: true },
{ name: 'Channel', value: `${old.mention} -> ${channel.mention}`, inline: true },
{ name: 'Time', value: `<t:${new Date().toISOString()}>`, inline: true },
],
title: 'Moved Voice Channel'
}
});
}
}
export default new VoiceChannelSwitchEvent('voiceChannelSwitch');
+7 -6
View File
@@ -1,12 +1,15 @@
import { Client } from 'discord.js'; // External Imports
import { CommandClient } from 'athena-prime';
// Internal Imports
import { Task } from '../types'; import { Task } from '../types';
import ScheduledTasks from './tasks'; import ScheduledTasks from './tasks';
class Scheduler { class Scheduler {
private tasks = new Map<Task, NodeJS.Timeout>(); private tasks = new Map<Task, NodeJS.Timeout>();
private context: Client; private context: CommandClient;
constructor(context: Client) { constructor(context: CommandClient) {
this.context = context; this.context = context;
} }
@@ -18,6 +21,4 @@ class Scheduler {
} }
} }
export { export { Scheduler, ScheduledTasks }
ScheduledTasks, Scheduler
};
@@ -1,12 +1,17 @@
import { Client } from 'discord.js'; // External Imports
import { CommandClient } from 'athena-prime';
// Internal Imports
import { Task } from '../../types'; import { Task } from '../../types';
import { checkExpiredBans } from '../../utils'; import { checkExpiredBans } from '../../utils';
// ---------------
class CheckExpiredBansTask implements Task { class CheckExpiredBansTask implements Task {
interval: number = 300000; interval: number = 300000;
firstRun: boolean = true; firstRun: boolean = true;
async handle(context: Client): Promise<void> { async handle(context: CommandClient) {
await checkExpiredBans(context); await checkExpiredBans(context);
} }
} }
@@ -1,12 +1,17 @@
import { Client } from 'discord.js'; // External Imports
import { CommandClient } from 'athena-prime';
// Internal Imports
import { Task } from '../../types'; import { Task } from '../../types';
import { closeOldTickets } from '../../utils'; import { closeOldTickets } from '../../utils';
// ---------------
class CloseStaleTicketsTask implements Task { class CloseStaleTicketsTask implements Task {
interval: number = 3.6e6; interval: number = 3.6e6;
firstRun: boolean = true; firstRun: boolean = true;
async handle(context: Client): Promise<void> { async handle(context: CommandClient) {
await closeOldTickets(context); await closeOldTickets(context);
} }
} }
@@ -1,3 +1,7 @@
// External Imports
import { CommandClient } from 'athena-prime';
// Internal Imports
import { Task } from '../../types'; import { Task } from '../../types';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
@@ -5,7 +9,7 @@ class PruneOldMessagesTask implements Task {
interval: number = 3.6e6; interval: number = 3.6e6;
firstRun: boolean = true; firstRun: boolean = true;
async handle(): Promise<void> { async handle(context: CommandClient) {
await Database.pruneOldMessages(); await Database.pruneOldMessages();
} }
} }
+15 -12
View File
@@ -2,7 +2,7 @@ import path from 'path';
import { open, Database as SqliteDatabase } from 'sqlite'; import { open, Database as SqliteDatabase } from 'sqlite';
import sqlite3 from 'sqlite3'; import sqlite3 from 'sqlite3';
import { Message } from '../events'; import { Message } from '../events';
import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, RoleButton, TicketMessage, TicketPhrase } from '../types'; import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, RoleButton, TicketMessage, TicketPhrase } from '../types';
import { deserializeMessage, serializeMessage, wait } from '../utils'; import { deserializeMessage, serializeMessage, wait } from '../utils';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { Encrypter } from './Encrypter'; import { Encrypter } from './Encrypter';
@@ -13,7 +13,7 @@ export const enum PrivateHelpTicketStatus {
} }
export class Database { export class Database {
private static db: SqliteDatabase; static db: SqliteDatabase;
static async open(file: string): Promise<void> { static async open(file: string): Promise<void> {
if (Database.db) return; if (Database.db) return;
@@ -28,7 +28,6 @@ export class Database {
console.log('SQLite database opened'); console.log('SQLite database opened');
await Database.ensure(); await Database.ensure();
await Database.migrate(); await Database.migrate();
} }
@@ -75,15 +74,19 @@ export class Database {
//#region Guild Settings //#region Guild Settings
static async getGuildSettings(guildId: string): Promise<GuildSettings> { static async GetOrCreateSettings(guildId: string): Promise<GuildSettings> {
return await Database.db.get<GuildSettings>('SELECT * FROM settings WHERE guild_id = ?', guildId) as GuildSettings; await Database.db.run(
} 'INSERT OR IGNORE INTO settings (guild_id) VALUES (?)',
guildId
);
static async putGuild(guildId: string) { return await Database.db.get<GuildSettings>(
await Database.db.run('INSERT INTO settings(guild_id) VALUES (?)', guildId); 'SELECT * FROM settings WHERE guild_id = ?',
} guildId
) as GuildSettings;
}
static async updateGuildSettings(guildId: string, key: GuildSetting, value: string) { static async updateSettings(guildId: string, key: Exclude<keyof GuildSettings, 'guild_id'>, value: string) {
await Database.db.run(`UPDATE settings SET ${key} = ? WHERE guild_id = ?`, value, guildId); await Database.db.run(`UPDATE settings SET ${key} = ? WHERE guild_id = ?`, value, guildId);
} }
@@ -332,10 +335,10 @@ export class Database {
} }
static async pruneExpiredBans(date: Date) { static async pruneExpiredBans(date: Date) {
await Database.db.all<Ban[]>('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date);
} }
static async removeBan(userId: string) { static async deleteBan(userId: string) {
await Database.db.run('DELETE from bans WHERE user_id_hash = ?', Encrypter.hash(userId)); await Database.db.run('DELETE from bans WHERE user_id_hash = ?', Encrypter.hash(userId));
} }
-1
View File
@@ -33,7 +33,6 @@ export type GuildSettings = {
site_breaker_role_id?: string site_breaker_role_id?: string
} }
type GuildSetting = Exclude<keyof GuildSettings, 'guild_id'>
export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels'; export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels';
export type TicketMessage = { export type TicketMessage = {
+2 -2
View File
@@ -1,8 +1,8 @@
import { Client } from 'discord.js'; import { CommandClient } from 'athena-prime';
export interface Task { export interface Task {
interval: number; interval: number;
firstRun: boolean; firstRun: boolean;
handle(context: Client): Promise<void>; handle(context: CommandClient): Promise<void>;
}; };
+10 -18
View File
@@ -1,7 +1,10 @@
import { Guild } from 'discord.js'; // External Imports
import { Database } from '../shared/Database'; import { Guild } from 'athena-prime';
// Internal Imports
import { userIsBanned } from './e621-utils'; import { userIsBanned } from './e621-utils';
import { config } from '../config'; import config from '../config';
import { Database } from '../shared/Database';
export type AltData = { export type AltData = {
type: 'e621' | 'discord' type: 'e621' | 'discord'
@@ -13,7 +16,6 @@ export type AltData = {
export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> { export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> {
const e621UserIds = await Database.getE621Ids(discordId); const e621UserIds = await Database.getE621Ids(discordId);
const toIgnore = ignore.concat(e621UserIds); const toIgnore = ignore.concat(e621UserIds);
let content = ''; let content = '';
@@ -22,7 +24,6 @@ export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ig
if (ignore.includes(e621Id)) continue; if (ignore.includes(e621Id)) continue;
const alts = await getDiscordAlts(e621Id, guild, depth + 1, toIgnore); const alts = await getDiscordAlts(e621Id, guild, depth + 1, toIgnore);
const banned = await userIsBanned(e621Id); const banned = await userIsBanned(e621Id);
content += `${' '.repeat((depth - 1) * 2)}- ${config.E621_BASE_URL}/users/${e621Id}${banned ? ' [BANNED]' : ''}\n${alts}`; content += `${' '.repeat((depth - 1) * 2)}- ${config.E621_BASE_URL}/users/${e621Id}${banned ? ' [BANNED]' : ''}\n${alts}`;
@@ -38,13 +39,10 @@ export async function getDiscordAlts(e621Id: number, guild: Guild, depth = 1, ig
for (const discordId of discordIds) { for (const discordId of discordIds) {
const alts = await getE621Alts(discordId, guild, depth + 1, ignore); const alts = await getE621Alts(discordId, guild, depth + 1, ignore);
let banned = false; let banned = false;
// It's either this or fetch all the bans and sift through them for every discord alt. // It's either this or fetch all the bans and sift through them for every discord alt.
try { banned = !!(await guild.getBan(discordId));
banned = !!(await guild.bans.fetch(discordId));
} catch (e) { }
content += `${' '.repeat((depth - 1) * 2)}- <@${discordId}> (${discordId})${banned ? ' [BANNED]' : ''}\n${alts}`; content += `${' '.repeat((depth - 1) * 2)}- <@${discordId}> (${discordId})${banned ? ' [BANNED]' : ''}\n${alts}`;
} }
@@ -69,14 +67,13 @@ async function getE621AltData(discordId: string, guild: Guild | null, depth = 1,
// It's either this or fetch all the bans and sift through them for every discord alt. // It's either this or fetch all the bans and sift through them for every discord alt.
try { try {
banned = guild ? !!(await guild.bans.fetch(discordId)) : false; banned = guild ? !!(await guild.getBan(discordId)) : false;
} catch (e) { } } catch (e) { }
const data: AltData = { type: 'discord', thisId: discordId, banned, alts: [] }; const data: AltData = { type: 'discord', thisId: discordId, banned, alts: [] };
for (const e621Id of e621UserIds) { for (const e621Id of e621UserIds) {
if (ignore.includes(e621Id)) continue; if (ignore.includes(e621Id)) continue;
data.alts.push(await getDiscordAltData(e621Id, guild, depth + 1, toIgnore)); data.alts.push(await getDiscordAltData(e621Id, guild, depth + 1, toIgnore));
} }
@@ -85,12 +82,9 @@ async function getE621AltData(discordId: string, guild: Guild | null, depth = 1,
async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1, ignore: number[] = []): Promise<AltData> { async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1, ignore: number[] = []): Promise<AltData> {
const discordIds = await Database.getDiscordIds(e621Id); const discordIds = await Database.getDiscordIds(e621Id);
const data: AltData = { type: 'e621', thisId: e621Id, banned: await userIsBanned(e621Id), alts: [] }; const data: AltData = { type: 'e621', thisId: e621Id, banned: await userIsBanned(e621Id), alts: [] };
for (const discordId of discordIds) { for (const discordId of discordIds) data.alts.push(await getE621AltData(discordId, guild, depth + 1, ignore));
data.alts.push(await getE621AltData(discordId, guild, depth + 1, ignore));
}
return data; return data;
} }
@@ -98,9 +92,7 @@ async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1,
export function e621IdsFromAltData(altData: AltData, data: number[] = []) { export function e621IdsFromAltData(altData: AltData, data: number[] = []) {
if (altData.type == 'e621' && !data.includes(altData.thisId as number)) data.push(altData.thisId as number); if (altData.type == 'e621' && !data.includes(altData.thisId as number)) data.push(altData.thisId as number);
for (const alt of altData.alts) { for (const alt of altData.alts) e621IdsFromAltData(alt, data);
e621IdsFromAltData(alt, data);
}
return data; return data;
} }
+11 -11
View File
@@ -1,22 +1,22 @@
import { Client } from 'discord.js'; // External Imports
import { CommandClient } from 'athena-prime';
// Internal Imports
import config from '../config';
import { Database } from '../shared/Database'; import { Database } from '../shared/Database';
import { config } from '../config'; import { Ban } from '../types';
export async function checkExpiredBans(client: Client) { // ---------------
const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!);
export async function checkExpiredBans(client: CommandClient) {
const guild = client.guilds.get(config.DISCORD_GUILD_ID!);
if (!guild) return; if (!guild) return;
const date = new Date(); const date = new Date();
for (const ban of await Database.getExpiredBans(date)) { for (const ban of await Database.getExpiredBans(date)) {
try { await guild.unbanMember(ban.user_id, 'Ban Expired.');
await guild.bans.remove(ban.user_id);
} catch (e) {
console.error(`Error unbanning user: ${ban.user_id}`);
console.error(e);
}
} }
await Database.pruneExpiredBans(date); await Database.db.all<Ban[]>('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date);
} }
+7 -11
View File
@@ -1,18 +1,15 @@
import { APIEmbed, Client } from 'discord.js'; import { CommandClient, Constants } from 'athena-prime';
type EmbedSeverity = type Severity = 'info' | 'warning' | 'error' | 'success';
| 'default'
| 'info'
| 'warning'
| 'error'
| 'success'
export function CreateDefaultEmbed(context: Client): APIEmbed { export function CreateDefaultEmbed(context: CommandClient): Constants.APIEmbed {
if (!context.user) return {}; if (!context.user) return {};
return { return {
color: 0x014995,
footer: { footer: {
icon_url: context.user.avatarURL()!, icon_url: context.user.avatarURL,
text: context.user.username text: context.user.username
}, },
@@ -20,9 +17,8 @@ export function CreateDefaultEmbed(context: Client): APIEmbed {
}; };
} }
export function SetSeverity(severity: EmbedSeverity): APIEmbed { export function SetSeverity(severity: Severity): Constants.APIEmbed {
switch (severity) { switch (severity) {
case 'default': return { color: 0x014995 };
case 'info': return { color: 0x5865F2 }; case 'info': return { color: 0x5865F2 };
case 'warning': return { color: 0xFEE75C }; case 'warning': return { color: 0xFEE75C };
case 'error': return { color: 0xED4245 }; case 'error': return { color: 0xED4245 };
-28
View File
@@ -1,28 +0,0 @@
import { ChatInputCommandInteraction, GuildMember, ModalSubmitInteraction, UserContextMenuCommandInteraction } from 'discord.js';
import { Database } from '../shared/Database';
import { E621User } from '../types';
import { getE621User } from './e621-utils';
export async function syncName(interaction: ChatInputCommandInteraction | UserContextMenuCommandInteraction | ModalSubmitInteraction, member: GuildMember, id: number | null) {
if (member.roles.highest.comparePositionTo(member.guild.members.me!.roles.highest) > 0) {
const res = interaction.user.id == member.id ? 'your' : 'their';
return interaction.editReply(`I am unable to set ${res} nickname as ${res} role is higher than mine.`);
}
const availableIds = await Database.getE621Ids(interaction.user.id);
let e621User: E621User | null;
if (!id || !availableIds.includes(id)) {
e621User = await getE621User(availableIds[0]);
} else {
e621User = await getE621User(id);
}
if (!e621User) {
return interaction.editReply("Couldn't figure out what your name was. Please contact an administrator.");
}
await member.setNickname(e621User.name);
interaction.editReply(`Nickname set to: ${e621User.name}`);
}
+10 -11
View File
@@ -1,26 +1,25 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, ThreadChannel, UserContextMenuCommandInteraction } from 'discord.js'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, UserContextMenuCommandInteraction } from 'discord.js';
import { Database } from '../shared/Database'; import { Database } from '../shared/Database';
import { createTextInput, createYesNoMenu } from './modal-utils'; import { createTextInput, createYesNoMenu } from './modal-utils';
export async function closeOldTickets(client: Client) { import { CommandClient, ThreadChannel } from 'athena-prime';
// TODO: Handle Modmail Errors.
export async function closeOldTickets(client: CommandClient) {
for (const ticket of await Database.getAllOpenPrivateHelpTickets()) { for (const ticket of await Database.getAllOpenPrivateHelpTickets()) {
try { const thread = client.getChannel(ticket.thread_id) as ThreadChannel;
const thread = await client.channels.fetch(ticket.thread_id) as ThreadChannel; const latestMessage = (await (thread.getMessages({ limit: 1 }))).at(0);
const latestMessage = (await thread.messages.fetch({ limit: 1 })).at(0);
if (latestMessage && latestMessage.createdTimestamp <= Date.now() - 432e6) { if (latestMessage && latestMessage.timestamp <= Date.now() - 432e6) {
await Database.closePrivateHelpTicket(thread.id); await Database.closePrivateHelpTicket(thread.id);
await thread.send('This ticket has been closed due to inactivity.'); await thread.createMessage('This ticket has been closed due to inactivity.');
thread.edit({ thread.edit({
archived: true, archived: true,
locked: true locked: true
}); });
} }
} catch (e) {
console.error('Error closing ticket due to inactivity:');
console.error(e);
}
} }
} }