first commit
This commit is contained in:
50
.github/copilot-instructions.md
vendored
Normal file
50
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copilot Instructions for AI Coding Agents
|
||||
|
||||
## Project Overview
|
||||
This is a TypeScript-based Discord bot project, organized for modularity and clarity. The bot is structured to support command handling, event listeners, web endpoints, and startup routines.
|
||||
|
||||
## Architecture & Key Components
|
||||
- **src/**: Main source directory.
|
||||
- **commands/**: Each file implements a bot command (e.g., `ping.ts`).
|
||||
- **handlers/**: Contains logic for registering commands and startup routines.
|
||||
- **listeners/**: Event-driven handlers for Discord events, organized by event type (e.g., `GuildMemberAdd`, `messageCreate`).
|
||||
- **lib/**: Shared utilities for command registration and startup handler loading.
|
||||
- **web/**: Web endpoints for bot-related actions (e.g., `gitCommit.ts`).
|
||||
- **config.ts**: Central configuration file.
|
||||
- **types.ts**: Project-specific type definitions.
|
||||
- **webserver.ts**: Entry point for the web server.
|
||||
- **index.ts**: Main bot entry point.
|
||||
|
||||
## Developer Workflows
|
||||
- **Build & Run**: Use Node.js with `pnpm` for dependency management. Start the bot with `pnpm start` or `node src/index.ts`.
|
||||
- **Debugging**: Modular structure allows targeted debugging. Focus on the relevant handler or listener for the event/command.
|
||||
- **No tests or linting scripts**: If needed, add them to `package.json`.
|
||||
|
||||
## Project Conventions
|
||||
- **Event listeners** are grouped by event type in subfolders under `listeners/`.
|
||||
- **Commands** are implemented as individual files in `commands/` and registered via `handlers/registerCommands.ts`.
|
||||
- **Startup routines** are in `handlers/startup/` and loaded via `lib/loadStartupHandlers.ts`.
|
||||
- **Web endpoints** are in `web/` and served by `webserver.ts`.
|
||||
- **Type definitions** are centralized in `types.ts`.
|
||||
- **Configuration** is managed in `config.ts`.
|
||||
|
||||
## Integration & Communication
|
||||
- **Discord.js** (or similar) is assumed for bot interaction, though not explicitly shown.
|
||||
- **Webserver** integrates with bot logic via endpoints in `web/`.
|
||||
- **Command and event registration** is handled via utility files in `lib/` and `handlers/`.
|
||||
|
||||
## Examples
|
||||
- To add a new command: Create a file in `commands/`, then ensure it's registered in `handlers/registerCommands.ts`.
|
||||
- To handle a new Discord event: Add a handler in the appropriate `listeners/<Event>/` folder.
|
||||
- To add a startup routine: Implement in `handlers/startup/` and register via `lib/loadStartupHandlers.ts`.
|
||||
|
||||
## References
|
||||
- [src/index.ts](src/index.ts): Bot entry point
|
||||
- [src/webserver.ts](src/webserver.ts): Web server entry point
|
||||
- [src/handlers/registerCommands.ts](src/handlers/registerCommands.ts): Command registration
|
||||
- [src/lib/commandRegistry.ts](src/lib/commandRegistry.ts): Command registry utility
|
||||
- [src/listeners/messageCreate/logMessage.ts](src/listeners/messageCreate/logMessage.ts): Example event handler
|
||||
|
||||
---
|
||||
|
||||
**Review and update these instructions as the project evolves. If any section is unclear or incomplete, please provide feedback for improvement.**
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.env
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
bot:
|
||||
image: node:24
|
||||
container_name: shsf_dc_bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "$PORT:$PORT"
|
||||
command: sh -c "npm i -g pnpm && pnpm install && pnpm dev"
|
||||
533
package-lock.json
generated
Normal file
533
package-lock.json
generated
Normal file
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"name": "shsf-dc-bot",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "shsf-dc-bot",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.16.3",
|
||||
"dotenv": "^16.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
||||
"integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
|
||||
"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.33",
|
||||
"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==",
|
||||
"license": "Apache-2.0",
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
|
||||
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.1.1",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.3",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.16",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.3"
|
||||
},
|
||||
"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/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/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"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/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"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/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.40",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz",
|
||||
"integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.25.1",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
|
||||
"integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.13.0",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/rest": "^2.6.0",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.38.33",
|
||||
"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": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"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/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"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/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "shsf-dc-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "Discord bot built with Discord.js and TypeScript",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"discord.js": "^14.16.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.2.1",
|
||||
"mongodb": "~7.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
1097
pnpm-lock.yaml
generated
Normal file
1097
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
src/commands/ping.ts
Normal file
13
src/commands/ping.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Client, SlashCommandBuilder } from "discord.js";
|
||||
import { registerCommand } from "../lib/commandRegistry";
|
||||
|
||||
export default async function (_client: Client) {
|
||||
registerCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("ping")
|
||||
.setDescription("Replies with Pong!"),
|
||||
async execute(interaction) {
|
||||
await interaction.reply("Pong!");
|
||||
},
|
||||
});
|
||||
}
|
||||
29
src/config.ts
Normal file
29
src/config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ActivityType } from "discord.js";
|
||||
|
||||
|
||||
|
||||
export const GUILD_ID = '1475098530505953441';
|
||||
|
||||
|
||||
export const CHANNELS = {
|
||||
RULES: "1475100731991392539",
|
||||
DEV: "1475110775235543092",
|
||||
ANNOUNCEMENTS: "1475100626286547004",
|
||||
UPDATES: "1475101645963661333",
|
||||
DEV_CHAT: "1475101776561569875",
|
||||
GENERAL: "1475098531814707343",
|
||||
DEV_MEMES: "1475101184069992670",
|
||||
MODERATOR_ONLY: "1475100731991392542",
|
||||
};
|
||||
|
||||
export const ROLES = {
|
||||
RULES: "1475100051352191047",
|
||||
};
|
||||
|
||||
// Discord modified the way activities work, for now, we'll only use custom ones
|
||||
export const ROTATE_ACTIVITIES:{content:string;type:ActivityType}[] = [
|
||||
{content: "Fixing Bugs", type: ActivityType.Custom},
|
||||
{content: "Adding New Features", type: ActivityType.Custom},
|
||||
{content: "Improving Performance", type: ActivityType.Custom},
|
||||
{content: "Listening to Feedback", type: ActivityType.Custom},
|
||||
]
|
||||
36
src/handlers/registerCommands.ts
Normal file
36
src/handlers/registerCommands.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Client, Events, REST, Routes } from "discord.js";
|
||||
import { getAllCommands, getCommand } from "../lib/commandRegistry";
|
||||
import { GUILD_ID } from "../config";
|
||||
|
||||
export default async function (client: Client) {
|
||||
const token = process.env.DISCORD_TOKEN!;
|
||||
const clientId = client.user!.id; // use the ready client's ID
|
||||
|
||||
const rest = new REST().setToken(token);
|
||||
|
||||
const commandData = getAllCommands().map((cmd) => cmd.data.toJSON());
|
||||
|
||||
await rest.put(Routes.applicationGuildCommands(clientId, GUILD_ID), {
|
||||
body: commandData,
|
||||
});
|
||||
|
||||
console.log(`Registered ${commandData.length} slash command(s).`);
|
||||
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = getCommand(interaction.commandName);
|
||||
if (!command) return;
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: "An error occurred.", ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred.", ephemeral: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
155
src/handlers/startup/checkRules.ts
Normal file
155
src/handlers/startup/checkRules.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
Client,
|
||||
EmbedBuilder,
|
||||
Events,
|
||||
GuildMember,
|
||||
Partials,
|
||||
TextChannel,
|
||||
} from "discord.js";
|
||||
import { CHANNELS, GUILD_ID, ROLES } from "../../config";
|
||||
|
||||
const RULES_EMOJI = "✅";
|
||||
|
||||
export default async function checkRules(client: Client): Promise<void> {
|
||||
const guild = await client.guilds.fetch(GUILD_ID);
|
||||
const channel = (await guild.channels.fetch(
|
||||
CHANNELS["RULES"],
|
||||
)) as TextChannel;
|
||||
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
console.error(
|
||||
"[checkRules] Rules channel not found or is not a text channel.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for an existing rules message posted by the bot
|
||||
const messages = await channel.messages.fetch({ limit: 50 });
|
||||
const existingMessage = messages.find((m) => m.author.id === client.user!.id);
|
||||
|
||||
if (!existingMessage) {
|
||||
const embed1 = new EmbedBuilder()
|
||||
.setTitle("📜 Server Rules — Part 1: General Conduct")
|
||||
.setColor(0x5865f2)
|
||||
.setDescription(
|
||||
"Please read and follow all rules to keep this server a safe and welcoming place for everyone.",
|
||||
)
|
||||
.addFields(
|
||||
{
|
||||
name: "1. Be Respectful",
|
||||
value:
|
||||
"Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.",
|
||||
},
|
||||
{
|
||||
name: "2. No Spam",
|
||||
value:
|
||||
"Do not spam messages, emojis, or mentions. Keep conversations relevant to the channel topic.",
|
||||
},
|
||||
{
|
||||
name: "3. No NSFW Content",
|
||||
value:
|
||||
"Explicit, graphic, or otherwise inappropriate content is strictly prohibited.",
|
||||
},
|
||||
{
|
||||
name: "4. No Self-Promotion",
|
||||
value:
|
||||
"Do not advertise other servers, social media accounts, or services without prior approval from staff.",
|
||||
},
|
||||
{
|
||||
name: "5. Follow Discord ToS",
|
||||
value:
|
||||
"All members must comply with [Discord's Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines).",
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
const embed2 = new EmbedBuilder()
|
||||
.setTitle("📋 Server Rules — Part 2: Channels & Community")
|
||||
.setColor(0x57f287)
|
||||
.addFields(
|
||||
{
|
||||
name: "6. Use the Right Channels",
|
||||
value:
|
||||
"Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
|
||||
},
|
||||
{
|
||||
name: "7. No Doxxing",
|
||||
value:
|
||||
"Sharing personal or private information of others without their explicit consent is strictly forbidden.",
|
||||
},
|
||||
{
|
||||
name: "8. English in Main Channels",
|
||||
value:
|
||||
"Please communicate in English in main channels so all members and staff can participate.",
|
||||
},
|
||||
{
|
||||
name: "9. Listen to Staff",
|
||||
value:
|
||||
"Follow the instructions of moderators and admins. If you disagree with a decision, open a support ticket calmly.",
|
||||
},
|
||||
{
|
||||
name: "10. Have Fun!",
|
||||
value:
|
||||
"This is a community — be kind, stay positive, and enjoy your time here. 🎉",
|
||||
},
|
||||
)
|
||||
.setFooter({
|
||||
text: "React with ✅ below to accept the rules and gain access to the server.",
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
const rulesMessage = await channel.send({ embeds: [embed1, embed2] });
|
||||
await rulesMessage.react(RULES_EMOJI);
|
||||
console.log("[checkRules] Rules message posted successfully.");
|
||||
} else {
|
||||
console.log("[checkRules] Rules message already exists, skipping.");
|
||||
}
|
||||
|
||||
// Grant role when a member reacts with ✅ in the rules channel
|
||||
client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
||||
if (user.bot) return;
|
||||
if (reaction.message.channelId !== CHANNELS.RULES) return;
|
||||
if (reaction.emoji.name !== RULES_EMOJI) return;
|
||||
|
||||
// Resolve partials if needed
|
||||
try {
|
||||
if (reaction.partial) await reaction.fetch();
|
||||
if (user.partial) await user.fetch();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await guild.members.fetch(user.id);
|
||||
await member.roles.add(ROLES.RULES);
|
||||
console.log(`[checkRules] Granted rules role to ${user.tag ?? user.id}`);
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to add rules role:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove role when the reaction is removed
|
||||
client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
||||
if (user.bot) return;
|
||||
if (reaction.message.channelId !== CHANNELS.RULES) return;
|
||||
if (reaction.emoji.name !== RULES_EMOJI) return;
|
||||
|
||||
try {
|
||||
if (reaction.partial) await reaction.fetch();
|
||||
if (user.partial) await user.fetch();
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to fetch reaction or user:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await guild.members.fetch(user.id);
|
||||
await member.roles.remove(ROLES.RULES);
|
||||
console.log(
|
||||
`[checkRules] Removed rules role from ${user.tag ?? user.id}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to remove rules role:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
18
src/handlers/startup/rotatingActivity.ts
Normal file
18
src/handlers/startup/rotatingActivity.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ActivityType, Client } from "discord.js";
|
||||
import { ROTATE_ACTIVITIES } from "../../config";
|
||||
|
||||
export default async function rotatingActivity(client: Client): Promise<void> {
|
||||
if (!client.user) {
|
||||
console.error("[rotatingActivity] Client is not ready yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
client.user.setActivity("Fixing Bugs", { type: ActivityType.Custom });
|
||||
let index = 0;
|
||||
setInterval(() => {
|
||||
const activity = ROTATE_ACTIVITIES[index];
|
||||
client.user!.setActivity(activity.content, { type: activity.type });
|
||||
index = (index + 1) % ROTATE_ACTIVITIES.length;
|
||||
console.log(`[rotatingActivity] Updated activity to: ${activity.content} (${activity.type})`);
|
||||
}, 60000); // Rotate every 60 seconds
|
||||
}
|
||||
53
src/index.ts
Normal file
53
src/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Client, Events, GatewayIntentBits, Message, Partials } from 'discord.js';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import loadModulesFromDir from './lib/loadStartupHandlers';
|
||||
import * as mongoDB from "mongodb";
|
||||
import { env } from 'process';
|
||||
import webserver from './webserver';
|
||||
dotenv.config();
|
||||
|
||||
const dbclient: mongoDB.MongoClient = new mongoDB.MongoClient(
|
||||
env.MONGO_DB!
|
||||
);
|
||||
|
||||
const db: mongoDB.Db = dbclient.db(env.DB_NAME!);
|
||||
|
||||
export { db, dbclient };
|
||||
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction, Partials.User],
|
||||
});
|
||||
|
||||
let readyClient: typeof client | null = null;
|
||||
|
||||
client.once(Events.ClientReady, async (rc) => {
|
||||
readyClient = rc;
|
||||
console.log(`Logged in as ${rc.user.tag}`);
|
||||
|
||||
const dirs = [
|
||||
path.join(__dirname, 'commands'), // load commands first
|
||||
path.join(__dirname, 'handlers'),
|
||||
path.join(__dirname, 'listeners'),
|
||||
];
|
||||
|
||||
for (const dir of dirs) {
|
||||
await loadModulesFromDir(dir, client);
|
||||
}
|
||||
});
|
||||
|
||||
// Start webserver in the background
|
||||
webserver().catch(console.error);
|
||||
|
||||
// Exports
|
||||
export { client, readyClient };
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
20
src/lib/commandRegistry.ts
Normal file
20
src/lib/commandRegistry.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
export interface Command {
|
||||
data: SlashCommandBuilder;
|
||||
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
|
||||
}
|
||||
|
||||
const commands = new Map<string, Command>();
|
||||
|
||||
export function registerCommand(command: Command) {
|
||||
commands.set(command.data.name, command);
|
||||
}
|
||||
|
||||
export function getCommand(name: string): Command | undefined {
|
||||
return commands.get(name);
|
||||
}
|
||||
|
||||
export function getAllCommands(): Command[] {
|
||||
return Array.from(commands.values());
|
||||
}
|
||||
32
src/lib/loadStartupHandlers.ts
Normal file
32
src/lib/loadStartupHandlers.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Client } from "discord.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default async function loadModulesFromDir(dir: string, client: Client): Promise<void> {
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.warn(`[loadModules] Directory not found: ${dir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await loadModulesFromDir(fullPath, client); // recurse
|
||||
} else if (entry.isFile() && /\.(ts|js)$/.test(entry.name)) {
|
||||
try {
|
||||
const mod = await import(fullPath);
|
||||
if (typeof mod.default === "function") {
|
||||
await mod.default(client);
|
||||
console.log(`[loadModules] Loaded: ${fullPath}`);
|
||||
} else {
|
||||
console.warn(`[loadModules] No default export function in: ${entry.name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[loadModules] Failed to load ${entry.name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/listeners/GuildMemberAdd/addUser.ts
Normal file
36
src/listeners/GuildMemberAdd/addUser.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Client, Events, GuildMember } from "discord.js";
|
||||
import { db } from "../..";
|
||||
|
||||
// Adds a User to the database as they just joined the server. This is used for tracking purposes and to store user data in the future.
|
||||
export default async function addUserToDB(client: Client): Promise<void> {
|
||||
client.on(Events.GuildMemberAdd, async (member) => {
|
||||
console.log(`[addUserToDB] [${member.user.tag}] Joined the server`);
|
||||
|
||||
await db.collection("users").insertOne({
|
||||
userId: member.user.id,
|
||||
authorTag: member.user.tag,
|
||||
content: member.user.username,
|
||||
guildId: member.guild.id,
|
||||
timestamp: new Date(member.joinedTimestamp!),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Silenced becuase its very noisy on every message
|
||||
export async function addUserToDBManually(member: GuildMember): Promise<void> {
|
||||
// console.log(`[addUserToDBManually] [${member.user.tag}] Adding user to DB manually`);
|
||||
|
||||
const existingUser = await db.collection("users").findOne({ userId: member.user.id });
|
||||
if (existingUser) {
|
||||
// console.log(`[addUserToDBManually] [${member.user.tag}] User already exists in DB, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("users").insertOne({
|
||||
userId: member.user.id,
|
||||
authorTag: member.user.tag,
|
||||
content: member.user.username,
|
||||
guildId: member.guild.id,
|
||||
timestamp: new Date(member.joinedTimestamp!),
|
||||
});
|
||||
}
|
||||
14
src/listeners/GuildMemberRemove/removeUser.ts
Normal file
14
src/listeners/GuildMemberRemove/removeUser.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Client, Events, GuildMember } from "discord.js";
|
||||
import { db } from "../..";
|
||||
|
||||
// Removes a User from the database as they just left the server.
|
||||
export default async function removeUserFromDB(client: Client): Promise<void> {
|
||||
client.on(Events.GuildMemberRemove, async (member) => {
|
||||
console.log(`[removeUserFromDB] [${member.user.tag}] Left the server`);
|
||||
|
||||
await db.collection("users").deleteOne({
|
||||
userId: member.user.id,
|
||||
guildId: member.guild.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
20
src/listeners/messageCreate/helloWorld.ts
Normal file
20
src/listeners/messageCreate/helloWorld.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Client, Events, Message } from "discord.js";
|
||||
|
||||
export default async function helloWorld(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, (message: Message) => {
|
||||
// Ignore messages from bots
|
||||
if (message.author.bot) return;
|
||||
|
||||
if (message.content.toLowerCase().replace("!", "") === "hello world") {
|
||||
message.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: "Hello, World!",
|
||||
description: "Hello from the SHSF Team!",
|
||||
color: 0x00ff00,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
26
src/listeners/messageCreate/logMessage.ts
Normal file
26
src/listeners/messageCreate/logMessage.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Client, Events, Message } from "discord.js";
|
||||
import { db } from "../..";
|
||||
import { addUserToDBManually } from "../GuildMemberAdd/addUser";
|
||||
|
||||
export default async function logMessage(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (!message.guildId) return; // Only log messages from guilds (ignore DMs)
|
||||
if (!message.member) return; // Ignore messages without member info (should be rare)
|
||||
|
||||
// Log ALL message inlcuding bots
|
||||
console.log(`[logMessage] [${message.author.tag}] Sent a message (${message.content.length} chars)`);
|
||||
|
||||
await db.collection("messages").insertOne({
|
||||
messageId: message.id,
|
||||
authorId: message.author.id,
|
||||
authorTag: message.author.tag,
|
||||
content: message.content,
|
||||
channelId: message.channelId,
|
||||
guildId: message.guildId,
|
||||
timestamp: new Date(message.createdTimestamp),
|
||||
});
|
||||
|
||||
// Does this user exist in our database? If not, add them (this is for users who were in the server before the bot was added)
|
||||
await addUserToDBManually(message.member);
|
||||
});
|
||||
}
|
||||
0
src/types.ts
Normal file
0
src/types.ts
Normal file
96
src/web/gitCommit.ts
Normal file
96
src/web/gitCommit.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Express, Request, Response } from 'express';
|
||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||
import { CHANNELS } from '../config';
|
||||
import { client, db } from '../index';
|
||||
|
||||
const configured_channel = CHANNELS.UPDATES;
|
||||
|
||||
export default async function gitCommitPOST(app: Express) {
|
||||
app.post('/git-commit', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const event = req.headers['x-github-event'] as string;
|
||||
|
||||
// Acknowledge ping events
|
||||
if (event === 'ping') {
|
||||
console.log('[WEB-gitCommit] Received GitHub ping event');
|
||||
return res.status(200).json({ success: true, message: 'pong' });
|
||||
}
|
||||
|
||||
if (event !== 'push') {
|
||||
return res.status(200).json({ success: true, message: `Event '${event}' ignored` });
|
||||
}
|
||||
|
||||
const body = req.body;
|
||||
const repo = body.repository;
|
||||
const pusher = body.pusher;
|
||||
const commits: any[] = body.commits ?? [];
|
||||
const headCommit = body.head_commit;
|
||||
const ref: string = body.ref ?? '';
|
||||
const branch = ref.replace('refs/heads/', '');
|
||||
const compareUrl: string = body.compare ?? '';
|
||||
const forced: boolean = body.forced ?? false;
|
||||
|
||||
if (!repo || !headCommit) {
|
||||
return res.status(400).json({ error: 'Invalid push payload' });
|
||||
}
|
||||
|
||||
// Build commit list (max X)
|
||||
const SHOW_MAX = 5;
|
||||
const commitLines = commits.slice(0, SHOW_MAX).map((c: any) => {
|
||||
const shortId = c.id.substring(0, 7);
|
||||
const msg = c.message.split('\n')[0].substring(0, 64);
|
||||
return `[\`${shortId}\`](${c.url}) ${msg}...`;
|
||||
});
|
||||
|
||||
if (commits.length > SHOW_MAX) {
|
||||
commitLines.push(`...and ${commits.length - SHOW_MAX} more`);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(forced ? 0xff4444 : 0x2ea44f)
|
||||
.setTitle(`${forced ? '⚠️ Force Push' : '📦 New Push'} to \`${branch}\``)
|
||||
.setURL(compareUrl)
|
||||
.setAuthor({
|
||||
name: pusher.name,
|
||||
iconURL: `https://github.com/${pusher.name}.png`,
|
||||
url: `https://github.com/${pusher.name}`,
|
||||
})
|
||||
.addFields(
|
||||
{ name: '🌿 Branch', value: `\`${branch}\``, inline: true },
|
||||
{ name: `📝 Commits (${commits.length})`, value: commitLines.join('\n') || '_No commits_' },
|
||||
)
|
||||
.setFooter({ text: `Delivery: ${req.headers['x-github-delivery'] ?? 'unknown'}` })
|
||||
.setTimestamp(headCommit.timestamp ? new Date(headCommit.timestamp) : new Date());
|
||||
|
||||
const channel = await client.channels.fetch(configured_channel) as TextChannel | null;
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
console.error('[WEB-gitCommit] Configured channel not found or not text-based');
|
||||
return res.status(500).json({ error: 'Discord channel unavailable' });
|
||||
}
|
||||
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
console.log(`[WEB-gitCommit] Push event sent to configured channel (${commits.length} commits on ${branch})`);
|
||||
|
||||
// Reactions for engagement
|
||||
await message.react('👍');
|
||||
await message.react('🔥');
|
||||
await message.react('🤯');
|
||||
|
||||
// Add to DB
|
||||
await db.collection('git_commits').insertOne({
|
||||
repository: repo.full_name,
|
||||
pusher: pusher.name,
|
||||
branch,
|
||||
commitCount: commits.length,
|
||||
compareUrl,
|
||||
forced,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
return res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[WEB-gitCommit] Error handling git commit:', error);
|
||||
return res.status(500).json({ success: false, error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
11
src/web/index.ts
Normal file
11
src/web/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Express, Request, Response } from "express";
|
||||
|
||||
export default async function gitCommitPOST(app: Express) {
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.status(200).json({ message: "Hello, world!" });
|
||||
});
|
||||
|
||||
app.post("/", (req: Request, res: Response) => {
|
||||
res.status(200).json({ message: "Hello, world!" });
|
||||
});
|
||||
}
|
||||
43
src/webserver.ts
Normal file
43
src/webserver.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export default async function webserver() {
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const webDir = path.join(__dirname, 'web');
|
||||
if (fs.existsSync(webDir)) {
|
||||
const files = fs.readdirSync(webDir).filter(f => f.endsWith('.ts') || f.endsWith('.js'));
|
||||
for (const file of files) {
|
||||
const mod = await import(path.join(webDir, file));
|
||||
const handler = mod.default ?? mod;
|
||||
if (typeof handler === 'function') {
|
||||
await handler(app);
|
||||
console.log(`[WEB] Loaded web route: ${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[WEB] Web server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
const IgnoredPaths = ['/favicon.ico', '/robots.txt', "/", "/hello"];
|
||||
const KnownPaths = ["/git-commit", "/"];
|
||||
|
||||
// log all incoming requests
|
||||
app.use((req, res, next) => {
|
||||
if (IgnoredPaths.includes(req.url)) {
|
||||
return next();
|
||||
}
|
||||
if (!KnownPaths.includes(req.url)) {
|
||||
console.warn(`[WEB] Unknown Route request: ${req.method} ${req.url} {${req.ip}}`);
|
||||
} else {
|
||||
console.log(`[WEB] Trusted Route request: ${req.method} ${req.url} {${req.ip}}`);
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"alwaysStrict": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": false,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user