This commit is contained in:
Maufeat 2025-07-18 22:52:32 +02:00
commit 76a7335c7c
9 changed files with 963 additions and 0 deletions

140
.gitignore vendored Normal file
View file

@ -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-*

3
README.md Normal file
View file

@ -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

382
package-lock.json generated Normal file
View file

@ -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
}
}
}
}
}

18
package.json Normal file
View file

@ -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"
}
}

33
src/client.js Normal file
View file

@ -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);

2
src/config.js Normal file
View file

@ -0,0 +1,2 @@
import dotenv from 'dotenv';
dotenv.config();

View file

@ -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 selfhosted 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(`[DescriptionWatcher] 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(`[DescriptionWatcher] No thread named "${threadName}"`);
return;
}
// Unarchive 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(`[DescriptionWatcher] Thread "${threadName}" missing starter message`);
return;
}
if (starter.content.trim() === desiredContent.trim()) {
console.log(`[DescriptionWatcher] "${threadName}" already uptodate`);
} else {
await starter.edit(desiredContent);
console.log(`[DescriptionWatcher] Updated description for "${threadName}"`);
}
if (wasArchived) await thread.setArchived(true, 'restoring archived state');
}
async function checkDescriptions(client) {
if (isCheckingDescriptions) {
console.log('[DescriptionWatcher] 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(`[DescriptionWatcher] 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(`[DescriptionWatcher] Skipping invalid thread name: "${th.name}"`);
continue;
}
await syncThread(channel, num);
await wait(750); // tweak for ratelimit comfort
}
} catch (err) {
console.error('[DescriptionWatcher] 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;

107
src/functions/log_parser.js Normal file
View file

@ -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 <Info> 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 (!/<Error>|<Critical>/.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);
}
}

View file

@ -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;