From 76a7335c7c4e71b296815e33a64e36678ecab5f7 Mon Sep 17 00:00:00 2001 From: Maufeat Date: Fri, 18 Jul 2025 22:52:32 +0200 Subject: [PATCH] init --- .gitignore | 140 ++++++++++ README.md | 3 + package-lock.json | 382 +++++++++++++++++++++++++++ package.json | 18 ++ src/client.js | 33 +++ src/config.js | 2 + src/functions/description_watcher.js | 162 ++++++++++++ src/functions/log_parser.js | 107 ++++++++ src/functions/release_watcher.js | 116 ++++++++ 9 files changed, 963 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/client.js create mode 100644 src/config.js create mode 100644 src/functions/description_watcher.js create mode 100644 src/functions/log_parser.js create mode 100644 src/functions/release_watcher.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10fc173 --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite logs files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a33504 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Eden Discord Bot + +No readme yet, no ci/cd yet. If you make changes and want them to be reviewed, create a PR and ping Maufeat \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5693234 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,382 @@ +{ + "name": "eden-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eden-bot", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "discord.js": "^14.21.0", + "dotenv": "^17.0.0", + "node-fetch": "^3.3.2" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.2.tgz", + "integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.1", + "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.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.5.1.tgz", + "integrity": "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw==", + "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.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "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==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "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==", + "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==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "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==", + "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==", + "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==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.0.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz", + "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "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==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.13", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.13.tgz", + "integrity": "sha512-FELWJRgLVQuR7Az8RhdEZE0k6QNjSW9PCUcU1iyP2Gke8HrJmnMceSS9pD93UM64s3tvZzJPajpPLjWZJylf4g==" + }, + "node_modules/discord.js": { + "version": "14.21.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.21.0.tgz", + "integrity": "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ==", + "dependencies": { + "@discordjs/builders": "^1.11.2", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.1", + "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" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.0.tgz", + "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "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==" + }, + "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==" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..904cd79 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "eden-bot", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "discord.js": "^14.21.0", + "dotenv": "^17.0.0", + "node-fetch": "^3.3.2" + } +} diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..fe30e19 --- /dev/null +++ b/src/client.js @@ -0,0 +1,33 @@ +import './config.js'; + +import { Client, GatewayIntentBits } from 'discord.js'; +import parseLog from './functions/log_parser.js'; +import initializeReleaseWatcher from './functions/release_watcher.js'; +import initializeDescriptionWatcher from './functions/description_watcher.js'; + +const { DISCORD_TOKEN } = process.env; + +if (!DISCORD_TOKEN) { + console.error('Fatal: DISCORD_TOKEN is not set in the .env file.'); + process.exit(1); +} + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +client.once('ready', () => { + console.log(`Logged in as ${client.user.tag}!`); + + initializeReleaseWatcher(client); + initializeDescriptionWatcher(client); +}); + +client.on('messageCreate', parseLog); + +// Log in to Discord +client.login(DISCORD_TOKEN); \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..8ca876f --- /dev/null +++ b/src/config.js @@ -0,0 +1,2 @@ +import dotenv from 'dotenv'; +dotenv.config(); \ No newline at end of file diff --git a/src/functions/description_watcher.js b/src/functions/description_watcher.js new file mode 100644 index 0000000..11268cd --- /dev/null +++ b/src/functions/description_watcher.js @@ -0,0 +1,162 @@ +import fetch from 'node-fetch'; +import { ChannelType } from 'discord.js'; + +const { + CHANNEL_ID, + GITHUB_REPO: REPO = 'Eden-CI/PR', + GITHUB_TOKEN, + EDEN_TOKEN, + POLL_INTERVAL_MS, +} = process.env; + +const POLL_INTERVAL = parseInt(POLL_INTERVAL_MS) || 10 * 60 * 1000; +let isCheckingDescriptions = false; + + +function disabled() { + console.log('[Description Watcher] Service is disabled due to missing environment variables.'); + console.log('[Description Watcher] CHANNEL_ID = ' + CHANNEL_ID + ' GITHUB_TOKEN = ' + GITHUB_TOKEN + ' EDEN_TOKEN = ' + EDEN_TOKEN); +} + +const wait = ms => new Promise(res => setTimeout(res, ms)); + +async function buildThreadContent(buildNumber) { + let release; + let relRes = await fetch(`https://api.github.com/repos/${REPO}/releases/tags/${buildNumber}`, { + headers: { + 'User-Agent': 'description-watcher', + Authorization: `token ${GITHUB_TOKEN}`, + }, + }); + + if (relRes.status === 404) { + // Tag lookup failed – list the latest 100 releases and find one whose tag_name == buildNumber + const listRes = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=100`, { + headers: { + 'User-Agent': 'description-watcher', + Authorization: `token ${GITHUB_TOKEN}`, + }, + }); + if (listRes.ok) { + const releases = await listRes.json(); + release = releases.find(r => r.tag_name === String(buildNumber)); + } + } else if (relRes.ok) { + release = await relRes.json(); + } + + if (!release) throw new Error(`No release with tag "${buildNumber}" found`); + + // 2️⃣ Fetch the self‑hosted Eden PR to get its body / description + let desc = 'No description available.'; + const prRes = await fetch( + `https://git.eden-emu.dev/api/v1/repos/eden-emu/eden/pulls/${buildNumber}`, + { headers: { Authorization: `token ${EDEN_TOKEN}` } } + ); + if (prRes.ok) { + const pr = await prRes.json(); + desc = pr.body || pr.description || desc; + } + + // 3️⃣ Assemble identical markdown to announceRelease() + const title = `Build ${buildNumber}`; + const prUrl = `https://git.eden-emu.dev/eden-emu/eden/pulls/${buildNumber}`; + const dlUrl = release.html_url || ''; + let content = `**${title} - ${release.name}** + +${desc} + +🔗 [Go to pull request](${prUrl}) + +📝 [Go to downloads](${dlUrl})`; + if (content.length > 2_000) content = content.slice(0, 1_997) + '...'; + return content; +} + +async function syncThread(channel, buildNumber) { + let desiredContent; + try { + desiredContent = await buildThreadContent(buildNumber); + } catch (err) { + console.warn(`[Description‑Watcher] Skipping Build ${buildNumber}: ${err.message}`); + return; + } + + const threadName = `Build ${buildNumber}`; + + const { threads: active } = await channel.threads.fetchActive(); + const { threads: archived } = await channel.threads.fetchArchived(); + let thread = active.find(t => t.name === threadName) ?? archived.find(t => t.name === threadName); + if (!thread) { + console.warn(`[Description‑Watcher] No thread named "${threadName}"`); + return; + } + + // Un‑archive if needed so we can edit + const wasArchived = thread.archived; + if (wasArchived) await thread.setArchived(false, 'syncing release description'); + + const starter = await thread.fetchStarterMessage(); + if (!starter) { + console.warn(`[Description‑Watcher] Thread "${threadName}" missing starter message`); + return; + } + + if (starter.content.trim() === desiredContent.trim()) { + console.log(`[Description‑Watcher] "${threadName}" already up‑to‑date`); + } else { + await starter.edit(desiredContent); + console.log(`[Description‑Watcher] Updated description for "${threadName}"`); + } + + if (wasArchived) await thread.setArchived(true, 'restoring archived state'); +} + +async function checkDescriptions(client) { + if (isCheckingDescriptions) { + console.log('[Description‑Watcher] Skipping run – previous check still active'); + return; + } + isCheckingDescriptions = true; + + try { + const channel = await client.channels.fetch(CHANNEL_ID); + if (!channel || channel.type !== ChannelType.GuildForum) { + console.error(`[Description‑Watcher] Channel ${CHANNEL_ID} is not a forum`); + return; + } + + // Combine active + archived threads once; we only need their names + const threadNamePrefix = 'Build '; + const activeThreads = [...(await channel.threads.fetchActive()).threads.values()]; + const archivedThreads = [...(await channel.threads.fetchArchived()).threads.values()]; + const allThreads = [...activeThreads, ...archivedThreads]; + + for (const th of allThreads) { + if (!th.name.startsWith(threadNamePrefix)) continue; + const num = Number(th.name.slice(threadNamePrefix.length).trim()); + if (Number.isNaN(num)) { + console.warn(`[Description‑Watcher] Skipping invalid thread name: "${th.name}"`); + continue; + } + await syncThread(channel, num); + await wait(750); // tweak for rate‑limit comfort + } + } catch (err) { + console.error('[Description‑Watcher] Error during check:', err); + } finally { + isCheckingDescriptions = false; + } +} + + +function initializeDescriptionWatcher(client) { + console.log(`[Description Watcher] Service initialized. Watching ${REPO} every ${POLL_INTERVAL / 1000} seconds.`); + checkDescriptions(client); + setInterval(() => checkDescriptions(client), POLL_INTERVAL); +} + +const isEnabled = CHANNEL_ID && GITHUB_TOKEN && EDEN_TOKEN; +const moduleToExport = isEnabled ? initializeDescriptionWatcher : disabled; + +export default moduleToExport; \ No newline at end of file diff --git a/src/functions/log_parser.js b/src/functions/log_parser.js new file mode 100644 index 0000000..67b52a5 --- /dev/null +++ b/src/functions/log_parser.js @@ -0,0 +1,107 @@ +import fetch from 'node-fetch'; +import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; + +const IGNORE_ISSUE_PATTERNS = [ + /has a different file type \(unknown\) than its extension/i, + /Filesystem object at path=.* does not exist/i, +]; + + +export default async function parseLog(message) { + if (message.author.bot || message.attachments.size === 0) { + return; + } + + const attachment = message.attachments.find(att => + att.name.startsWith('eden_log') && att.name.endsWith('.txt') + ); + + if (!attachment) { + return; + } + + console.log(`Parsing log file: ${attachment.name} from ${message.author.tag}`); + + try { + const response = await fetch(attachment.url); + if (!response.ok) { + console.error(`Failed to fetch attachment: ${response.statusText}`); + return; + } + const text = await response.text(); + const lines = text.split(/\r?\n/); + + const specs = {}; + let gameName, gameVersion; + + for (const line of lines) { + if (/Host CPU:/.test(line)) specs.cpu = line.split('Host CPU:')[1].trim(); + if (/Host CPU Cores:/.test(line)) specs.cores = line.split('Host CPU Cores:')[1].trim(); + if (/Host CPU Threads:/.test(line)) specs.threads = line.split('Host CPU Threads:')[1].trim(); + if (/Host OS:/.test(line)) specs.os = line.split('Host OS:')[1].trim(); + if (/Host RAM:/.test(line)) specs.ram = line.split('Host RAM:')[1].trim(); + if (/Host Swap:/.test(line)) specs.swap = line.split('Host Swap:')[1].trim(); + if (/Render\.Vulkan.*Device:/.test(line)) specs.gpu = line.split('Device:')[1].trim(); + + // [ 0.479785] Frontend yuzu\main.cpp:SetFirmwareVersion:5135: Installed firmware: NintendoSDK Firmware for NX 19.0.1-1.0 + const fwMatch = line.match(/Installed firmware:[^\d]*(\d+\.\d+\.\d+(?:-[\d.]+)?)/); + if (fwMatch) specs.firmware = fwMatch[1]; + + const gameMatch = line.match(/Booting game:.*?\|\s*(.*?)\s+\(.*\)\s+\|\s*(.*)/); + if (gameMatch) { + gameName = gameMatch[1].trim(); + gameVersion = gameMatch[2].trim(); + } + } + + const embedTitle = gameName ? `[${specs.firmware ?? "Unknown FW"}] ${gameName} (v${gameVersion})` : 'Log Parser Summary'; + const embed = new EmbedBuilder() + .setTitle(embedTitle) + .setColor(0xbe41f5) + .setDescription(`Summary for **${attachment.name}**.`) + .addFields( + { name: 'User', value: message.author.tag, inline: false }, + { name: 'OS', value: specs.os || 'N/A', inline: false }, + { name: 'CPU', value: specs.cpu || 'N/A', inline: true }, + { name: 'GPU', value: specs.gpu || 'N/A', inline: true }, + { name: 'Cores', value: specs.cores || 'N/A', inline: true }, + { name: 'Threads', value: specs.threads || 'N/A', inline: true }, + { name: 'RAM', value: specs.ram || 'N/A', inline: true }, + { name: 'Swap', value: specs.swap || 'N/A', inline: true }, + ); + + const issues = lines.filter((l) => { + if (!/|/.test(l)) return false; + return !IGNORE_ISSUE_PATTERNS.some((pat) => pat.test(l)); + }); + + if (issues.length) { + const issuesToDisplay = issues + .slice(0, 3) + .map((l) => l.replace(/\[\s*[\d.]+\]\s*/, '').trim()); + + embed.addFields({ + name: `${issues.length} Issues Found`, + value: `\n\n\`\`\`\n${issuesToDisplay.join('\n')}\n\`\`\``, + }); + if (issues.length > 3) { + const remaining = issues.length - 3; + embed.setFooter({ text: `... and ${remaining} more issue(s) in the log.` }); + } + } else { + embed.addFields({ name: 'Issues', value: '✅ No critical or error issues found.' }); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('Download Log file') + .setStyle(ButtonStyle.Link) + .setURL(attachment.url) + ); + + await message.reply({ embeds: [embed], components: [row] }); + + } catch (err) { + console.error('Error in parseLog:', err); + } +} \ No newline at end of file diff --git a/src/functions/release_watcher.js b/src/functions/release_watcher.js new file mode 100644 index 0000000..e18644d --- /dev/null +++ b/src/functions/release_watcher.js @@ -0,0 +1,116 @@ +import fetch from 'node-fetch'; +import { ChannelType, MessageFlags } from 'discord.js'; + +const { + CHANNEL_ID, + GITHUB_REPO: REPO = 'Eden-CI/PR', + GITHUB_TOKEN, + EDEN_TOKEN, + POLL_INTERVAL_MS, +} = process.env; + +const POLL_INTERVAL = parseInt(POLL_INTERVAL_MS) || 10 * 60 * 1000; +let isCheckingReleases = false; + + +function disabled() { + console.log('[Release Watcher] Service is disabled due to missing environment variables.'); + console.log('[Release Watcher] CHANNEL_ID = '+CHANNEL_ID+' GITHUB_TOKEN = '+GITHUB_TOKEN+' EDEN_TOKEN = '+EDEN_TOKEN); +} + +async function announceRelease(release, channel, baseTag) { + try { + const prRes = await fetch( + `https://git.eden-emu.dev/api/v1/repos/eden-emu/eden/pulls/${baseTag}`, + { headers: { 'Authorization': `token ${EDEN_TOKEN}` } } + ); + let desc = 'No description available.'; + if (prRes.ok) { + const data = await prRes.json(); + desc = data.body || data.description || desc; + } + const title = `Build ${baseTag}`; + const prUrl = `https://git.eden-emu.dev/eden-emu/eden/pulls/${baseTag}`; + let content = `**${title} - ${release.name}**\n\n${desc}\n\n🔗 [Go to pull request](${prUrl})\n\n📝 [Go to downloads](${release.html_url})`; + if (content.length > 2000) content = content.slice(0, 1997) + '...'; + + await channel.threads.create({ + name: title, + autoArchiveDuration: 10080, + message: { content, flags: MessageFlags.SuppressEmbeds } + }); + console.log(`[Release Watcher] Created thread for ${title}`); + } catch (e) { + console.error(`[Release Watcher] Error announcing release ${release.tag_name}:`, e); + } +} + +async function checkReleases(client) { + if (isCheckingReleases) { + console.log('[Release Watcher] Skipping check: a check is already in progress.'); + return; + } + isCheckingReleases = true; + try { + const res = await fetch(`https://api.github.com/repos/${REPO}/releases`, { + headers: { 'User-Agent': 'release-watcher', 'Authorization': `token ${GITHUB_TOKEN}` } + }); + if (res.status === 403) { + console.error('[Release Watcher] GitHub API error: Rate limit or access denied.'); + return; + } + if (!res.ok) throw new Error(`GitHub API responded with status ${res.status}`); + + const allReleases = await res.json(); + if (!Array.isArray(allReleases)) return; + + const latestReleasesByTag = new Map(); + for (const release of allReleases) { + const baseTag = release.tag_name.split('-')[0]; + if (!latestReleasesByTag.has(baseTag)) { + latestReleasesByTag.set(baseTag, release); + } + } + + const allUniqueReleases = Array.from(latestReleasesByTag.values()); + allUniqueReleases.sort((a, b) => parseInt(b.tag_name) - parseInt(a.tag_name)); + const toProcess = allUniqueReleases.slice(0, 10).reverse(); + + const channel = await client.channels.fetch(CHANNEL_ID); + if (channel.type !== ChannelType.GuildForum) { + console.error(`[Release Watcher] Channel ${CHANNEL_ID} is not a Forum Channel.`); + return; + } + + const activeThreads = await channel.threads.fetchActive(); + const archivedThreads = await channel.threads.fetchArchived(); + const existingThreadNames = new Set([ + ...activeThreads.threads.map(t => t.name), + ...archivedThreads.threads.map(t => t.name) + ]); + + for (const release of toProcess) { + const baseTag = release.tag_name.split('-')[0]; + const title = `Build ${baseTag}`; + if (!existingThreadNames.has(title)) { + await announceRelease(release, channel, baseTag); + existingThreadNames.add(title); + } + } + } catch (e) { + console.error('[Release Watcher] Error in checkReleases:', e); + } finally { + isCheckingReleases = false; + } +} + +function initializeReleaseWatcher(client) { + console.log(`[Release Watcher] Service initialized. Watching ${REPO} every ${POLL_INTERVAL / 1000} seconds.`); + checkReleases(client); + setInterval(() => checkReleases(client), POLL_INTERVAL); +} + +const isEnabled = true; +const moduleToExport = isEnabled ? initializeReleaseWatcher : disabled; + +export default moduleToExport; \ No newline at end of file